From a860b0c0635500e628f91d26082a5df477f454e7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jesper=20=C3=96qvist?= <jesper@llbit.se>
Date: Tue, 25 Apr 2017 23:25:55 +0200
Subject: [PATCH] Basic JastAdd Docs

This is an Angular 4.0 application for viewing JastAdd documentation.

CodeMirror is used to display JastAdd source code.
---
 .angular-cli.json                             |   8 +-
 .gitignore                                    |   2 +
 README.md                                     |   4 +-
 TODO                                          |   5 +
 package.json                                  |   3 +-
 src/app/app.component.css                     | 118 +++++++++++++++++
 src/app/app.component.html                    |   3 -
 src/app/app.component.spec.ts                 |  45 ++++---
 src/app/app.component.ts                      |  98 +++++++++++++-
 src/app/app.module.ts                         |  44 ++++--
 .../declared-at/declared-at.component.spec.ts |  25 ++++
 src/app/declared-at/declared-at.component.ts  |  26 ++++
 src/app/doc.ts                                |  26 ++++
 src/app/editor.directive.spec.ts              |   8 ++
 src/app/editor.directive.ts                   |  35 +++++
 src/app/inherited-members.ts                  |  13 ++
 src/app/member-filter.service.spec.ts         |  15 +++
 src/app/member-filter.service.ts              |  15 +++
 src/app/member.ts                             |  40 ++++++
 src/app/name-filter.pipe.ts                   |  15 +++
 src/app/package-entry.ts                      |  11 ++
 src/app/package.service.ts                    |  20 +++
 src/app/package.ts                            |  12 ++
 src/app/parameter.ts                          |  14 ++
 src/app/parameters.component.ts               |  34 +++++
 src/app/selection.service.ts                  |  13 ++
 src/app/source-view/source-view.component.css |   0
 .../source-view/source-view.component.html    |   3 +
 .../source-view/source-view.component.spec.ts |  25 ++++
 src/app/source-view/source-view.component.ts  |  30 +++++
 src/app/source.service.spec.ts                |  15 +++
 src/app/source.service.ts                     |  17 +++
 src/app/string-filter.pipe.spec.ts            |   8 ++
 src/app/string-filter.pipe.ts                 |  15 +++
 src/app/type-details.component.css            |  83 ++++++++++++
 src/app/type-details.component.html           | 125 ++++++++++++++++++
 src/app/type-details.component.ts             |  81 ++++++++++++
 src/app/type-ref.component.ts                 |  48 +++++++
 src/app/type-ref.ts                           |  54 ++++++++
 src/app/type.service.ts                       |  20 +++
 src/app/type.ts                               |  69 ++++++++++
 src/assets/arrow_down_24px.svg                |  66 +++++++++
 src/assets/hamburger_24px.svg                 |  81 ++++++++++++
 src/assets/hdots_24px.svg                     |  82 ++++++++++++
 src/assets/search_grey_24px.svg               |  65 +++++++++
 src/assets/search_white_24px.svg              |  65 +++++++++
 src/assets/vdots_24px.svg                     |  82 ++++++++++++
 src/favicon.ico                               | Bin 5430 -> 1150 bytes
 src/index.html                                |   3 +-
 src/styles.css                                |   6 +
 50 files changed, 1652 insertions(+), 43 deletions(-)
 create mode 100644 TODO
 delete mode 100644 src/app/app.component.html
 create mode 100644 src/app/declared-at/declared-at.component.spec.ts
 create mode 100644 src/app/declared-at/declared-at.component.ts
 create mode 100644 src/app/doc.ts
 create mode 100644 src/app/editor.directive.spec.ts
 create mode 100644 src/app/editor.directive.ts
 create mode 100644 src/app/inherited-members.ts
 create mode 100644 src/app/member-filter.service.spec.ts
 create mode 100644 src/app/member-filter.service.ts
 create mode 100644 src/app/member.ts
 create mode 100644 src/app/name-filter.pipe.ts
 create mode 100644 src/app/package-entry.ts
 create mode 100644 src/app/package.service.ts
 create mode 100644 src/app/package.ts
 create mode 100644 src/app/parameter.ts
 create mode 100644 src/app/parameters.component.ts
 create mode 100644 src/app/selection.service.ts
 create mode 100644 src/app/source-view/source-view.component.css
 create mode 100644 src/app/source-view/source-view.component.html
 create mode 100644 src/app/source-view/source-view.component.spec.ts
 create mode 100644 src/app/source-view/source-view.component.ts
 create mode 100644 src/app/source.service.spec.ts
 create mode 100644 src/app/source.service.ts
 create mode 100644 src/app/string-filter.pipe.spec.ts
 create mode 100644 src/app/string-filter.pipe.ts
 create mode 100644 src/app/type-details.component.css
 create mode 100644 src/app/type-details.component.html
 create mode 100644 src/app/type-details.component.ts
 create mode 100644 src/app/type-ref.component.ts
 create mode 100644 src/app/type-ref.ts
 create mode 100644 src/app/type.service.ts
 create mode 100644 src/app/type.ts
 create mode 100644 src/assets/arrow_down_24px.svg
 create mode 100644 src/assets/hamburger_24px.svg
 create mode 100644 src/assets/hdots_24px.svg
 create mode 100644 src/assets/search_grey_24px.svg
 create mode 100644 src/assets/search_white_24px.svg
 create mode 100644 src/assets/vdots_24px.svg

diff --git a/.angular-cli.json b/.angular-cli.json
index 98a7d80..9bb6f94 100644
--- a/.angular-cli.json
+++ b/.angular-cli.json
@@ -8,6 +8,7 @@
       "root": "src",
       "outDir": "dist",
       "assets": [
+        "data",
         "assets",
         "favicon.ico"
       ],
@@ -19,9 +20,12 @@
       "testTsconfig": "tsconfig.spec.json",
       "prefix": "app",
       "styles": [
-        "styles.css"
+        "styles.css",
+        "../node_modules/codemirror/lib/codemirror.css",
+        "../node_modules/codemirror/theme/mbo.css"
+      ],
+      "scripts": [
       ],
-      "scripts": [],
       "environmentSource": "environments/environment.ts",
       "environments": {
         "dev": "environments/environment.ts",
diff --git a/.gitignore b/.gitignore
index 54bfd20..5198bf6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
 # See http://help.github.com/ignore-files/ for more about ignoring files.
 
+/src/data/
+
 # compiled output
 /dist
 /tmp
diff --git a/README.md b/README.md
index 45d47d7..397cf56 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,6 @@
-# RdView
+# RagDoc View
+
+This is a viewer for JastAdd documentation generated by RagDoc Builder.
 
 This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.0.1.
 
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..56b233b
--- /dev/null
+++ b/TODO
@@ -0,0 +1,5 @@
+* add thrown type descriptions
+* add type hierarchy in type details
+* add structured production representation
+* make current member filter more noticeable
+* Add declared-at info for types.
diff --git a/package.json b/package.json
index 7db8573..e19d786 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,8 @@
     "@angular/router": "^4.0.0",
     "core-js": "^2.4.1",
     "rxjs": "^5.1.0",
-    "zone.js": "^0.8.4"
+    "zone.js": "^0.8.4",
+    "codemirror": "5.25.2"
   },
   "devDependencies": {
     "@angular/cli": "1.0.1",
diff --git a/src/app/app.component.css b/src/app/app.component.css
index e69de29..fec4f10 100644
--- a/src/app/app.component.css
+++ b/src/app/app.component.css
@@ -0,0 +1,118 @@
+nav, article, h1 {
+  font-family: "Roboto",Helvetica,sans-serif;
+}
+.selected {
+  background-color: #BBD8DC !important;
+}
+.types {
+  list-style-type: none;
+  margin: 0;
+  padding: 0;
+  background-color: #607D8B;
+}
+.types li {
+  cursor: pointer;
+  position: relative;
+  left: 0;
+  background-color: #EEE;
+  margin: 1px 0;
+  padding: .3em 0 0 .7em;
+  height: 1.6em;
+}
+.types li:hover {
+  background-color: #CFD8DC;
+}
+.type .text {
+  position: relative;
+  top: -3px;
+}
+.topnav {
+  z-index: 5;
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  box-sizing: border-box;
+  height: 65px;
+  background-color: #CFD8DC;
+  box-shadow: 0 0 10px grey;
+}
+.topnav img {
+  visibility: hidden;
+  display: inline-block;
+  box-sizing: border-box;
+  width: 65px;
+  height: 65px;
+  padding: 15px;
+  position: absolute;
+}
+.topnav h1 {
+  display: inline-block;
+  vertical-align: middle;
+  box-sizing: border-box;
+  line-height: 65px;
+  padding: 0;
+  margin: 0 0 0 1em;
+  pointer-events: none;
+  white-space: nowrap;
+  overflow: hidden;
+}
+.sidenav {
+  width: 16em;
+  overflow-y: auto;
+  overflow-x: hidden;
+  z-index: 4;
+  position: fixed;
+  top: 65px;
+  left: 0;
+  bottom: 0;
+  box-shadow: 0 0 8px #888888;
+  background: white;
+}
+.package {
+  padding: .5em;
+}
+.group {
+  cursor: pointer;
+  padding: .5em .5em .5em 1em;
+  font-weight: bold;
+}
+.group img {
+  float: right;
+}
+article {
+  margin-left: 16em;
+  margin-top: 65px;
+  margin-bottom: 4em;
+  padding-top: 1em;
+  padding-left: 1em;
+}
+.search {
+  padding-left: 1em;
+  padding-top: 1em;
+  padding-right: 1em;
+  padding-bottom: 0.7em;
+}
+.search input {
+  border: 4px solid grey;
+  border-radius: 4px;
+  background: white url('../assets/search_grey_24px.svg') 10px 8px no-repeat;
+  padding: 10px 20px 10px 40px;
+  font-size: 16px;
+  width: 144px;
+}
+
+@media all and (max-width: 1024px) {
+  .hidemenu {
+    visibility: hidden;
+  }
+  .topnav h1 {
+    padding: 0 0 0 64px;
+  }
+  .topnav img {
+    visibility: visible;
+  }
+  article {
+    margin-left: 1em;
+  }
+}
diff --git a/src/app/app.component.html b/src/app/app.component.html
deleted file mode 100644
index b6931b5..0000000
--- a/src/app/app.component.html
+++ /dev/null
@@ -1,3 +0,0 @@
-<h1>
-  {{title}}
-</h1>
diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts
index c740bcd..7769024 100644
--- a/src/app/app.component.spec.ts
+++ b/src/app/app.component.spec.ts
@@ -1,32 +1,33 @@
-import { TestBed, async } from '@angular/core/testing';
-
 import { AppComponent } from './app.component';
 
-describe('AppComponent', () => {
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { By }           from '@angular/platform-browser';
+import { DebugElement } from '@angular/core';
+
+describe('AppComponent', function () {
+  let de: DebugElement;
+  let comp: AppComponent;
+  let fixture: ComponentFixture<AppComponent>;
+
   beforeEach(async(() => {
     TestBed.configureTestingModule({
-      declarations: [
-        AppComponent
-      ],
-    }).compileComponents();
+      declarations: [ AppComponent ]
+    })
+    .compileComponents();
   }));
 
-  it('should create the app', async(() => {
-    const fixture = TestBed.createComponent(AppComponent);
-    const app = fixture.debugElement.componentInstance;
-    expect(app).toBeTruthy();
-  }));
+  beforeEach(() => {
+    fixture = TestBed.createComponent(AppComponent);
+    comp = fixture.componentInstance;
+    de = fixture.debugElement.query(By.css('h1'));
+  });
 
-  it(`should have as title 'app works!'`, async(() => {
-    const fixture = TestBed.createComponent(AppComponent);
-    const app = fixture.debugElement.componentInstance;
-    expect(app.title).toEqual('app works!');
-  }));
+  it('should create component', () => expect(comp).toBeDefined() );
 
-  it('should render title in a h1 tag', async(() => {
-    const fixture = TestBed.createComponent(AppComponent);
+  it('should have expected <h1> text', () => {
     fixture.detectChanges();
-    const compiled = fixture.debugElement.nativeElement;
-    expect(compiled.querySelector('h1').textContent).toContain('app works!');
-  }));
+    const h1 = de.nativeElement;
+    expect(h1.innerText).toMatch(/angular/i,
+      '<h1> should say something about "Angular"');
+  });
 });
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index ff63e05..1b084c3 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -1,10 +1,100 @@
 import { Component } from '@angular/core';
+import { OnInit } from '@angular/core';
+
+import { Package } from './package';
+import { Type } from './type';
+import { PackageService } from './package.service';
+import { SelectionService } from './selection.service';
+import { Member } from './member';
+import { Parameter } from './parameter';
+
+import { ActivatedRoute, Params } from '@angular/router';
+import { Location } from '@angular/common';
 
 @Component({
   selector: 'app-root',
-  templateUrl: './app.component.html',
-  styleUrls: ['./app.component.css']
+  styleUrls: ['./app.component.css'],
+  template: `
+<nav class="topnav">
+  <img src="assets/hamburger_24px.svg" (click)="showMenu = !showMenu"><h1>{{title}}</h1>
+</nav>
+
+<nav class="sidenav" [class.hidemenu]="!showMenu">
+  <div class="search"><input [(ngModel)]="filter" placeholder="Search..."></div>
+
+  <ng-template ngFor let-package [ngForOf]="packages">
+    <div *ngIf="filteredPackage(package)">
+      <div class="package"><b>{{package.name}}</b></div>
+      <ng-template ngFor let-group [ngForOf]="package.groups">
+        <div *ngIf="filteredGroup(group)">
+          <div class="group" (click)="group.hidden = !group.hidden">{{typeKindNames[group.kind]}}<img src="assets/vdots_24px.svg" *ngIf="!group.hidden"><img src="assets/hdots_24px.svg" *ngIf="group.hidden"></div>
+          <ul class="types" [hidden]="group.hidden">
+          <li *ngFor="let type of group.members | nameFilter:filter" class="type"
+            [class.selected]="type.id === selectedType"
+            [routerLink]="['/type', type.id]"
+            (click)="showMenu = false">
+            {{type.name}}
+          </li>
+          </ul>
+        </div>
+      </ng-template>
+    </div>
+  </ng-template>
+</nav>
+
+<article>
+    <router-outlet></router-outlet>
+</article>
+  `,
+  providers: [
+    PackageService,
+    SelectionService,
+  ],
 })
-export class AppComponent {
-  title = 'app works!';
+export class AppComponent implements OnInit {
+  title = 'ExtendJ API Documentation';
+  showMenu = false;
+  packages : Package[];
+  filter = '';
+  selectedType = '';
+
+  private typeKindNames = {
+    'ast-class': 'AST CLASSES',
+    'interface': 'INTERFACES',
+    'class': 'CLASSES',
+  };
+
+  constructor(private packageService: PackageService,
+      private selectionService: SelectionService) {
+    selectionService.selection$.subscribe(id => this.selectedType = id);
+  }
+
+  ngOnInit(): void {
+    this.packageService.getPackages().then(packages => this.packages = packages);
+  }
+
+  declaredAt(member: Member): string {
+    if (member.doc) {
+      return `${member.doc.ragFile}:${member.doc.line}`;
+    } else {
+      return "";
+    }
+  }
+
+  filteredPackage(pkg: Package): boolean {
+    var filter = this.filter.toLowerCase();
+    for (var i = 0; i < pkg.groups.length; i++) {
+      if (this.filteredGroup(pkg.groups[i])) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  filteredGroup(group: any): boolean {
+    var filter = this.filter.toLowerCase();
+    var filtered = group.members.filter(member => member.name.toLowerCase().indexOf(filter) >= 0);
+    return filtered.length > 0;
+  }
 }
+
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 67ae491..f65502b 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -1,20 +1,46 @@
+import { NgModule }      from '@angular/core';
 import { BrowserModule } from '@angular/platform-browser';
-import { NgModule } from '@angular/core';
 import { FormsModule } from '@angular/forms';
 import { HttpModule } from '@angular/http';
+import { RouterModule } from '@angular/router';
 
-import { AppComponent } from './app.component';
+import { AppComponent }  from './app.component';
+import { TypeDetailsComponent }  from './type-details.component';
+import { ParametersComponent }  from './parameters.component';
+import { TypeReferenceComponent } from './type-ref.component';
+import { NameFilterPipe } from './name-filter.pipe';
+import { StringFilterPipe } from './string-filter.pipe';
+import { SourceViewComponent } from './source-view/source-view.component';
+import { EditorDirective } from './editor.directive';
+import { DeclaredAtComponent } from './declared-at/declared-at.component';
 
 @NgModule({
-  declarations: [
-    AppComponent
-  ],
-  imports: [
+  imports:      [
     BrowserModule,
     FormsModule,
-    HttpModule
+    HttpModule,
+    RouterModule.forRoot([
+      {
+        path: 'type/:id',
+        component: TypeDetailsComponent
+      },
+      {
+        path: 'source/:filename/:line',
+        component: SourceViewComponent
+      }
+    ]),
+  ],
+  declarations: [
+    AppComponent,
+    TypeDetailsComponent,
+    ParametersComponent,
+    TypeReferenceComponent,
+    NameFilterPipe,
+    StringFilterPipe,
+    SourceViewComponent,
+    EditorDirective,
+    DeclaredAtComponent,
   ],
-  providers: [],
-  bootstrap: [AppComponent]
+  bootstrap:    [ AppComponent ]
 })
 export class AppModule { }
diff --git a/src/app/declared-at/declared-at.component.spec.ts b/src/app/declared-at/declared-at.component.spec.ts
new file mode 100644
index 0000000..806276d
--- /dev/null
+++ b/src/app/declared-at/declared-at.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { DeclaredAtComponent } from './declared-at.component';
+
+describe('DeclaredAtComponent', () => {
+  let component: DeclaredAtComponent;
+  let fixture: ComponentFixture<DeclaredAtComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ DeclaredAtComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(DeclaredAtComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/app/declared-at/declared-at.component.ts b/src/app/declared-at/declared-at.component.ts
new file mode 100644
index 0000000..12d3c65
--- /dev/null
+++ b/src/app/declared-at/declared-at.component.ts
@@ -0,0 +1,26 @@
+import { Component, OnInit, Input } from '@angular/core';
+
+import {Doc} from '../doc';
+
+@Component({
+  selector: 'declared-at',
+  template: `
+    Declared at <a [routerLink]="['/source', filename, line]">{{filepath}}:{{line}}.</a>
+  `,
+})
+export class DeclaredAtComponent implements OnInit {
+
+  @Input() doc: Doc;
+  filename: string;
+  filepath: string;
+  line: string;
+
+  constructor() { }
+
+  ngOnInit() {
+    this.filepath = this.doc.ragFile;
+    this.filename = this.filepath.replace(/\/|\\/g, '_');
+    this.line = String(this.doc.line);
+  }
+
+}
diff --git a/src/app/doc.ts b/src/app/doc.ts
new file mode 100644
index 0000000..d201353
--- /dev/null
+++ b/src/app/doc.ts
@@ -0,0 +1,26 @@
+export class Doc {
+  ast: string;
+  ragFile: string;
+  line: number;
+  description: string;
+  apilevel: string;
+  params: string[]
+
+  paramDesc(name: string): string {
+    if (this.params) {
+      for (var i = 0; i < this.params.length; i++) {
+        var param = this.params[i];
+        var index = param.indexOf(' ');
+        if (index >= 0 && param.substring(0, index) === name) {
+          return ' : ' + param.substring(index + 1);
+        }
+      }
+    }
+    return '';
+  }
+
+  static fromJson(json: any): Doc {
+    var obj = Object.create(Doc.prototype);
+    return Object.assign(obj, json);
+  }
+}
diff --git a/src/app/editor.directive.spec.ts b/src/app/editor.directive.spec.ts
new file mode 100644
index 0000000..f385018
--- /dev/null
+++ b/src/app/editor.directive.spec.ts
@@ -0,0 +1,8 @@
+import { EditorDirective } from './editor.directive';
+
+describe('EditorDirective', () => {
+  it('should create an instance', () => {
+    const directive = new EditorDirective();
+    expect(directive).toBeTruthy();
+  });
+});
diff --git a/src/app/editor.directive.ts b/src/app/editor.directive.ts
new file mode 100644
index 0000000..ee368c7
--- /dev/null
+++ b/src/app/editor.directive.ts
@@ -0,0 +1,35 @@
+import { Directive, ElementRef, Input, OnChanges } from '@angular/core';
+
+import * as CodeMirror from 'codemirror';
+
+import 'codemirror/mode/clike/clike';
+import 'codemirror/addon/selection/active-line';
+
+@Directive({
+  selector: '[appEditor]'
+})
+export class EditorDirective implements OnChanges {
+  editor: any;
+  @Input() sourceText: string;
+  @Input() sourceLine: number;
+
+  constructor(public element: ElementRef) {
+    this.editor = new CodeMirror.fromTextArea(element.nativeElement, {
+      mode: 'text/x-java',
+      theme: 'mbo',
+      lineNumbers: true,
+      styleActiveLine: true,
+      lineWrapping: false,
+    });
+    this.editor.setSize('100%', '100%');
+  }
+
+  ngOnChanges() {
+    this.editor.setValue(this.sourceText || 'no source');
+    if (this.sourceLine) {
+      var line = this.sourceLine - 1;
+      this.editor.scrollIntoView({line: line + 30, ch: 0});
+      this.editor.setCursor({line: line, ch: 0});
+    }
+  }
+}
diff --git a/src/app/inherited-members.ts b/src/app/inherited-members.ts
new file mode 100644
index 0000000..bb6cd91
--- /dev/null
+++ b/src/app/inherited-members.ts
@@ -0,0 +1,13 @@
+import {TypeRef} from './type-ref';
+
+export class InheritedMembers {
+  superclass: TypeRef;
+  members: string[];
+
+  static fromJson(json: any): InheritedMembers {
+    return {
+      superclass: TypeRef.fromJson(json.superclass),
+      members: json.members as string[],
+    };
+  }
+}
diff --git a/src/app/member-filter.service.spec.ts b/src/app/member-filter.service.spec.ts
new file mode 100644
index 0000000..13aa2f5
--- /dev/null
+++ b/src/app/member-filter.service.spec.ts
@@ -0,0 +1,15 @@
+import { TestBed, inject } from '@angular/core/testing';
+
+import { MemberFilterService } from './member-filter.service';
+
+describe('MemberFilterService', () => {
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [MemberFilterService]
+    });
+  });
+
+  it('should ...', inject([MemberFilterService], (service: MemberFilterService) => {
+    expect(service).toBeTruthy();
+  }));
+});
diff --git a/src/app/member-filter.service.ts b/src/app/member-filter.service.ts
new file mode 100644
index 0000000..0f44d01
--- /dev/null
+++ b/src/app/member-filter.service.ts
@@ -0,0 +1,15 @@
+import { Injectable } from '@angular/core';
+import {Subject} from 'rxjs/Subject';
+
+@Injectable()
+export class MemberFilterService {
+  private filter = new Subject<string>();
+
+  filter$ = this.filter.asObservable();
+
+  constructor() { }
+
+  setFilter(filter: string) {
+    this.filter.next(filter);
+  }
+}
diff --git a/src/app/member.ts b/src/app/member.ts
new file mode 100644
index 0000000..6327b45
--- /dev/null
+++ b/src/app/member.ts
@@ -0,0 +1,40 @@
+import {Parameter} from './parameter';
+import {Doc} from './doc';
+import {TypeRef} from './type-ref';
+
+export class Member {
+  name: string;
+  type: TypeRef;
+  doc: Doc;
+  parameters: Parameter[];
+  throws: TypeRef[];
+
+  static fromJson(json: any): Member {
+    var params: Parameter[] = [];
+    var doc: Doc = undefined;
+    if (json.doc) {
+      doc = Doc.fromJson(json.doc);
+    }
+    if (json.params) {
+      params = json.params as Parameter[];
+    }
+    var throws: TypeRef[] = undefined;
+    if (json.throws) {
+      throws = (json.throws as TypeRef[]).map(TypeRef.fromJson);
+    }
+    if (json.type) {
+      return Object.assign({}, json, {
+        type: TypeRef.fromJson(json.type),
+        parameters: params.map(param => Parameter.fromJson(param)),
+        doc: doc,
+        throws: throws,
+      });
+    } else {
+      return Object.assign({}, json, {
+        parameters: params.map(param => Parameter.fromJson(param)),
+        doc: doc,
+        throws: throws,
+      });
+    }
+  }
+}
diff --git a/src/app/name-filter.pipe.ts b/src/app/name-filter.pipe.ts
new file mode 100644
index 0000000..45e8434
--- /dev/null
+++ b/src/app/name-filter.pipe.ts
@@ -0,0 +1,15 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+  name: 'nameFilter'
+})
+
+export class NameFilterPipe implements PipeTransform {
+  transform(items: any[], filter: string): any {
+    if (!items || !filter) {
+      return items;
+    }
+    return items.filter(item => item.name.toLowerCase().indexOf(filter.toLowerCase()) >= 0);
+  }
+}
+
diff --git a/src/app/package-entry.ts b/src/app/package-entry.ts
new file mode 100644
index 0000000..716e779
--- /dev/null
+++ b/src/app/package-entry.ts
@@ -0,0 +1,11 @@
+import {TypeRef} from './type-ref';
+
+export class PackageEntry {
+  kind: string;
+  name: string;
+  id: string;
+
+  static fromJson(json: any): PackageEntry {
+    return json as PackageEntry;
+  }
+}
diff --git a/src/app/package.service.ts b/src/app/package.service.ts
new file mode 100644
index 0000000..b440d72
--- /dev/null
+++ b/src/app/package.service.ts
@@ -0,0 +1,20 @@
+import { Injectable } from '@angular/core';
+import { Headers, Http } from '@angular/http';
+
+import 'rxjs/add/operator/toPromise';
+
+import { Package } from './package';
+
+@Injectable()
+export class PackageService {
+  private packageUrl = 'data/packages.json';
+
+  constructor(private http: Http) { }
+
+  getPackages(): Promise<Package[]> {
+    return this.http.get(this.packageUrl)
+      .toPromise()
+      .then(response => (response.json().data as Package[]).map(pkg => Package.fromJson(pkg)))
+      .catch(res => Promise.reject(`Failed to load packages: ${res}`));
+  }
+}
diff --git a/src/app/package.ts b/src/app/package.ts
new file mode 100644
index 0000000..dd797eb
--- /dev/null
+++ b/src/app/package.ts
@@ -0,0 +1,12 @@
+import { PackageEntry } from './package-entry';
+
+export class Package {
+  name: string;
+  groups: any[];
+
+  static fromJson(json: any): Package {
+    return json as Package;
+  }
+}
+
+
diff --git a/src/app/parameter.ts b/src/app/parameter.ts
new file mode 100644
index 0000000..929502a
--- /dev/null
+++ b/src/app/parameter.ts
@@ -0,0 +1,14 @@
+import {TypeRef} from './type-ref';
+
+export class Parameter {
+  type: TypeRef;
+  name: string;
+
+  static fromJson(json: any): Parameter {
+    return {
+      type: TypeRef.fromJson(json.t),
+      name: json.n,
+    };
+  }
+}
+
diff --git a/src/app/parameters.component.ts b/src/app/parameters.component.ts
new file mode 100644
index 0000000..1bd2227
--- /dev/null
+++ b/src/app/parameters.component.ts
@@ -0,0 +1,34 @@
+import { Component, Input, ComponentFactoryResolver } from '@angular/core';
+
+import {Parameter} from './parameter';
+
+@Component({
+  selector: 'parameters',
+  styles: [`
+    .parameters {
+      display: inline;
+      height: 1.2em;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+    .parameter {
+      display: inline;
+    }
+    .sep {
+      display: inline;
+      margin-right: .3em;
+    }
+  `],
+  template: `
+    <div class="parameters">
+      <div *ngFor="let param of params; let isLast=last" class="parameter">
+        <type-ref [type]="param.type"></type-ref> {{param.name}}<div *ngIf="!isLast" class="sep">,</div>
+      </div>
+    </div>
+  `
+})
+export class ParametersComponent {
+  @Input() params : Parameter[];
+
+  constructor(private _componentFactoryResolver: ComponentFactoryResolver) { }
+}
diff --git a/src/app/selection.service.ts b/src/app/selection.service.ts
new file mode 100644
index 0000000..312d8cc
--- /dev/null
+++ b/src/app/selection.service.ts
@@ -0,0 +1,13 @@
+import {Injectable} from '@angular/core';
+import {Subject} from 'rxjs/Subject';
+
+@Injectable()
+export class SelectionService {
+  private selection = new Subject<string>();
+
+  selection$ = this.selection.asObservable();
+
+  select(id: string) {
+    this.selection.next(id);
+  }
+}
diff --git a/src/app/source-view/source-view.component.css b/src/app/source-view/source-view.component.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/source-view/source-view.component.html b/src/app/source-view/source-view.component.html
new file mode 100644
index 0000000..6c33c8c
--- /dev/null
+++ b/src/app/source-view/source-view.component.html
@@ -0,0 +1,3 @@
+<p>
+  source-view works!
+</p>
diff --git a/src/app/source-view/source-view.component.spec.ts b/src/app/source-view/source-view.component.spec.ts
new file mode 100644
index 0000000..bf324c2
--- /dev/null
+++ b/src/app/source-view/source-view.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SourceViewComponent } from './source-view.component';
+
+describe('SourceViewComponent', () => {
+  let component: SourceViewComponent;
+  let fixture: ComponentFixture<SourceViewComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ SourceViewComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(SourceViewComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/app/source-view/source-view.component.ts b/src/app/source-view/source-view.component.ts
new file mode 100644
index 0000000..52af73e
--- /dev/null
+++ b/src/app/source-view/source-view.component.ts
@@ -0,0 +1,30 @@
+import { Component, OnInit, ViewChild } from '@angular/core';
+import { ActivatedRoute, Params } from '@angular/router';
+
+import 'rxjs/add/operator/switchMap';
+
+import {SourceService} from '../source.service';
+
+@Component({
+  selector: 'app-source-viewer',
+  providers: [ SourceService ],
+  template: `<textarea appEditor [sourceText]="source" [sourceLine]="line">{{source}}</textarea>`,
+})
+export class SourceViewComponent implements OnInit {
+  source: string;
+  line = 0;
+
+  constructor(private sourceService: SourceService,
+      private route: ActivatedRoute) { }
+
+  ngOnInit() {
+    this.route.params.switchMap((params: Params) => {
+      this.line = +params['line'];
+      return this.sourceService.getSource(params['filename']);
+    })
+    .subscribe(source => {
+      this.source = source;
+    });
+  }
+
+}
diff --git a/src/app/source.service.spec.ts b/src/app/source.service.spec.ts
new file mode 100644
index 0000000..a7dd4a3
--- /dev/null
+++ b/src/app/source.service.spec.ts
@@ -0,0 +1,15 @@
+import { TestBed, inject } from '@angular/core/testing';
+
+import { SourceService } from './source.service';
+
+describe('SourceService', () => {
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      providers: [SourceService]
+    });
+  });
+
+  it('should ...', inject([SourceService], (service: SourceService) => {
+    expect(service).toBeTruthy();
+  }));
+});
diff --git a/src/app/source.service.ts b/src/app/source.service.ts
new file mode 100644
index 0000000..4bceace
--- /dev/null
+++ b/src/app/source.service.ts
@@ -0,0 +1,17 @@
+import { Injectable } from '@angular/core';
+import { Headers, Http } from '@angular/http';
+
+import 'rxjs/add/operator/toPromise';
+
+@Injectable()
+export class SourceService {
+
+  constructor(private http: Http) { }
+
+  getSource(filename: String): Promise<string> {
+    return this.http.get(`data/${filename}`)
+        .toPromise()
+        .then(response => response.text())
+        .catch(res => Promise.reject(`Failed to load source: ${name}: ${res}.`));
+  }
+}
diff --git a/src/app/string-filter.pipe.spec.ts b/src/app/string-filter.pipe.spec.ts
new file mode 100644
index 0000000..4068ad8
--- /dev/null
+++ b/src/app/string-filter.pipe.spec.ts
@@ -0,0 +1,8 @@
+import { StringFilterPipe } from './string-filter.pipe';
+
+describe('StringFilterPipe', () => {
+  it('create an instance', () => {
+    const pipe = new StringFilterPipe();
+    expect(pipe).toBeTruthy();
+  });
+});
diff --git a/src/app/string-filter.pipe.ts b/src/app/string-filter.pipe.ts
new file mode 100644
index 0000000..bd815e6
--- /dev/null
+++ b/src/app/string-filter.pipe.ts
@@ -0,0 +1,15 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+  name: 'stringFilter'
+})
+export class StringFilterPipe implements PipeTransform {
+
+  transform(items: any[], filter: string): any {
+    if (!items || !filter) {
+      return items;
+    }
+    return items.filter(item => item.toLowerCase().indexOf(filter.toLowerCase()) >= 0);
+  }
+
+}
diff --git a/src/app/type-details.component.css b/src/app/type-details.component.css
new file mode 100644
index 0000000..8787da5
--- /dev/null
+++ b/src/app/type-details.component.css
@@ -0,0 +1,83 @@
+.doc-signature {
+  display: inline-block;
+  height: 1.3em;
+  width: calc(100% - 50px); /* Avoids line wrap in summary. */
+  overflow: hidden;
+  vertical-align: middle;
+}
+.attribute-kind {
+  color: #888;
+}
+.member-type {
+  width: 12em;
+  overflow: hidden;
+  display: inline-block;
+  vertical-align: top;
+}
+.member-name {
+  font-weight: bold;
+}
+.doc-preview {
+  display: inline;
+  float: right;
+  width: 40%;
+  line-height: 1.2em;
+  height: 1.2em;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  vertical-align: top;
+}
+summary {
+  cursor: pointer;
+  padding: .5em 0 .5em 0;
+}
+.member-details {
+  padding-bottom: .5em;
+}
+.members {
+  list-style-type: none;
+  margin: 0;
+  padding: 0;
+}
+.members li {
+  background-color: #EEE;
+  margin: .2em 0;
+  padding: 0 .7em 0 .7em;
+  border-radius: 4px;
+}
+.return {
+  display: inline-block;
+}
+.sep {
+  display: inline;
+  margin-right: .3em;
+}
+.filter input {
+  border: 4px solid grey;
+  border-radius: 4px;
+  background: white url('../assets/search_grey_24px.svg') 10px 8px no-repeat;
+  padding: 10px 20px 10px 40px;
+  font-size: 16px;
+  width: 144px;
+}
+.filter button {
+  border: 4px solid grey;
+  border-radius: 4px;
+  padding: 10px;
+  font-size: 16px;
+  background: lightgrey;
+}
+
+
+@media all and (max-width: 1024px) {
+  .doc-preview {
+    visibility: hidden;
+  }
+}
+@media all and (max-width: 500px) {
+  .member-type {
+    visibility: hidden;
+    width: 0;
+  }
+}
diff --git a/src/app/type-details.component.html b/src/app/type-details.component.html
new file mode 100644
index 0000000..93344b7
--- /dev/null
+++ b/src/app/type-details.component.html
@@ -0,0 +1,125 @@
+<div *ngIf="type">
+  <h2>{{type.kind}} {{type.name}}</h2>
+  <p *ngIf="type.superclass">extends <type-ref [type]="type.superclass"></type-ref>
+  <ng-container *ngIf="type.superinterfaces">implements <ng-container *ngFor="let iface of type.superinterfaces; let isLast = last"><type-ref [type]="iface"></type-ref><div *ngIf="!isLast" class="sep">, </div></ng-container></ng-container>
+  </p>
+  <div *ngIf="type.doc">
+    <p [innerHTML]="type.doc.description">
+
+    <p *ngIf="type.doc.astdecl">JastAdd production: {{type.doc.astdecl}}
+
+    <p *ngIf="type.doc && type.doc.ragFile"><declared-at [doc]="type.doc"></declared-at>
+  </div>
+  <div class="filter">
+    <input [(ngModel)]="filter" placeholder="Filter members...">
+    <button (click)="clearFilter()" [hidden]="filter === ''">Clear filter</button>
+  </div>
+
+  <div *ngIf="filteredMembers('constr')">
+  <h3>Constructors</h3>
+  <ul class="members">
+    <li *ngFor="let member of type.groups['constr'] | nameFilter:filter">
+      <details>
+      <summary>
+      <div class="doc-signature"><span class="member-name">{{member.name}}</span> ( <parameters [params]="member.parameters"></parameters> )
+        <div class="doc-preview" *ngIf="member.doc" [innerHTML]="member.doc.description"></div></div>
+      </summary>
+
+      <div class="member-details">
+      <p *ngIf="member.doc" [innerHTML]="member.doc.description"></p>
+      <p *ngIf="member.doc && member.doc.ragFile"><declared-at [doc]="member.doc"></declared-at>
+      <p *ngFor="let param of member.parameters; let index = index"><b>Parameter {{index+1}}</b> <type-ref [type]="param.type"></type-ref> <b>{{param.name}}</b><span *ngIf="member.doc" [innerHTML]="paramDesc(member.doc, param.name)"></span></p>
+      <p *ngIf="member.throws">Throws <ng-container *ngFor="let excp of member.throws; let isLast=last"><type-ref [type]="excp"></type-ref><div *ngIf="!isLast" class="sep">, </div></ng-container></p>
+      </div>
+      </details>
+    </li>
+  </ul>
+  </div>
+
+  <div *ngIf="filteredMembers('attr')">
+  <h3>Attributes</h3>
+  <ul class="members">
+    <li *ngFor="let member of type.groups['attr'] | nameFilter:filter">
+      <details>
+      <summary>
+      <div class="doc-signature"><div class="member-type"><span *ngIf="member.doc" class="attribute-kind">{{member.doc.attribute}}</span> <type-ref [type]="member.type"></type-ref></div> <span class="member-name">{{member.name}}</span> ( <parameters [params]="member.parameters"></parameters> )
+        <div class="doc-preview" *ngIf="member.doc" [innerHTML]="member.doc.description"></div></div>
+      </summary>
+
+      <div class="member-details">
+      <p *ngIf="member.doc" [innerHTML]="member.doc.description"></p>
+      <p *ngIf="member.doc && member.doc.ragFile"><declared-at [doc]="member.doc"></declared-at>
+    <p *ngFor="let param of member.parameters; let index = index"><b>Parameter {{index+1}}</b> <type-ref [type]="param.type"></type-ref> <b>{{param.name}}</b><span *ngIf="member.doc" [innerHTML]="paramDesc(member.doc, param.name)"></span></p>
+      <p><b>Returns</b> <type-ref [type]="member.type"></type-ref><ng-container *ngIf="member.doc && member.doc.return"> : <span class="return" *ngIf="member.doc" [innerHTML]="member.doc.return"></span></ng-container>
+      </div>
+      </details>
+    </li>
+  </ul>
+  </div>
+
+  <div *ngIf="type.inherited_attributes">
+    <ng-container *ngFor="let inherited of type.inherited_attributes">
+      <ng-container *ngIf="inheritedMembers(inherited)">
+      <h3>Attributes inherited from <type-ref [type]="inherited.superclass"></type-ref></h3>
+      <ng-container *ngFor="let member of inherited.members | stringFilter:filter; let isLast = last"><type-ref [type]="inherited.superclass" [name]="member" [filter]="member"></type-ref><ng-container *ngIf="!isLast">, </ng-container></ng-container>
+      </ng-container>
+    </ng-container>
+  </div>
+
+  <div *ngIf="filteredMembers('field')">
+  <h3>Fields</h3>
+  <ul class="members">
+    <li *ngFor="let member of type.groups['field'] | nameFilter:filter">
+      <details>
+      <summary>
+      <div class="doc-signature"><div class="member-type"><type-ref [type]="member.type"></type-ref></div> <span class="member-name">{{member.name}}</span>
+        <div class="doc-preview" *ngIf="member.doc" [innerHTML]="member.doc.description"></div></div>
+      </summary>
+
+      <p *ngIf="member.doc" [innerHTML]="member.doc.description"></p>
+      <p *ngIf="member.doc && member.doc.ragFile"><declared-at [doc]="member.doc"></declared-at>
+      </details>
+    </li>
+  </ul>
+  </div>
+
+  <div *ngIf="type.inherited_fields">
+    <ng-container *ngFor="let inherited of type.inherited_fields">
+      <ng-container *ngIf="inheritedMembers(inherited)">
+      <h3>Fields inherited from <type-ref [type]="inherited.superclass"></type-ref></h3>
+      <ng-container *ngFor="let member of inherited.members | stringFilter:filter; let isLast = last"><type-ref [type]="inherited.superclass" [name]="member" [filter]="member"></type-ref><ng-container *ngIf="!isLast">, </ng-container></ng-container>
+      </ng-container>
+    </ng-container>
+  </div>
+
+  <div *ngIf="filteredMembers('method')">
+  <h3>Methods</h3>
+  <ul class="members">
+    <li *ngFor="let member of type.groups['method'] | nameFilter:filter">
+      <details>
+      <summary>
+      <div class="doc-signature"><div class="member-type"><type-ref [type]="member.type"></type-ref></div> <span class="member-name">{{member.name}}</span> ( <parameters [params]="member.parameters"></parameters> )
+        <div class="doc-preview" *ngIf="member.doc" [innerHTML]="member.doc.description"></div></div>
+      </summary>
+
+      <div class="member-details">
+      <p *ngIf="member.doc" [innerHTML]="member.doc.description"></p>
+      <p *ngIf="member.doc && member.doc.ragFile"><declared-at [doc]="member.doc"></declared-at>
+      <p *ngFor="let param of member.parameters; let index = index"><b>Parameter {{index+1}}</b> <type-ref [type]="param.type"></type-ref> <b>{{param.name}}</b><span *ngIf="member.doc" [innerHTML]="paramDesc(member.doc, param.name)"></span></p>
+      <p><b>Returns</b> <type-ref [type]="member.type"></type-ref><ng-container *ngIf="member.doc && member.doc.return"> : <span class="return" *ngIf="member.doc" [innerHTML]="member.doc.return"></span></ng-container>
+      <p *ngIf="member.throws">Throws <ng-container *ngFor="let excp of member.throws; let isLast=last"><type-ref [type]="excp"></type-ref><div *ngIf="!isLast" class="sep">, </div></ng-container></p>
+      </div>
+      </details>
+    </li>
+  </ul>
+  </div>
+
+  <div *ngIf="type.inherited_methods">
+    <ng-container *ngFor="let inherited of type.inherited_methods">
+      <ng-container *ngIf="inheritedMembers(inherited)">
+      <h3>Methods inherited from <type-ref [type]="inherited.superclass"></type-ref></h3>
+      <ng-container *ngFor="let member of inherited.members | stringFilter:filter; let isLast = last"><type-ref [type]="inherited.superclass" [name]="member" [filter]="member"></type-ref><ng-container *ngIf="!isLast">, </ng-container></ng-container>
+      </ng-container>
+    </ng-container>
+  </div>
+</div>
diff --git a/src/app/type-details.component.ts b/src/app/type-details.component.ts
new file mode 100644
index 0000000..3d54cd6
--- /dev/null
+++ b/src/app/type-details.component.ts
@@ -0,0 +1,81 @@
+import { Component } from '@angular/core';
+import { OnInit } from '@angular/core';
+
+import { Package } from './package';
+import { Type } from './type';
+import { TypeService } from './type.service';
+import { MemberFilterService } from './member-filter.service';
+import { SelectionService } from './selection.service';
+import { Member } from './member';
+import { InheritedMembers } from './inherited-members';
+import {Doc} from './doc';
+import { Parameter } from './parameter';
+
+import { ActivatedRoute, Params } from '@angular/router';
+import { Location } from '@angular/common';
+
+import 'rxjs/add/operator/switchMap';
+
+@Component({
+  selector: 'type-details',
+  styleUrls: ['./type-details.component.css'],
+  templateUrl: './type-details.component.html',
+  providers: [
+    TypeService,
+    MemberFilterService,
+  ],
+})
+export class TypeDetailsComponent  implements OnInit {
+  type: Type = Object.create(Type.prototype);
+  filter: string = '';
+
+  constructor(private typeService: TypeService,
+      private memberFilterService: MemberFilterService,
+      private route: ActivatedRoute,
+      private location: Location,
+      private selectionService: SelectionService) {
+    memberFilterService.filter$.subscribe(filter => this.filter = filter);
+  }
+
+  ngOnInit() {
+    this.route.params.switchMap((params: Params) => {
+      return this.typeService.getType(params['id'])
+    })
+    .subscribe(type => {
+      this.selectionService.select(type.id);
+      this.type = type;
+    });
+  }
+
+  declaredAt(doc: Doc): string {
+    return `${doc.ragFile}:${doc.line}`;
+  }
+
+  paramDesc(doc: Doc, name: string): string {
+    if (doc) {
+      return doc.paramDesc(name);
+    }
+    return '';
+  }
+
+  filteredMembers(kind: string): boolean {
+    if (this.type.groups && this.type.groups[kind]) {
+      var filter = this.filter.toLowerCase();
+      var filtered = this.type.groups[kind].filter(item =>
+          item.name.toLowerCase().indexOf(filter) >= 0)
+      return filtered.length > 0;
+    }
+    return false;
+  }
+
+  inheritedMembers(inherited: InheritedMembers): boolean {
+    var filter = this.filter.toLowerCase();
+    var filtered = inherited.members.filter(item => item.toLowerCase().indexOf(filter) >= 0)
+    return filtered.length > 0;
+  }
+
+  clearFilter() {
+    this.filter = '';
+  }
+}
+
diff --git a/src/app/type-ref.component.ts b/src/app/type-ref.component.ts
new file mode 100644
index 0000000..34ad060
--- /dev/null
+++ b/src/app/type-ref.component.ts
@@ -0,0 +1,48 @@
+import { Component, Input, ComponentFactoryResolver } from '@angular/core';
+
+import {Type} from './type';
+import { MemberFilterService } from './member-filter.service';
+
+@Component({
+  selector: 'type-ref',
+  styles: [`
+    .sep {
+      display: inline;
+      margin-right: .3em;
+    }
+    .usertype {
+      color: #8c1339;
+      text-decoration: none;
+    }
+    .non-usertype {
+      color: #444;
+    }
+  `],
+  template: `<a *ngIf="type.id; else elseBlock" class="usertype" [routerLink]="['/type', type.id]" (click)="onClick()">{{getName()}}</a><!--
+  --><ng-template #elseBlock><span class="non-usertype">{{getName()}}</span></ng-template><!--
+    --><ng-container *ngIf="type.args && !name"><!--
+      -->&lt;<ng-container *ngFor="let arg of type.args; let isLast=last"><type-ref [type]="arg"></type-ref><div *ngIf="!isLast" class="sep">,</div></ng-container>&gt;<!--
+    --></ng-container>`
+})
+export class TypeReferenceComponent {
+  @Input() type : Type;
+  @Input() name : string;
+  @Input() filter : string;
+
+  constructor(private _componentFactoryResolver: ComponentFactoryResolver,
+    private memberFilterService: MemberFilterService) { }
+
+  getName(): string {
+    if (this.name) {
+      return this.name;
+    } else {
+      return this.type.name;
+    }
+  }
+
+  onClick() {
+    if (this.filter) {
+      this.memberFilterService.setFilter(this.filter);
+    }
+  }
+}
diff --git a/src/app/type-ref.ts b/src/app/type-ref.ts
new file mode 100644
index 0000000..154d4a6
--- /dev/null
+++ b/src/app/type-ref.ts
@@ -0,0 +1,54 @@
+export class TypeRef {
+  name: string;
+  id: string;
+  args: TypeRef[];
+
+  static fromJson(json: any): TypeRef {
+    var args: TypeRef[] = undefined;
+    if (json.a) {
+      args = (json.a as TypeRef[]).map(TypeRef.fromJson);
+    }
+    if (json.u) {
+      // User type.
+      if (json.i) {
+        return {
+          name: json.u,
+          id: TypeRef.typeId(json.u, json.i),
+          args: args,
+        };
+      } else {
+        return {
+          name: json.u,
+          id: TypeRef.simpleName(json.u),
+          args: args,
+        };
+      }
+    } else {
+      // Library or built-in type.
+      return {
+        name: json.n,
+        id: undefined,
+        args: args,
+      };
+    }
+  }
+
+  // Remove array and type args.
+  static simpleName(name: string): string {
+    return TypeRef.strip('<', TypeRef.strip('[', name));
+  }
+
+  static strip(tok: string, name: string): string {
+    const index = name.indexOf(tok);
+    if (index < 0) {
+      return name;
+    } else {
+      var simple = name.substring(0, index);
+      return simple;
+    }
+  }
+
+  static typeId(name: string, idPattern: string): string {
+    return idPattern.replace('%', TypeRef.simpleName(name));
+  }
+}
diff --git a/src/app/type.service.ts b/src/app/type.service.ts
new file mode 100644
index 0000000..cb9eebf
--- /dev/null
+++ b/src/app/type.service.ts
@@ -0,0 +1,20 @@
+import { Injectable } from '@angular/core';
+import { Headers, Http } from '@angular/http';
+
+import 'rxjs/add/operator/toPromise';
+
+import { Type } from './type';
+
+@Injectable()
+export class TypeService {
+
+  constructor(private http: Http) { }
+
+  getType(id: string): Promise<Type> {
+    const typeUrl = `data/${id}.json`;
+    return this.http.get(typeUrl)
+        .toPromise()
+        .then(response => Type.fromJson(response.json().data))
+        .catch(res => Promise.reject(`Failed to load type: ${id}: ${res}.`));
+  }
+}
diff --git a/src/app/type.ts b/src/app/type.ts
new file mode 100644
index 0000000..6ad43f6
--- /dev/null
+++ b/src/app/type.ts
@@ -0,0 +1,69 @@
+import {Doc} from './doc';
+import {Member} from './member';
+import {TypeRef} from './type-ref';
+import {InheritedMembers} from './inherited-members';
+
+export class Type {
+  kind: string;
+  name: string;
+  pkg: string;
+  mods: string[];
+  id: string;
+  doc: Doc;
+  groups: { [id: string] : Member[]; };
+  args: TypeRef[]; // Type arguments.
+  superclass: TypeRef;
+  superinterfaces: TypeRef[];
+  inherited_methods: InheritedMembers[];
+  inherited_attributes: InheritedMembers[];
+  inherited_fields: InheritedMembers[];
+
+  static fromJson(json: any): Type {
+    var groups = {};
+    if (json.groups) {
+      for (var i = 0; i < json.groups.length; i++) {
+        var group = json.groups[i];
+        if (group.members) {
+          groups[group.kind] = (group.members as Member[]).map(member => Member.fromJson(member));
+        }
+      }
+    }
+    var superclass: TypeRef = undefined;
+    if (json.superclass) {
+      superclass = TypeRef.fromJson(json.superclass);
+    }
+    var superinterfaces: TypeRef[] = undefined;
+    if (json.superinterfaces) {
+      superinterfaces = (json.superinterfaces as TypeRef[]).map(TypeRef.fromJson);
+    }
+    var inherited_methods: InheritedMembers[] = undefined;
+    if (json.inherited_methods) {
+      inherited_methods = (json.inherited_methods as any[]).map(inherited =>
+          InheritedMembers.fromJson(inherited));
+    }
+    var inherited_attributes: InheritedMembers[] = undefined;
+    if (json.inherited_attributes) {
+      inherited_attributes = (json.inherited_attributes as any[]).map(inherited =>
+          InheritedMembers.fromJson(inherited));
+    }
+    var inherited_fields: InheritedMembers[] = undefined;
+    if (json.inherited_fields) {
+      inherited_fields = (json.inherited_fields as any[]).map(inherited =>
+          InheritedMembers.fromJson(inherited));
+    }
+    var args: TypeRef[] = undefined;
+    if (json.args) {
+      args = (json.args as any[]).map(arg => TypeRef.fromJson(arg));
+    }
+    return Object.assign({}, json, {
+      groups: groups,
+      id: TypeRef.typeId(json.name, json.id),
+      superclass: superclass,
+      superinterfaces: superinterfaces,
+      inherited_methods: inherited_methods,
+      inherited_attributes: inherited_attributes,
+      inherited_fields: inherited_fields,
+    });
+  }
+}
+
diff --git a/src/assets/arrow_down_24px.svg b/src/assets/arrow_down_24px.svg
new file mode 100644
index 0000000..5630615
--- /dev/null
+++ b/src/assets/arrow_down_24px.svg
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="24"
+   height="24"
+   viewBox="0 0 24 24"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="arrow_down_24px.svg">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="31.678384"
+     inkscape:cx="5.4015737"
+     inkscape:cy="12.166913"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:snap-page="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1017"
+     inkscape:window-x="-8"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1028.3622)">
+    <path
+       style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#666666;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+       d="m 2.2910156,9.7011719 c -1.2697658,0 -2.30078123,1.0290281 -2.30078123,2.2988281 0,0.7108 0.32929283,1.337966 0.83593751,1.759766 l 7.92773432,7.929687 c 1.6731978,1.673198 4.3295148,1.76227 6.4648438,0 l 7.927734,-7.929687 C 23.653129,13.337966 23.982422,12.7108 23.982422,12 c 0,-1.2698 -1.031015,-2.2988281 -2.300781,-2.2988281 -0.672306,0 -1.271002,0.2938062 -1.691407,0.7539061 l -7.156209,7.154463 c -0.876709,1.039344 -0.910342,0.784861 -1.796652,-0.101233 L 3.9824219,10.455078 C 3.5620177,9.9949781 2.9633216,9.7011719 2.2910156,9.7011719 Z"
+       transform="translate(0,1028.3622)"
+       id="path4195"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ssccccssccccs" />
+  </g>
+</svg>
diff --git a/src/assets/hamburger_24px.svg b/src/assets/hamburger_24px.svg
new file mode 100644
index 0000000..19f1b5d
--- /dev/null
+++ b/src/assets/hamburger_24px.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="24"
+   height="24"
+   viewBox="0 0 24 24"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="hamburger_24px.svg">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="22.627417"
+     inkscape:cx="2.1195307"
+     inkscape:cy="9.0194555"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:snap-page="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1017"
+     inkscape:window-x="-8"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1028.3621)">
+    <rect
+       style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#666666;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+       id="rect4136"
+       width="24"
+       height="4.6875"
+       x="0"
+       y="1028.3621" />
+    <rect
+       y="1038.0182"
+       x="0"
+       height="4.6875"
+       width="24"
+       id="rect4163"
+       style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#666666;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+    <rect
+       style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#666666;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+       id="rect4165"
+       width="24"
+       height="4.6880002"
+       x="0"
+       y="1047.6741" />
+  </g>
+</svg>
diff --git a/src/assets/hdots_24px.svg b/src/assets/hdots_24px.svg
new file mode 100644
index 0000000..90e6351
--- /dev/null
+++ b/src/assets/hdots_24px.svg
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="24"
+   height="24"
+   viewBox="0 0 24 24"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="hdots_24px.svg">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="22.4"
+     inkscape:cx="3.1793573"
+     inkscape:cy="10.235898"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:snap-page="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1017"
+     inkscape:window-x="-8"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1028.3622)">
+    <g
+       id="g4164"
+       transform="matrix(0,1,-1,0,1052.3981,1027.706)"
+       style="fill:#666666">
+      <circle
+         r="2.2991071"
+         cy="1030.6882"
+         cx="12.656249"
+         id="path4136"
+         style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#666666;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+      <circle
+         style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#666666;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+         id="circle4160"
+         cx="12.656249"
+         cy="1040.3981"
+         r="2.2991071" />
+      <circle
+         r="2.2991071"
+         cy="1050.1079"
+         cx="12.656249"
+         id="circle4162"
+         style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#666666;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+    </g>
+  </g>
+</svg>
diff --git a/src/assets/search_grey_24px.svg b/src/assets/search_grey_24px.svg
new file mode 100644
index 0000000..468bbf8
--- /dev/null
+++ b/src/assets/search_grey_24px.svg
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="24"
+   height="24"
+   viewBox="0 0 24 24"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="search_grey_24px.svg">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="22.627417"
+     inkscape:cx="-4.9515371"
+     inkscape:cy="9.0194555"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:snap-page="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1017"
+     inkscape:window-x="-8"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1028.3621)">
+    <path
+       style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#666666;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;filter-blend-mode:normal;filter-gaussianBlur-deviation:0"
+       d="m 8.5185547,1028.9784 a 7.6455922,7.6455922 0 0 0 -7.64648437,7.6464 7.6455922,7.6455922 0 0 0 7.64648437,7.6465 7.6455922,7.6455922 0 0 0 3.7832033,-1.0078 l 8.482422,8.4824 2.34375,-2.3437 -8.371094,-8.3711 a 7.6455922,7.6455922 0 0 0 1.408203,-4.4063 7.6455922,7.6455922 0 0 0 -7.6464843,-7.6464 z m 0,2.5859 a 5.0602331,5.0602331 0 0 1 5.0605473,5.0605 5.0602331,5.0602331 0 0 1 -5.0605473,5.0606 5.0602331,5.0602331 0 0 1 -5.0605469,-5.0606 5.0602331,5.0602331 0 0 1 5.0605469,-5.0605 z"
+       id="path4155"
+       inkscape:connector-curvature="0" />
+  </g>
+</svg>
diff --git a/src/assets/search_white_24px.svg b/src/assets/search_white_24px.svg
new file mode 100644
index 0000000..14c170f
--- /dev/null
+++ b/src/assets/search_white_24px.svg
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="24"
+   height="24"
+   viewBox="0 0 24 24"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="search_white_24px.svg">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="22.627417"
+     inkscape:cx="2.1195307"
+     inkscape:cy="9.0194555"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:snap-bbox="true"
+     inkscape:snap-page="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1017"
+     inkscape:window-x="-8"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1028.3621)">
+    <path
+       style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+       d="m 8.5185547,1028.9784 a 7.6455922,7.6455922 0 0 0 -7.64648437,7.6464 7.6455922,7.6455922 0 0 0 7.64648437,7.6465 7.6455922,7.6455922 0 0 0 3.7832033,-1.0078 l 8.482422,8.4824 2.34375,-2.3437 -8.371094,-8.3711 a 7.6455922,7.6455922 0 0 0 1.408203,-4.4063 7.6455922,7.6455922 0 0 0 -7.6464843,-7.6464 z m 0,2.5859 a 5.0602331,5.0602331 0 0 1 5.0605473,5.0605 5.0602331,5.0602331 0 0 1 -5.0605473,5.0606 5.0602331,5.0602331 0 0 1 -5.0605469,-5.0606 5.0602331,5.0602331 0 0 1 5.0605469,-5.0605 z"
+       id="path4155"
+       inkscape:connector-curvature="0" />
+  </g>
+</svg>
diff --git a/src/assets/vdots_24px.svg b/src/assets/vdots_24px.svg
new file mode 100644
index 0000000..0d0f125
--- /dev/null
+++ b/src/assets/vdots_24px.svg
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="24"
+   height="24"
+   viewBox="0 0 24 24"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="vdots_24px.svg">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="22.4"
+     inkscape:cx="3.1793573"
+     inkscape:cy="10.235898"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:snap-page="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1017"
+     inkscape:window-x="-8"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1028.3622)">
+    <g
+       id="g4164"
+       transform="translate(-0.65624905,-0.03587129)"
+       style="fill:#666666">
+      <circle
+         r="2.2991071"
+         cy="1030.6882"
+         cx="12.656249"
+         id="path4136"
+         style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#666666;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+      <circle
+         style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#666666;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+         id="circle4160"
+         cx="12.656249"
+         cy="1040.3981"
+         r="2.2991071" />
+      <circle
+         r="2.2991071"
+         cy="1050.1079"
+         cx="12.656249"
+         id="circle4162"
+         style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#666666;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:7;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+    </g>
+  </g>
+</svg>
diff --git a/src/favicon.ico b/src/favicon.ico
index 8081c7ceaf2be08bf59010158c586170d9d2d517..49ef308e4ec2f538edbac743b32ea687fa81da4a 100644
GIT binary patch
literal 1150
zcmZQzU}Ruq5D);-3Je)63=Con3=A3!3=9Gc3=9ek5OD?)z`R}^+4-G14GX$-r_AkC
zmm)zc!_szL!3CW<?-q3G{0HIro!SK;{UCD|bZOTt=+yQD@p0ivlT_GI^mpi<SkR%f
zb3v!hACP$qI&}`C>s{2LZL^?LH+n&rZW;(L?9{mdRSR|l8vEbkc5M}8GZu7eZ$ndq
zO&?rrVW)N$s(#&#==#^s4f(%&ZN>lH>&pJGnCt-2+o7|2ewVfcvR)8-VW-Y2Wc?@h
z^!$JN^y&Y5_wN0F{P^+zCy(#^-#9Pi|NKs!QjmIdyr5Gjd104MJ}7(_cIo~5@a`3g
z{#UPF{l9Tx{r`ho>dv6+MdvT+vskx&>)!tnX|?}7B8&ffMHT-qZJ7A~{ISJgys%TJ
zYeA<@^@2{FJaoMv{=yF3qPcS?{%`G>{=d0*_W!2t>HlZVnfrh9l8pbm*Azg)0g^U$
zfz-qB{00S%McrEF3p#Xm!@^>5uPL&66!yYSoue?lAbMe^c05cUG4%Wn-4h^vFubfs
zO=Ll*&Tl+=pUv;kj#$*8?Y^*E*JW;pwmwWRj9$>GyB(_k_rgw{tqVJKE<pJpw||}4
zV9W(mi;Z5;spkxe<HfC-k|6m79opGQ`d`oK(-s1W6N^_hX!9)S)M=aFsap%mf7tZ>
ysApjK;LgDCft`V&ff<THY<>oY27U&H4~!6efRTX#h9B@VFg)UCVE8A`zyJUQb9{{e

literal 5430
zcmZQzU}Ruo5D;KsU|>*S$Y5b$Py?|PAp8{q3=DlN3=A3!3=AM8AYn!Z5Xr#czyeZ(
zivR!r&xj418$Jv*GrWJ_KzG-1BfZ1c*wkPXGdFx_X>R!NxVhoO|K^4d{u}6S1!F_q
z-D{2Yk1J!-0}?Yge5hz{_;58uA4o3<BlJVe+4kE|cTc~u{waQt9vB9>)!gvmZ-37x
z|HA^G{<km!=>=g_{h>jJ{(F1u`){DT<D-$@AzLK<?I3$6_rLoO0=XG4|AY9*?vIW*
z0+RaQ-g*V%AH6-*NcuN})H~Zg`v3LI-~W4ef5p(hW%HB&@813VZ(+I}tlvO)doPmy
zn;`ulykhC6|G$6#`+xJsPcXi5<Kur2{{8#!|7lZiLiKL_Z>YO_Ka&2>AiW?Q==1dd
zpFjWpKYjB1|CUXk|8Lyz`2XWaU;lsm_UFI7%`Uip1Kr(Ek@Wus>4o8ayTAT_`}WU&
zYtsk+t;~1)|M33j|D{XrBk2Xj>5iWu{pN-bm0|i|bV~ela9Gt=zWU$SbL~IK?Vj$S
za0lr_U=#h*{N{!aePQ~L>B|>?{J(PP$N$%_zyII5<tduo5HpPRk2{(hKFmhe2V+;2
zy#l-8-@pI=!-5ZC)o-MCIMv+nVJ%D#GHq%6@c*-?zyBXS@(Qb7P<k}f+f!|B_z>iV
ztH^p`?Eap2|5K7qVAXG^yX&rj?oL-w7=p05;X_Sx!-uWrh7X^^^xK+0MAN(LsiE%f
zR%89+`XD{HFw{-c%?$5<HqhPq+CX<#x3S(aOKj>H82<mK1cS;MQ0d~pz`y`2XBt2S
z4{9;PfX!}V#LNvJcAFXA{cfap;IXm(DSKk{P(zKm;lp@y!-pRsaRE#F+x{Er9X@TW
ze@dPjc3?BZ-0-2bx#7cy5PLyo7zo4chopN0-5vjp^^UGJ(Lc+L%?xtH%ncvPBFb%$
zoyZv7euz5^^>+U@)<51!j(spa=7tYB%?%%}G&g*RQU>AoKg51e9t2@Sy}cie^-spb
z%p{gJH+<M^Zusygs$IzbhOx2xA7nQ&HqtwI-&p^YH6i=W4If6D8$Ntb*k01?hqwit
zMvoiopO(et4rD(^1V8&<UiA8ZS<x#nwlRMMt-D}$gJ|6TFE2g!zpmyY82fo2fR|TB
zdIxXfu^(<nXu#9|pfVa<SpWb3zq<T2Tn-UF*zJ!Dht$a+y}y3_{qN$m2X4Qi-oCfk
z>^C=jD2*@|<hO_a4;}ahG6IZGpZZR;{hK#E0qX$~>()Mk+YO2bP~N~{zdySD8Obj|
zM!<2f-%~U<VEW(5e)oTnpFx^H;TPbG2z!wI2D;n+n;KkVMt1|Q`tJHQP<0M6?f?J9
z3qBIGf9e!a^g`6{-SZs9?-2X9{x{Y?ZH&eKrRe?#@$0JJKn(l;|Np0tfB$dU^aTlj
z{=aGCV-)=UJ;J@Q(MNIEe<Bl${f9yJp<`>)NB>{G_yczXv5d24-(s~J6b8on$ERbl
z|1P@SF#hBT?}@b*t}ZwGG*0`C^p5VuVn5Du$HngP|8L*^!OfmN<^BK6)R+G=Q=fy|
zF`%~0gud%=`8#($|2Nb_EsH_^hG8SUL!d;0(!T(;(_#L_Mz31_32y$LU9fgEBK;pe
z1ZmfS6xCE+!0C688;r1&0od$B77Oxu`v31=km>*b|M~O(zn9CC|K^4e`y)aRLnQwH
z|Ns2?_y49wu(mw*FhDN@%ncuEBin_Gy>HJ~xIMF`e}LP+Yv(h#cyISLynYAyA5sQf
zV}+#wbHj(Rxa>q0PfbLWN3UM~`EO<N;D4b1L3la+>C-Q8zW`zfcE7{qO!QBiBH7=G
zY!5#6)ys(dU0(DOocCd#pELUo5q^ib%UJ(pHj@2o@!5$iS5@{JX5IhWw|@Tr`xlZg
ze*XCT-`N3C&%peNi$*R3%ncu&Mz#YV+sfqO|L4zs!`%R5tX_%UCxG}9m;K0PfVttr
z$N20-mz&TF=?}x~2GujZ-usCTdyt!u%K&r3hwy$Xx?R}#PBxFh{b`u}yLVx)XF&Gg
z!bW=gL4_)~48q^WgtP_F-LZ7hN0{~hqa%-y<adypkjnsa?DzMAlt(8|z9HFeko};(
z0#f>CH8*@%X>RxsXB~v@e-MAqt}p+y(od0KKd23Q;JLB>vB@U-=R{%lBh%)F4~5MQ
zA5JzmeE1S%2R<C&{TOdO1Mw%w&i${A^^eas)<1=_?ThRN7#rI62K7O3q)$ls53vU|
zzZvT7dvC0NY^jO<8FiR>l+fme51q{oAMQtPqr>VSnEi%&dp{fN9bIp%f69;&y9ntu
oH+-0AZusz`nc=<PhI)Ix8|fY0ZmfSAK5j-xKe9a9Z~)!805(+b-T(jq

diff --git a/src/index.html b/src/index.html
index 78ba4dc..f20bd8b 100644
--- a/src/index.html
+++ b/src/index.html
@@ -2,8 +2,9 @@
 <html>
 <head>
   <meta charset="utf-8">
-  <title>RdView</title>
+  <title>JastAdd API Docs</title>
   <base href="/">
+  <link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
 
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <link rel="icon" type="image/x-icon" href="favicon.ico">
diff --git a/src/styles.css b/src/styles.css
index 90d4ee0..55e5ebc 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -1 +1,7 @@
 /* You can add global styles to this file, and also import other style files */
+a:hover {
+  background: #ff4;
+}
+::selection {
+  background: #ff4;
+}
-- 
GitLab