Alexander Rose 7 роки тому
батько
коміт
fb78bd2dd9
100 змінених файлів з 9132 додано та 928 видалено
  1. 686 47
      package-lock.json
  2. 21 14
      package.json
  3. 3 4
      src/apps/cif2bcif/converter.ts
  4. 2 2
      src/apps/cif2bcif/field-classifier.ts
  5. 0 65
      src/apps/render-test/components/assemblies.tsx
  6. 0 89
      src/apps/render-test/components/color-theme.tsx
  7. 0 62
      src/apps/render-test/components/file-input.tsx
  8. 0 21
      src/apps/render-test/components/observer.tsx
  9. 0 60
      src/apps/render-test/components/sphere-detail.tsx
  10. 0 28
      src/apps/render-test/components/viewport.tsx
  11. 0 70
      src/apps/render-test/components/visibility.tsx
  12. 0 13
      src/apps/render-test/index.tsx
  13. 0 226
      src/apps/render-test/state.ts
  14. 0 103
      src/apps/render-test/ui.tsx
  15. 0 58
      src/apps/render-test/utils/index.ts
  16. 0 57
      src/apps/render-test/utils/mcubes.ts
  17. 2 2
      src/apps/schema-generator/schema-from-mmcif-dic.ts
  18. 7 7
      src/apps/schema-generator/util/cif-dic.ts
  19. 13 0
      src/apps/viewer/index.html
  20. 88 0
      src/apps/viewer/index.tsx
  21. 61 0
      src/mol-app/context/context.ts
  22. 49 0
      src/mol-app/controller/controller.ts
  23. 27 0
      src/mol-app/controller/entity/tree.ts
  24. 216 0
      src/mol-app/controller/layout.ts
  25. 77 0
      src/mol-app/controller/misc/jobs.ts
  26. 22 0
      src/mol-app/controller/misc/log.ts
  27. 27 0
      src/mol-app/controller/transform/list.ts
  28. 27 0
      src/mol-app/controller/visualization/viewport.ts
  29. 33 0
      src/mol-app/event/basic.ts
  30. 43 0
      src/mol-app/event/event.ts
  31. 59 0
      src/mol-app/service/dispatcher.ts
  32. 132 0
      src/mol-app/service/job.ts
  33. 51 0
      src/mol-app/service/logger.ts
  34. 45 0
      src/mol-app/skin/base.scss
  35. 25 0
      src/mol-app/skin/bootstrap.scss
  36. 68 0
      src/mol-app/skin/bootstrap/badges.scss
  37. 244 0
      src/mol-app/skin/bootstrap/button-groups.scss
  38. 168 0
      src/mol-app/skin/bootstrap/buttons.scss
  39. 617 0
      src/mol-app/skin/bootstrap/forms.scss
  40. 171 0
      src/mol-app/skin/bootstrap/input-groups.scss
  41. 66 0
      src/mol-app/skin/bootstrap/labels.scss
  42. 40 0
      src/mol-app/skin/bootstrap/mixins.scss
  43. 12 0
      src/mol-app/skin/bootstrap/mixins/background-variant.scss
  44. 18 0
      src/mol-app/skin/bootstrap/mixins/border-radius.scss
  45. 65 0
      src/mol-app/skin/bootstrap/mixins/buttons.scss
  46. 22 0
      src/mol-app/skin/bootstrap/mixins/clearfix.scss
  47. 88 0
      src/mol-app/skin/bootstrap/mixins/forms.scss
  48. 122 0
      src/mol-app/skin/bootstrap/mixins/grid.scss
  49. 33 0
      src/mol-app/skin/bootstrap/mixins/image.scss
  50. 12 0
      src/mol-app/skin/bootstrap/mixins/labels.scss
  51. 8 0
      src/mol-app/skin/bootstrap/mixins/opacity.scss
  52. 9 0
      src/mol-app/skin/bootstrap/mixins/tab-focus.scss
  53. 12 0
      src/mol-app/skin/bootstrap/mixins/text-emphasis.scss
  54. 8 0
      src/mol-app/skin/bootstrap/mixins/text-overflow.scss
  55. 222 0
      src/mol-app/skin/bootstrap/mixins/vendor-prefixes.scss
  56. 424 0
      src/mol-app/skin/bootstrap/normalize.scss
  57. 161 0
      src/mol-app/skin/bootstrap/scaffolding.scss
  58. 298 0
      src/mol-app/skin/bootstrap/type.scss
  59. 353 0
      src/mol-app/skin/bootstrap/variables.scss
  60. 24 0
      src/mol-app/skin/colors/blue.scss
  61. 22 0
      src/mol-app/skin/colors/dark.scss
  62. 30 0
      src/mol-app/skin/colors/light.scss
  63. 144 0
      src/mol-app/skin/components/controls-base.scss
  64. 197 0
      src/mol-app/skin/components/controls.scss
  65. 225 0
      src/mol-app/skin/components/entity.scss
  66. 28 0
      src/mol-app/skin/components/help.scss
  67. 131 0
      src/mol-app/skin/components/jobs.scss
  68. 97 0
      src/mol-app/skin/components/log.scss
  69. 69 0
      src/mol-app/skin/components/misc.scss
  70. 142 0
      src/mol-app/skin/components/panel.scss
  71. 164 0
      src/mol-app/skin/components/slider.scss
  72. 93 0
      src/mol-app/skin/components/viewport.scss
  73. BIN
      src/mol-app/skin/fonts/fontello.eot
  74. 442 0
      src/mol-app/skin/fonts/fontello.svg
  75. BIN
      src/mol-app/skin/fonts/fontello.ttf
  76. BIN
      src/mol-app/skin/fonts/fontello.woff
  77. BIN
      src/mol-app/skin/fonts/fontello.woff2
  78. 135 0
      src/mol-app/skin/icons.scss
  79. 29 0
      src/mol-app/skin/layout.scss
  80. 60 0
      src/mol-app/skin/layout/common.scss
  81. 81 0
      src/mol-app/skin/layout/landscape.scss
  82. 89 0
      src/mol-app/skin/layout/outside.scss
  83. 99 0
      src/mol-app/skin/layout/portrait.scss
  84. 45 0
      src/mol-app/skin/logo.scss
  85. 2 0
      src/mol-app/skin/molstar-blue.scss
  86. 2 0
      src/mol-app/skin/molstar-dark.scss
  87. 2 0
      src/mol-app/skin/molstar-light.scss
  88. 38 0
      src/mol-app/skin/ui.scss
  89. 78 0
      src/mol-app/skin/variables.scss
  90. 167 0
      src/mol-app/ui/controls/common.tsx
  91. 814 0
      src/mol-app/ui/controls/slider.tsx
  92. 98 0
      src/mol-app/ui/entity/tree.tsx
  93. 89 0
      src/mol-app/ui/layout.tsx
  94. 64 0
      src/mol-app/ui/misc/jobs.tsx
  95. 69 0
      src/mol-app/ui/misc/log.tsx
  96. 29 0
      src/mol-app/ui/transform/file-loader.tsx
  97. 74 0
      src/mol-app/ui/transform/list.tsx
  98. 67 0
      src/mol-app/ui/transform/model.tsx
  99. 143 0
      src/mol-app/ui/transform/spacefill.tsx
  100. 93 0
      src/mol-app/ui/view.tsx

Різницю між файлами не показано, бо вона завелика
+ 686 - 47
package-lock.json


+ 21 - 14
package.json

@@ -12,13 +12,12 @@
   },
   "scripts": {
     "lint": "tslint src/**/*.ts",
-    "build": "cpx \"src/**/*.{vert,frag,glsl}\" build/node_modules/ && tsc",
+    "build": "cpx \"src/**/*.{vert,frag,glsl,scss,woff,woff2,ttf,otf,eot,svg,html}\" build/node_modules/ && tsc",
     "watch": "tsc -watch",
-    "watch-shader": "cpx \"src/**/*.{vert,frag,glsl}\" build/node_modules/ --watch",
+    "watch-extra": "cpx \"src/**/*.{vert,frag,glsl,scss,woff,woff2,ttf,otf,eot,svg,html}\" build/node_modules/ --watch",
     "test": "jest",
-    "script": "node build/node_modules/script.js",
-    "app-render-test": "webpack build/node_modules/apps/render-test/index.js --mode development -o web/render-test/index.js",
-    "app-render-test-watch": "webpack build/node_modules/apps/render-test/index.js -w --mode development -o web/render-test/index.js"
+    "build-viewer": "webpack build/node_modules/apps/viewer/index.js --mode development -o build/viewer/index.js",
+    "watch-viewer": "webpack build/node_modules/apps/viewer/index.js -w --mode development -o build/viewer/index.js"
   },
   "jest": {
     "moduleFileExtensions": [
@@ -34,6 +33,7 @@
       "build/node_modules"
     ],
     "moduleNameMapper": {
+      "mol-app($|/.*)": "<rootDir>/src/mol-app$1",
       "mol-data($|/.*)": "<rootDir>/src/mol-data$1",
       "mol-geo($|/.*)": "<rootDir>/src/mol-geo$1",
       "mol-gl($|/.*)": "<rootDir>/src/mol-gl$1",
@@ -60,35 +60,42 @@
     "@types/benchmark": "^1.0.31",
     "@types/express": "^4.11.1",
     "@types/jest": "^22.2.3",
-    "@types/node": "^9.6.8",
+    "@types/node": "^9.6.16",
     "@types/node-fetch": "^1.6.9",
-    "@types/react": "^16.3.13",
+    "@types/react": "^16.3.14",
     "@types/react-dom": "^16.0.5",
     "benchmark": "^2.1.4",
     "copyfiles": "^2.0.0",
     "cpx": "^1.5.0",
+    "css-loader": "^0.28.11",
     "extra-watch-webpack-plugin": "^1.0.3",
+    "extract-text-webpack-plugin": "^4.0.0-beta.0",
+    "file-loader": "^1.1.11",
     "glslify-import": "^3.1.0",
     "glslify-loader": "^1.0.2",
     "jest": "^22.4.3",
     "jest-raw-loader": "^1.0.1",
+    "node-sass": "^4.9.0",
     "raw-loader": "^0.5.1",
-    "ts-jest": "^22.4.4",
-    "tslint": "^5.9.1",
+    "resolve-url-loader": "^2.3.0",
+    "sass-loader": "^7.0.1",
+    "style-loader": "^0.21.0",
+    "ts-jest": "^22.4.6",
+    "tslint": "^5.10.0",
     "typescript": "^2.8.3",
-    "uglify-js": "^3.3.23",
+    "uglify-js": "^3.3.25",
     "util.promisify": "^1.0.0",
-    "webpack": "^4.6.0",
-    "webpack-cli": "^2.1.2"
+    "webpack": "^4.8.3",
+    "webpack-cli": "^2.1.3"
   },
   "dependencies": {
     "argparse": "^1.0.10",
     "express": "^4.16.3",
     "gl": "^4.0.4",
-    "material-ui": "^1.0.0-beta.43",
+    "immutable": "^4.0.0-rc.9",
     "node-fetch": "^2.1.2",
     "react": "^16.3.2",
     "react-dom": "^16.3.2",
-    "rxjs": "^6.0.0"
+    "rxjs": "^6.1.0"
   }
 }

+ 3 - 4
src/apps/cif2bcif/converter.ts

@@ -5,7 +5,7 @@
  */
 
 import Iterator from 'mol-data/iterator'
-import CIF, { Category } from 'mol-io/reader/cif'
+import CIF, { CifCategory } from 'mol-io/reader/cif'
 import * as Encoder from 'mol-io/writer/cif'
 import * as fs from 'fs'
 import classify from './field-classifier'
@@ -20,14 +20,14 @@ async function getCIF(path: string) {
     return parsed.result;
 }
 
-function createDefinition(cat: Category): Encoder.CategoryDefinition {
+function createDefinition(cat: CifCategory): Encoder.CategoryDefinition {
     return {
         name: cat.name,
         fields: cat.fieldNames.map(n => classify(n, cat.getField(n)!))
     }
 }
 
-function getCategoryInstanceProvider(cat: Category): Encoder.CategoryProvider {
+function getCategoryInstanceProvider(cat: CifCategory): Encoder.CategoryProvider {
     return function (ctx: any) {
         return {
             data: cat,
@@ -50,4 +50,3 @@ export default async function convert(path: string, asText = false) {
     }
     return encoder.getData();
 }
-

+ 2 - 2
src/apps/cif2bcif/field-classifier.ts

@@ -5,7 +5,7 @@
  */
 
 import { Column } from 'mol-data/db'
-import { Field } from 'mol-io/reader/cif/data-model'
+import { CifField } from 'mol-io/reader/cif/data-model'
 import { FieldDefinition, FieldType } from 'mol-io/writer/cif/encoder'
 
 const intRegex = /^-?\d+$/
@@ -13,7 +13,7 @@ const floatRegex = /^-?(([0-9]+)[.]?|([0-9]*[.][0-9]+))([(][0-9]+[)])?([eE][+-]?
 
 // Classify a cif field as str, int or float based the data it contains.
 // To classify a field as int or float all items are checked.
-function classify(name: string, field: Field): FieldDefinition {
+function classify(name: string, field: CifField): FieldDefinition {
     let floatCount = 0, hasString = false;
     for (let i = 0, _i = field.rowCount; i < _i; i++) {
         const k = field.valueKind(i);

+ 0 - 65
src/apps/render-test/components/assemblies.tsx

@@ -1,65 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import * as React from 'react'
-import { WithStyles } from 'material-ui/styles';
-import { MenuItem } from 'material-ui/Menu';
-import { InputLabel } from 'material-ui/Input';
-import { FormControl } from 'material-ui/Form';
-import Select from 'material-ui/Select';
-
-import State from '../state'
-import Observer from './observer';
-import { Assembly } from 'mol-model/structure/model/properties/symmetry';
-
-interface AssemblyState {
-    loading: boolean
-    assemblies: ReadonlyArray<Assembly>
-    value: string
-}
-
-export default class Assemblies extends Observer<{ state: State } & WithStyles, AssemblyState> {
-    state: AssemblyState = { loading: false, assemblies: [], value: '' }
-
-    componentDidMount() {
-        this.subscribe(this.props.state.loading, value => {
-           this.setState({ loading: value });
-        });
-        this.subscribe(this.props.state.model, value => {
-            this.setState({ assemblies: value ? value.symmetry.assemblies : [] });
-        });
-        this.subscribe(this.props.state.assembly, value => {
-            this.setState({ value });
-        });
-    }
-
-    handleValueChange = (event: React.ChangeEvent<any>) => {
-        this.props.state.assembly.next(event.target.value)
-    }
-
-    render() {
-        const { classes } = this.props;
-
-        const items = this.state.assemblies.map((value, idx) => {
-            return <MenuItem key={idx} value={value.id}>{value.details}</MenuItem>
-        })
-
-        return <FormControl className={classes.formControl}>
-            <InputLabel htmlFor='assembly-value'>Assembly</InputLabel>
-            <Select
-                className={classes.selectField}
-                value={this.state.value}
-                onChange={this.handleValueChange}
-                inputProps={{
-                    name: 'value',
-                    id: 'assembly-value',
-                }}
-            >
-                {items}
-            </Select>
-        </FormControl>
-    }
-}

+ 0 - 89
src/apps/render-test/components/color-theme.tsx

@@ -1,89 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import * as React from 'react'
-import { WithStyles } from 'material-ui/styles';
-import { MenuItem } from 'material-ui/Menu';
-import { InputLabel } from 'material-ui/Input';
-import { FormControl } from 'material-ui/Form';
-import Select from 'material-ui/Select';
-
-import State, { ColorTheme as _ColorTheme } from '../state'
-import Observer from './observer';
-import { Color, ColorNames } from 'mol-util/color';
-
-interface ColorThemeState {
-    loading: boolean
-    name: _ColorTheme
-    value: Color
-}
-
-export default class ColorTheme extends Observer<{ state: State } & WithStyles, ColorThemeState> {
-    state: ColorThemeState = { loading: false, name: 'element-symbol' as _ColorTheme, value: 0xFF0000 }
-
-    componentDidMount() {
-        this.subscribe(this.props.state.loading, value => {
-           this.setState({ loading: value });
-        });
-        this.subscribe(this.props.state.colorTheme, value => {
-            this.setState({ name: value });
-         });
-         this.subscribe(this.props.state.colorValue, value => {
-            this.setState({ value: value });
-         });
-    }
-
-    handleNameChange = (event: React.ChangeEvent<any>) => {
-        this.props.state.colorTheme.next(event.target.value)
-    }
-
-    handleValueChange = (event: React.ChangeEvent<any>) => {
-        this.props.state.colorValue.next(event.target.value)
-    }
-
-    render() {
-        const { classes } = this.props;
-
-        const colorThemeItems = Object.keys(_ColorTheme).map((name, idx) => {
-            return <MenuItem key={idx} value={name}>{name}</MenuItem>
-        })
-
-        const colorValueItems = Object.keys(ColorNames).map((name, idx) => {
-            return <MenuItem key={idx} value={(ColorNames as any)[name]}>{name}</MenuItem>
-        })
-
-        return <div>
-            <FormControl className={classes.formControl}>
-                <InputLabel htmlFor='color-theme-name'>Color Theme</InputLabel>
-                <Select
-                    className={classes.selectField}
-                    value={this.state.name}
-                    onChange={this.handleNameChange}
-                    inputProps={{
-                        name: 'name',
-                        id: 'color-theme-name',
-                    }}
-                >
-                    {colorThemeItems}
-                </Select>
-            </FormControl>
-            <FormControl className={classes.formControl}>
-                    <InputLabel htmlFor='uniform-color-value'>Color Value</InputLabel>
-                    <Select
-                        className={classes.selectField}
-                        value={this.state.value}
-                        onChange={this.handleValueChange}
-                        inputProps={{
-                            name: 'value',
-                            id: 'uniform-color-value',
-                        }}
-                    >
-                        {colorValueItems}
-                    </Select>
-            </FormControl>
-        </div>
-    }
-}

+ 0 - 62
src/apps/render-test/components/file-input.tsx

@@ -1,62 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import * as React from 'react'
-import { WithStyles } from 'material-ui/styles';
-import TextField from 'material-ui/TextField';
-import Button from 'material-ui/Button';
-
-import State from '../state'
-import Observer from './observer';
-
-export default class FileInput extends Observer<{ state: State } & WithStyles, { loading: boolean }> {
-    state = { loading: false }
-
-    componentDidMount() {
-        this.subscribe(this.props.state.loading, value => {
-           this.setState({ loading: value });
-        });
-    }
-
-    render() {
-        const { classes, state } = this.props;
-
-        return <div>
-            <TextField
-                label='PDB ID'
-                className={classes.textField}
-                disabled={this.state.loading}
-                margin='normal'
-                onChange={(event) => {
-                    state.pdbId = event.target.value
-                }}
-                onKeyPress={(event) => {
-                    if (event.key === 'Enter') state.loadPdbId()
-                }}
-            />
-            <input
-                accept='*.cif'
-                className={classes.input}
-                id='button-file'
-                type='file'
-                onChange={(event) => {
-                    if (event.target.files) {
-                        state.loadFile(event.target.files[0])
-                    }
-                }}
-            />
-            <label htmlFor='button-file'>
-                <Button
-                    variant='raised'
-                    component='span'
-                    className={classes.button}
-                >
-                    Open CIF
-                </Button>
-            </label>
-        </div>
-    }
-}

+ 0 - 21
src/apps/render-test/components/observer.tsx

@@ -1,21 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import * as React from 'react'
-import { Observable, Subscription } from 'rxjs';
-
-export default class Observer<S, P> extends React.Component<S, P> {
-    private _subs: Subscription[] = []
-
-    subscribe<T>(obs: Observable<T>, onNext: (v: T) => void) {
-        this._subs.push(obs.subscribe(onNext));
-    }
-
-    componentWillUnmount() {
-        for (const s of this._subs) s.unsubscribe();
-        this._subs = [];
-    }
-}

+ 0 - 60
src/apps/render-test/components/sphere-detail.tsx

@@ -1,60 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import * as React from 'react'
-import { WithStyles } from 'material-ui/styles';
-import { MenuItem } from 'material-ui/Menu';
-import { InputLabel } from 'material-ui/Input';
-import { FormControl } from 'material-ui/Form';
-import Select from 'material-ui/Select';
-
-import State from '../state'
-import Observer from './observer';
-
-interface SphereDetailState {
-    loading: boolean
-    value: number
-}
-
-export default class SphereDetail extends Observer<{ state: State } & WithStyles, SphereDetailState> {
-    state: SphereDetailState = { loading: false, value: 2 }
-
-    componentDidMount() {
-        this.subscribe(this.props.state.loading, value => {
-           this.setState({ loading: value });
-        });
-        this.subscribe(this.props.state.sphereDetail, value => {
-            this.setState({ value });
-         });
-    }
-
-    handleValueChange = (event: React.ChangeEvent<any>) => {
-        this.props.state.sphereDetail.next(event.target.value)
-    }
-
-    render() {
-        const { classes } = this.props;
-
-        const items = [0, 1, 2, 3].map((value, idx) => {
-            return <MenuItem key={idx} value={value}>{value.toString()}</MenuItem>
-        })
-
-        return <FormControl className={classes.formControl}>
-            <InputLabel htmlFor='sphere-detail-value'>Sphere Detail</InputLabel>
-            <Select
-                className={classes.selectField}
-                value={this.state.value}
-                onChange={this.handleValueChange}
-                inputProps={{
-                    name: 'value',
-                    id: 'sphere-detail-value',
-                }}
-            >
-                {items}
-            </Select>
-        </FormControl>
-    }
-}

+ 0 - 28
src/apps/render-test/components/viewport.tsx

@@ -1,28 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import * as React from 'react'
-import State from '../state'
-
-export default class Viewport extends React.Component<{ state: State }, { initialized: boolean }> {
-    private container: HTMLDivElement | null = null;
-    private canvas: HTMLCanvasElement | null = null;
-    state = { initialized: false }
-
-    componentDidMount() {
-        if (this.container && this.canvas) {
-            this.props.state.initRenderer(this.canvas, this.container).then(() => {
-                this.setState({ initialized: true })
-            })
-        }
-    }
-
-    render() {
-        return <div ref={elm => this.container = elm} style={{ height: '100%' }}>
-            <canvas ref={elm => this.canvas = elm}></canvas>
-        </div>
-    }
-}

+ 0 - 70
src/apps/render-test/components/visibility.tsx

@@ -1,70 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import * as React from 'react'
-import { WithStyles } from 'material-ui/styles';
-import { FormLabel, FormControl, FormGroup, FormControlLabel } from 'material-ui/Form';
-import Checkbox from 'material-ui/Checkbox';
-
-import State from '../state'
-import Observer from './observer';
-
-interface VisibilityState {
-    loading: boolean
-    spacefill: boolean
-    point: boolean
-}
-
-export default class Visibility extends Observer<{ state: State } & WithStyles, VisibilityState> {
-    state: VisibilityState = { loading: false, spacefill: true, point: true }
-
-    componentDidMount() {
-        this.subscribe(this.props.state.loading, value => {
-           this.setState({ loading: value });
-        });
-        this.subscribe(this.props.state.spacefillVisibility, value => {
-            this.setState({ spacefill: value });
-        });
-        this.subscribe(this.props.state.pointVisibility, value => {
-            this.setState({ point: value });
-        });
-    }
-
-    handleChange = (event: React.ChangeEvent<any>) => {
-        switch (event.target.name) {
-            case 'point': this.props.state.pointVisibility.next(event.target.checked); break;
-            case 'spacefill': this.props.state.spacefillVisibility.next(event.target.checked); break;
-        }
-    }
-
-    render() {
-        const { classes } = this.props
-
-        return <div className={classes.formControl}>
-            <FormControl component='fieldset'>
-                <FormLabel component='legend'>Visibility</FormLabel>
-                <FormGroup>
-                    <FormControlLabel
-                        control={<Checkbox
-                            checked={this.state.point}
-                            onChange={this.handleChange}
-                            name='point'
-                        />}
-                        label='Point'
-                    />
-                    <FormControlLabel
-                        control={<Checkbox
-                            checked={this.state.spacefill}
-                            onChange={this.handleChange}
-                            name='spacefill'
-                        />}
-                        label='Spacefill'
-                    />
-                </FormGroup>
-            </FormControl>
-        </div>
-    }
-}

+ 0 - 13
src/apps/render-test/index.tsx

@@ -1,13 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import UI from './ui'
-import State from './state'
-import * as React from 'react'
-import * as ReactDOM from 'react-dom'
-
-const state = new State()
-ReactDOM.render(<UI state={ state } />, document.getElementById('app'));

+ 0 - 226
src/apps/render-test/state.ts

@@ -1,226 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { BehaviorSubject } from 'rxjs';
-
-// import { ValueCell } from 'mol-util/value-cell'
-
-// import { Vec3, Mat4 } from 'mol-math/linear-algebra'
-import Viewer from 'mol-view/viewer'
-// import { createColorTexture } from 'mol-gl/util';
-// import Icosahedron from 'mol-geo/primitive/icosahedron'
-// import Box from 'mol-geo/primitive/box'
-import Spacefill, { SpacefillProps } from 'mol-geo/representation/structure/spacefill'
-import Point, { PointProps } from 'mol-geo/representation/structure/point'
-
-import { Run } from 'mol-task'
-import { Symmetry, Structure, Model } from 'mol-model/structure'
-
-// import mcubes from './utils/mcubes'
-import { getModelFromPdbId, getModelFromFile, log, Volume, getVolumeFromEmdId } from './utils'
-import { StructureRepresentation } from 'mol-geo/representation/structure';
-import { Color } from 'mol-util/color';
-import Surface, { SurfaceProps } from 'mol-geo/representation/volume/surface';
-import { VolumeIsoValue } from 'mol-model/volume';
-import { VolumeRepresentation } from 'mol-geo/representation/volume';
-// import Cylinder from 'mol-geo/primitive/cylinder';
-
-
-export const ColorTheme = {
-    'atom-index': {},
-    'chain-id': {},
-    'element-symbol': {},
-    'instance-index': {},
-    'uniform': {}
-}
-export type ColorTheme = keyof typeof ColorTheme
-
-export default class State {
-    viewer: Viewer
-    pdbId = '1crn'
-    // pdbId = '5ire'
-    emdId = '8116'
-    // pdbId = '6G1K'
-    // emdId = '4339'
-    // pdbId = '4cup'
-    // emdId = ''
-    model = new BehaviorSubject<Model | undefined>(undefined)
-    volume = new BehaviorSubject<Volume | undefined>(undefined)
-    initialized = new BehaviorSubject<boolean>(false)
-    loading = new BehaviorSubject<boolean>(false)
-
-    colorTheme = new BehaviorSubject<ColorTheme>('element-symbol')
-    colorValue = new BehaviorSubject<Color>(0xFF4411)
-    sphereDetail = new BehaviorSubject<number>(0)
-    assembly = new BehaviorSubject<string>('')
-
-    pointVisibility = new BehaviorSubject<boolean>(true)
-    spacefillVisibility = new BehaviorSubject<boolean>(true)
-
-    pointRepr: StructureRepresentation<PointProps>
-    spacefillRepr: StructureRepresentation<SpacefillProps>
-    surfaceRepr: VolumeRepresentation<SurfaceProps>
-
-    constructor() {
-        this.colorTheme.subscribe(() => this.update())
-        this.colorValue.subscribe(() => this.update())
-        this.sphereDetail.subscribe(() => this.update())
-        this.assembly.subscribe(() => this.initStructure())
-
-        this.pointVisibility.subscribe(() => this.updateVisibility())
-        this.spacefillVisibility.subscribe(() => this.updateVisibility())
-    }
-
-    getSpacefillProps (): SpacefillProps {
-        const colorThemeName = this.colorTheme.getValue()
-        return {
-            doubleSided: true,
-            detail: this.sphereDetail.getValue(),
-            colorTheme: colorThemeName === 'uniform' ?
-                { name: colorThemeName, value: this.colorValue.getValue() } :
-                { name: colorThemeName }
-        }
-    }
-
-    getPointProps (): PointProps {
-        const colorThemeName = this.colorTheme.getValue()
-        return {
-            sizeTheme: { name: 'uniform', value: 0.1 },
-            colorTheme: colorThemeName === 'uniform' ?
-                { name: colorThemeName, value: this.colorValue.getValue() } :
-                { name: colorThemeName }
-        }
-    }
-
-    async initRenderer (canvas: HTMLCanvasElement, container: HTMLDivElement) {
-        this.viewer = Viewer.create(canvas, container)
-        this.initialized.next(true)
-        this.loadPdbId()
-        this.loadEmdId()
-        this.viewer.animate()
-    }
-
-    async getStructure () {
-        const model = this.model.getValue()
-        if (!model) return
-        const assembly = this.assembly.getValue()
-        let structure: Structure
-        const assemblies = model.symmetry.assemblies
-        if (assemblies.length) {
-            structure = await Run(Symmetry.buildAssembly(Structure.ofModel(model), assembly || '1'), log, 500)
-        } else {
-            structure = Structure.ofModel(model)
-        }
-        return structure
-    }
-
-    async initStructure () {
-        const { viewer } = this
-        if (!viewer || !this.model.getValue()) return
-
-        if (this.pointRepr) this.viewer.remove(this.pointRepr)
-        if (this.spacefillRepr) this.viewer.remove(this.spacefillRepr)
-
-        const structure = await this.getStructure()
-        if (!structure) return
-
-        this.pointRepr = StructureRepresentation(Point)
-        await Run(this.pointRepr.create(structure, this.getPointProps()), log, 500)
-        viewer.add(this.pointRepr)
-
-        this.spacefillRepr = StructureRepresentation(Spacefill)
-        await Run(this.spacefillRepr.create(structure, this.getSpacefillProps()), log, 500)
-        viewer.add(this.spacefillRepr)
-
-        this.updateVisibility()
-        viewer.requestDraw()
-        console.log(viewer.stats)
-    }
-
-    setModel(model: Model) {
-        this.model.next(model)
-        this.initStructure()
-        this.loading.next(false)
-    }
-
-    async loadFile (file: File) {
-        this.viewer.clear()
-        this.loading.next(true)
-        this.setModel((await getModelFromFile(file))[0])
-    }
-
-    async initVolume () {
-        const { viewer } = this
-        const v = this.volume.getValue()
-        if (!viewer || !v) return
-
-        if (this.surfaceRepr) this.viewer.remove(this.surfaceRepr)
-
-        this.surfaceRepr = VolumeRepresentation(Surface)
-        await Run(this.surfaceRepr.create(v.volume, {
-            isoValue: VolumeIsoValue.relative(v.volume.dataStats, 3.0),
-            alpha: 0.5,
-            flatShaded: false,
-            flipSided: true,
-            doubleSided: true
-        }), log, 500)
-        viewer.add(this.surfaceRepr)
-
-        viewer.requestDraw()
-        console.log(viewer.stats)
-    }
-
-    async loadPdbId () {
-        if (this.pointRepr) this.viewer.remove(this.pointRepr)
-        if (this.spacefillRepr) this.viewer.remove(this.spacefillRepr)
-        if (this.pdbId.length !== 4) return
-        this.loading.next(true)
-        this.setModel((await getModelFromPdbId(this.pdbId))[0])
-    }
-
-    setVolume(volume: Volume) {
-        this.volume.next(volume)
-        this.initVolume()
-        this.loading.next(false)
-    }
-
-    async loadEmdId () {
-        if (this.surfaceRepr) this.viewer.remove(this.surfaceRepr)
-        if (this.emdId.length !== 4) return
-        this.loading.next(true)
-        this.setVolume(await getVolumeFromEmdId(this.emdId))
-    }
-
-    async update () {
-        if (!this.spacefillRepr) return
-        await Run(this.spacefillRepr.update(this.getSpacefillProps()), log, 500)
-        await Run(this.pointRepr.update(this.getPointProps()), log, 500)
-        this.viewer.add(this.spacefillRepr)
-        this.viewer.add(this.pointRepr)
-        this.viewer.update()
-        this.viewer.requestDraw()
-        console.log(this.viewer.stats)
-    }
-
-    updateVisibility () {
-        if (!this.viewer) return
-        if (this.pointRepr) {
-            if (this.pointVisibility.getValue()) {
-                this.viewer.show(this.pointRepr)
-            } else {
-                this.viewer.hide(this.pointRepr)
-            }
-        }
-        if (this.spacefillRepr) {
-            if (this.spacefillVisibility.getValue()) {
-                this.viewer.show(this.spacefillRepr)
-            } else {
-                this.viewer.hide(this.spacefillRepr)
-            }
-        }
-        this.viewer.requestDraw()
-    }
-}

+ 0 - 103
src/apps/render-test/ui.tsx

@@ -1,103 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import * as React from 'react'
-import { withStyles, WithStyles, Theme, StyleRulesCallback } from 'material-ui/styles';
-import Typography from 'material-ui/Typography';
-import Toolbar from 'material-ui/Toolbar';
-import AppBar from 'material-ui/AppBar';
-import Drawer from 'material-ui/Drawer';
-
-import State from './state'
-
-import Viewport from './components/viewport'
-import FileInput from './components/file-input'
-import ColorTheme from './components/color-theme'
-import SphereDetail from './components/sphere-detail'
-import Visibility from './components/visibility'
-import Assemblies from './components/assemblies'
-
-
-const styles: StyleRulesCallback = (theme: Theme) => ({
-    root: {
-        flexGrow: 1,
-        height: 830,
-        zIndex: 1,
-        overflow: 'hidden',
-        position: 'relative',
-        display: 'flex',
-    },
-    appBar: {
-        zIndex: theme.zIndex.drawer + 1,
-    },
-    drawerPaper: {
-        position: 'relative',
-        width: 240,
-    },
-    content: {
-        flexGrow: 1,
-        backgroundColor: theme.palette.background.default,
-        padding: theme.spacing.unit * 3,
-        minWidth: 0, // So the Typography noWrap works
-    },
-    toolbar: theme.mixins.toolbar,
-    formControl: {
-        margin: theme.spacing.unit,
-        width: 200,
-    },
-    textField: {
-        marginLeft: theme.spacing.unit,
-        marginRight: theme.spacing.unit,
-        width: 200,
-    },
-    button: {
-        margin: theme.spacing.unit,
-    },
-    input: {
-        display: 'none',
-    },
-} as any);
-
-const decorate = withStyles(styles);
-
-interface Props {
-    state: State;
-};
-
-class UI extends React.Component<{ state: State } & WithStyles, {  }> {
-    render() {
-        const { classes, state } = this.props;
-        return (
-            <div className={classes.root}>
-                <AppBar position='absolute' className={classes.appBar}>
-                    <Toolbar>
-                    <Typography variant='title' color='inherit' noWrap>
-                        Mol* Render Test
-                    </Typography>
-                    </Toolbar>
-                </AppBar>
-                <Drawer variant='permanent' classes={{ paper: classes.drawerPaper, }}>
-                    <div className={classes.toolbar} />
-                    <FileInput state={state} classes={classes}></FileInput>
-                    <form className={classes.root} autoComplete='off'>
-                        <div>
-                            <Assemblies state={state} classes={classes}></Assemblies>
-                            <ColorTheme state={state} classes={classes}></ColorTheme>
-                            <SphereDetail state={state} classes={classes}></SphereDetail>
-                            <Visibility state={state} classes={classes}></Visibility>
-                        </div>
-                    </form>
-                </Drawer>
-                <main className={classes.content}>
-                    <div className={classes.toolbar} />
-                    <Viewport state={state}></Viewport>
-                </main>
-            </div>
-        );
-    }
-}
-
-export default decorate<Props>(UI)

+ 0 - 58
src/apps/render-test/utils/index.ts

@@ -1,58 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import CIF from 'mol-io/reader/cif'
-import { Run, Progress } from 'mol-task'
-import { Model } from 'mol-model/structure'
-import { VolumeData, parseDensityServerData } from 'mol-model/volume'
-import { DensityServer_Data_Database } from 'mol-io/reader/cif/schema/density-server';
-
-export function log(progress: Progress) {
-    const p = progress.root.progress
-    console.log(`${p.message} ${(p.current/p.max*100).toFixed(2)}%`)
-}
-
-export async function downloadCif(url: string, isBinary: boolean) {
-    const data = await fetch(url);
-    return parseCif(isBinary ? new Uint8Array(await data.arrayBuffer()) : await data.text());
-}
-
-export async function parseCif(data: string|Uint8Array) {
-    const comp = CIF.parse(data)
-    const parsed = await Run(comp, log, 500);
-    if (parsed.isError) throw parsed;
-    return parsed.result
-}
-
-export async function getModelFromPdbId(pdbid: string) {
-    const cif = await downloadCif(`https://files.rcsb.org/download/${pdbid}.cif`, false)
-    return Model.create({ kind: 'mmCIF', data: CIF.schema.mmCIF(cif.blocks[0]) })
-}
-
-const readFileAsText = (file: File) => {
-    const fileReader = new FileReader()
-    return new Promise<string>((resolve, reject) => {
-        fileReader.onerror = () => {
-            fileReader.abort()
-            reject(new DOMException('Error parsing input file.'))
-        }
-        fileReader.onload = () => resolve(fileReader.result)
-        fileReader.readAsText(file)
-    })
-}
-
-export async function getModelFromFile(file: File) {
-    const cif = await parseCif(await readFileAsText(file))
-    return Model.create({ kind: 'mmCIF', data: CIF.schema.mmCIF(cif.blocks[0]) })
-}
-
-export type Volume = { source: DensityServer_Data_Database, volume: VolumeData }
-
-export async function getVolumeFromEmdId(emdid: string): Promise<Volume> {
-    const cif = await downloadCif(`https://webchem.ncbr.muni.cz/DensityServer/em/emd-${emdid}/cell?detail=4`, true)
-    const data = CIF.schema.densityServer(cif.blocks[1])
-    return { source: data, volume: await Run(parseDensityServerData(data)) }
-}

+ 0 - 57
src/apps/render-test/utils/mcubes.ts

@@ -1,57 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import { Run } from 'mol-task'
-import { computeMarchingCubes } from 'mol-geo/util/marching-cubes/algorithm'
-import { Mesh } from 'mol-geo/shape/mesh'
-import { Tensor, Mat4, Vec3 } from 'mol-math/linear-algebra'
-
-function fillField(tensor: Tensor, f: (x: number, y: number, z: number) => number, min: number[], max: number[]): Tensor {
-    const { space: { set, dimensions: [ii, jj, kk] }, data } = tensor;
-
-    const dx = (max[0] - min[0]) / (ii - 1);
-    const dy = (max[1] - min[1]) / (jj - 1);
-    const dz = (max[2] - min[2]) / (kk - 1);
-
-    for (let i = 0, x = min[0]; i < ii; i++, x += dx) {
-        for (let j = 0, y = min[1]; j < jj; j++, y += dy) {
-            for (let k = 0, z = min[2]; k < kk; k++, z += dz) {
-                set(data, i, j, k, f(x, y, z));
-            }
-        }
-    }
-
-    return tensor
-}
-
-export default async function computeSurface(f: (x: number, y: number, z: number) => number, data?: { field: Tensor, surface: Mesh }) {
-    let field: Tensor;
-    if (data) field = data.field;
-    else {
-        const space = Tensor.Space([30, 30, 30], [0, 1, 2]);
-        field = Tensor.create(space, space.create(Float32Array));
-    }
-
-    const min = Vec3.create(-1.1, -1.1, -1.1), max = Vec3.create(1.1, 1.1, 1.1);
-
-    fillField(field, f, min, max);
-    const surface = await Run(computeMarchingCubes({
-        scalarField: field,
-        isoLevel: 0,
-        oldSurface: data ? data.surface : void 0
-    }));
-
-    const translation = Mat4.fromTranslation(Mat4.zero(), min);
-    const grid = Vec3.zero();
-    Vec3.fromArray(grid, field.space.dimensions as any, 0);
-    const size = Vec3.sub(Vec3.zero(), max, min);
-    const scale = Mat4.fromScaling(Mat4.zero(), Vec3.create(size[0] / (grid[0] - 1), size[1] / (grid[1] - 1), size[2] / (grid[2] - 1)));
-
-    const transform = Mat4.mul(Mat4.zero(), translation, scale);
-    Mesh.transformImmediate(surface, transform);
-    Mesh.computeNormalsImmediate(surface);
-    return { surface, field };
-}

+ 2 - 2
src/apps/schema-generator/schema-from-mmcif-dic.ts

@@ -10,7 +10,7 @@ import * as fs from 'fs'
 import fetch from 'node-fetch'
 
 import Csv from 'mol-io/reader/csv/parser'
-import CIF, { Frame } from 'mol-io/reader/cif'
+import CIF, { CifFrame } from 'mol-io/reader/cif'
 import { generateSchema } from './util/cif-dic'
 import { generate } from './util/generate'
 import { Filter } from './util/json-schema'
@@ -29,7 +29,7 @@ async function runGenerateSchema(name: string, fieldNamesPath?: string, typescri
     const ihmDicVersion = CIF.schema.dic(ihmDic.result.blocks[0]).dictionary.version.value(0)
     const version = `Dictionary versions: mmCIF ${mmcifDicVersion}, IHM ${ihmDicVersion}.`
 
-    const frames: Frame[] = [...mmcifDic.result.blocks[0].saveFrames, ...ihmDic.result.blocks[0].saveFrames]
+    const frames: CifFrame[] = [...mmcifDic.result.blocks[0].saveFrames, ...ihmDic.result.blocks[0].saveFrames]
     const schema = generateSchema(frames)
 
     const filter = fieldNamesPath ? await getFieldNamesFilter(fieldNamesPath) : undefined

+ 7 - 7
src/apps/schema-generator/util/cif-dic.ts

@@ -6,7 +6,7 @@
 
 import { Database, ValueColumn, ListColumn } from './json-schema'
 import * as Data from 'mol-io/reader/cif/data-model'
-import { Frame } from 'mol-io/reader/cif/data-model';
+import { CifFrame } from 'mol-io/reader/cif/data-model';
 
 export function getFieldType (type: string, values?: string[]): ValueColumn|ListColumn {
     switch (type) {
@@ -76,7 +76,7 @@ export function getFieldType (type: string, values?: string[]): ValueColumn|List
     return 'str'
 }
 
-type FrameCategories = { [category: string]: Data.Frame }
+type FrameCategories = { [category: string]: Data.CifFrame }
 type FrameLinks = { [k: string]: string }
 
 interface FrameData {
@@ -85,7 +85,7 @@ interface FrameData {
 }
 
 // get field from given or linked category
-function getField ( category: string, field: string, d: Data.Frame, ctx: FrameData): Data.Field|undefined {
+function getField ( category: string, field: string, d: Data.CifFrame, ctx: FrameData): Data.CifField|undefined {
     const { categories, links } = ctx
 
     const cat = d.categories[category]
@@ -105,7 +105,7 @@ function getField ( category: string, field: string, d: Data.Frame, ctx: FrameDa
     }
 }
 
-function getEnums (d: Data.Frame, ctx: FrameData) {
+function getEnums (d: Data.CifFrame, ctx: FrameData) {
     const value = getField('item_enumeration', 'value', d, ctx)
     const enums: string[] = []
     if (value) {
@@ -119,7 +119,7 @@ function getEnums (d: Data.Frame, ctx: FrameData) {
     }
 }
 
-function getCode (d: Data.Frame, ctx: FrameData): [string, string[]|undefined]|undefined {
+function getCode (d: Data.CifFrame, ctx: FrameData): [string, string[]|undefined]|undefined {
     const code = getField('item_type', 'code', d, ctx)
     if (code) {
         return [ code.str(0), getEnums(d, ctx) ]
@@ -128,7 +128,7 @@ function getCode (d: Data.Frame, ctx: FrameData): [string, string[]|undefined]|u
     }
 }
 
-function getSubCategory (d: Data.Frame, ctx: FrameData): string|undefined {
+function getSubCategory (d: Data.CifFrame, ctx: FrameData): string|undefined {
     const value = getField('item_sub_category', 'id', d, ctx)
     if (value) {
         return value.str(0)
@@ -178,7 +178,7 @@ const SPACE_SEPARATED_LIST_FIELDS = [
     '_pdbx_soln_scatter.data_analysis_software_list', // SCTPL5 GNOM
 ];
 
-export function generateSchema (frames: Frame[]) {
+export function generateSchema (frames: CifFrame[]) {
     const schema: Database = {}
 
     const categories: FrameCategories = {}

+ 13 - 0
src/apps/viewer/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8" />
+        <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+        <title>Mol* Render Test</title>
+        <link href='./app.css', rel="stylesheet">
+    </head>
+    <body>
+        <div id="app"></div>
+        <script type="text/javascript" src="./index.js"></script>
+    </body>
+</html>

+ 88 - 0
src/apps/viewer/index.tsx

@@ -0,0 +1,88 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react'
+import * as ReactDOM from 'react-dom'
+
+import './index.html'
+import 'mol-app/skin/molstar-light.scss'
+
+import { Context } from 'mol-app/context/context';
+import { Viewport } from 'mol-app/ui/visualization/viewport'
+import { makeEmptyTargets, LayoutRegion } from 'mol-app/controller/layout';
+import { Layout } from 'mol-app/ui/layout';
+import { LogController } from 'mol-app/controller/misc/log';
+import { Log } from 'mol-app/ui/misc/log';
+import { JobsController } from 'mol-app/controller/misc/jobs';
+import { BackgroundJobs, Overlay } from 'mol-app/ui/misc/jobs';
+import { EntityTree } from 'mol-app/ui/entity/tree';
+import { EntityTreeController } from 'mol-app/controller/entity/tree';
+import { TransformListController } from 'mol-app/controller/transform/list';
+import { TransformList } from 'mol-app/ui/transform/list';
+
+const elm = document.getElementById('app')
+if (!elm) throw new Error('Can not find element with id "app".')
+
+const ctx = new Context()
+const targets = makeEmptyTargets();
+
+targets[LayoutRegion.Main].components.push({
+    key: 'molstar-internal-viewport',
+    controller: ctx.viewport,
+    region: LayoutRegion.Main,
+    view: Viewport,
+    isStatic: true
+});
+
+targets[LayoutRegion.Bottom].components.push({
+    key: 'molstar-log',
+    controller: new LogController(ctx),
+    region: LayoutRegion.Bottom,
+    view: Log,
+    isStatic: true
+});
+
+targets[LayoutRegion.Main].components.push({
+    key: 'molstar-background-jobs',
+    controller: new JobsController(ctx, 'Background'),
+    region: LayoutRegion.Main,
+    view: BackgroundJobs,
+    isStatic: true
+});
+
+targets[LayoutRegion.Root].components.push({
+    key: 'molstar-overlay',
+    controller: new JobsController(ctx, 'Normal'),
+    region: LayoutRegion.Root,
+    view: Overlay,
+    isStatic: true
+});
+
+targets[LayoutRegion.Right].components.push({
+    key: 'molstar-transform-list',
+    controller: new TransformListController(ctx),
+    region: LayoutRegion.Right,
+    view: TransformList,
+    isStatic: false
+});
+
+targets[LayoutRegion.Left].components.push({
+    key: 'molstar-entity-tree',
+    controller: new EntityTreeController(ctx),
+    region: LayoutRegion.Left,
+    view: EntityTree,
+    isStatic: true
+});
+
+ctx.createLayout(targets, elm)
+ctx.layout.setState({
+    isExpanded: true,
+    hideControls: false,
+    collapsedControlsLayout: 0
+})
+// ctx.viewport.setState()
+
+ReactDOM.render(React.createElement(Layout, { controller: ctx.layout }), elm);

+ 61 - 0
src/mol-app/context/context.ts

@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ */
+
+import { UUID } from 'mol-util'
+import { PerformanceMonitor } from 'mol-util/performance-monitor';
+import { Dispatcher } from '../service/dispatcher'
+import { Logger } from '../service/logger'
+import { LayoutTarget, LayoutController } from '../controller/layout';
+import { ViewportController } from '../controller/visualization/viewport';
+import { Stage } from 'mol-view/stage';
+import { AnyTransform } from 'mol-view/state/transform';
+import { BehaviorSubject } from 'rxjs';
+import { AnyEntity } from 'mol-view/state/entity';
+
+export class Settings {
+    private settings = new Map<string, any>();
+
+    set(key: string, value: any) {
+        this.settings.set(key, value);
+    }
+
+    get(key: string) {
+        return this.settings.get(key);
+    }
+}
+
+export class Context {
+    id = UUID.create()
+
+    dispatcher = new Dispatcher();
+    logger = new Logger(this);
+    performance = new PerformanceMonitor();
+
+    stage = new Stage();
+    viewport = new ViewportController(this);
+    layout: LayoutController;
+    settings = new Settings();
+
+    currentEntity = new BehaviorSubject(undefined) as BehaviorSubject<AnyEntity | undefined>
+    currentTransforms = new BehaviorSubject([] as AnyTransform[])
+
+    createLayout(targets: LayoutTarget[], target: HTMLElement) {
+        this.layout = new LayoutController(this, targets, target);
+    }
+
+    initStage(canvas: HTMLCanvasElement, container: HTMLDivElement) {
+        this.stage.initRenderer(canvas, container)
+        return true
+    }
+
+    destroy() {
+        if (this.stage) {
+            this.stage.dispose()
+            this.stage = null as any
+        }
+    }
+}

+ 49 - 0
src/mol-app/controller/controller.ts

@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ */
+
+import { BehaviorSubject } from 'rxjs';
+import { merge } from 'mol-util';
+import { Context } from '../context/context'
+import { LayoutRegion } from './layout';
+
+export class Controller<State> {
+
+    private _state = new BehaviorSubject<State>(<any>void 0);
+    private _latestState: State = <any>void 0;
+
+    get dispatcher() {
+        return this.context.dispatcher;
+    }
+
+    setState(...states: Partial<State>[]) {
+        let s = merge(this._latestState, ...states);
+        if (s !== this._latestState) {
+            this._latestState = s;
+            this._state.next(s);
+        }
+    }
+
+    get state() {
+        return this._state;
+    }
+
+    get latestState() {
+        return this._latestState;
+    }
+
+    constructor(public context: Context, initialState: State) {
+        this._latestState = initialState;
+    }
+}
+
+export interface ControllerInfo {
+    key: string;
+    controller: Controller<any>;
+    view: any;
+    region: LayoutRegion;
+    isStatic?: boolean;
+}

+ 27 - 0
src/mol-app/controller/entity/tree.ts

@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ */
+
+import { Context } from '../../context/context'
+import { Controller } from '../controller';
+import { AnyEntity } from 'mol-view/state/entity';
+
+export interface EntityTreeState {
+    entities: Set<AnyEntity>
+}
+
+export class EntityTreeController extends Controller<EntityTreeState> {
+    constructor(context: Context) {
+        super(context, { entities: new Set() });
+
+        context.stage.ctx.change.subscribe(() => {
+            if (context.stage.ctx) {
+                this.state.next({ entities: context.stage.ctx.entities }) // TODO
+                this.setState({ entities: context.stage.ctx.entities })
+            }
+        })
+    }
+}

+ 216 - 0
src/mol-app/controller/layout.ts

@@ -0,0 +1,216 @@
+/*
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ */
+
+import { Context } from '../context/context'
+import { Controller, ControllerInfo } from './controller'
+import { CommonEvents, LayoutEvents } from '../event/basic';
+
+export enum LayoutRegion {
+    Main     = 0,
+    Top      = 1,
+    Right    = 2,
+    Bottom   = 3,
+    Left     = 4,
+    Root     = 5
+}
+
+export enum CollapsedControlsLayout {
+    Outside   = 0,
+    Landscape = 1,
+    Portrait  = 2
+}
+
+export class LayoutTarget {
+    components: ControllerInfo[] = [];
+    constructor(public cssClass: string) {
+    }
+}
+
+export function makeEmptyTargets() {
+    let ret: LayoutTarget[] = [];
+    for (let i = 0; i <= LayoutRegion.Root; i++) {
+        ret.push(new LayoutTarget(LayoutRegion[i].toLowerCase()));
+    }
+    return ret;
+}
+
+export type RegionState = 'Hidden' | 'Sticky' | 'Default'
+
+export interface LayoutState {
+    isExpanded: boolean,
+    hideControls: boolean,
+    collapsedControlsLayout: CollapsedControlsLayout,
+    regionStates?: { [region: number]: RegionState }
+}
+
+interface RootState {
+    top: string | null,
+    bottom: string | null,
+    left: string | null,
+    right: string | null,
+
+    width: string | null;
+    height: string | null;
+    maxWidth: string | null;
+    maxHeight: string | null;
+    margin: string | null;
+    marginLeft: string | null;
+    marginRight: string | null;
+    marginTop: string | null;
+    marginBottom: string | null;
+
+    scrollTop: number,
+    scrollLeft: number,
+    position: string | null,
+    overflow: string | null,
+    viewports: HTMLElement[],
+    zindex: string | null
+}
+
+export class LayoutController extends Controller<LayoutState> {
+
+    update(state: Partial<LayoutState>) {
+        let prevExpanded = !!this.latestState.isExpanded;
+        this.setState(state);
+        if (typeof state.isExpanded === 'boolean' && state.isExpanded !== prevExpanded) this.handleExpand();
+
+        this.dispatcher.schedule(() => CommonEvents.LayoutChanged.dispatch(this.context, {}));
+    }
+
+    private rootState: RootState | undefined = void 0;
+    private expandedViewport: HTMLMetaElement;
+
+    private getScrollElement() {
+        if ((document as any).scrollingElement) return (document as any).scrollingElement;
+        if (document.documentElement) return document.documentElement;
+        return document.body;
+    }
+
+    private handleExpand() {
+        try {
+            let body = document.getElementsByTagName('body')[0];
+            let head = document.getElementsByTagName('head')[0];
+
+            if (!body || !head) return;
+
+            if (this.latestState.isExpanded) {
+
+                let children = head.children;
+                let hasExp = false;
+                let viewports: HTMLElement[] = [];
+                for (let i = 0; i < children.length; i++) {
+                    if (children[i] === this.expandedViewport) {
+                        hasExp = true;
+                    } else if (((children[i] as any).name || '').toLowerCase() === 'viewport') {
+                        viewports.push(children[i] as any);
+                    }
+                }
+
+                for (let v of viewports) {
+                    head.removeChild(v);
+                }
+
+                if (!hasExp) head.appendChild(this.expandedViewport);
+
+
+                let s = body.style;
+
+                let doc = this.getScrollElement();
+                let scrollLeft = doc.scrollLeft;
+                let scrollTop = doc.scrollTop;
+
+                this.rootState = {
+                    top: s.top, bottom: s.bottom, right: s.right, left: s.left, scrollTop, scrollLeft, position: s.position, overflow: s.overflow, viewports, zindex: this.root.style.zIndex,
+                    width: s.width, height: s.height,
+                    maxWidth: s.maxWidth, maxHeight: s.maxHeight,
+                    margin: s.margin, marginLeft: s.marginLeft, marginRight: s.marginRight, marginTop: s.marginTop, marginBottom: s.marginBottom
+                };
+
+                s.overflow = 'hidden';
+                s.position = 'fixed';
+                s.top = '0';
+                s.bottom = '0';
+                s.right = '0';
+                s.left = '0';
+
+                s.width = '100%';
+                s.height = '100%';
+                s.maxWidth = '100%';
+                s.maxHeight = '100%';
+                s.margin = '0';
+                s.marginLeft = '0';
+                s.marginRight = '0';
+                s.marginTop = '0';
+                s.marginBottom = '0';
+
+                this.root.style.zIndex = '100000';
+            } else {
+                // root.style.overflow = rootOverflow;
+                let children = head.children;
+                for (let i = 0; i < children.length; i++) {
+                    if (children[i] === this.expandedViewport) {
+                        head.removeChild(this.expandedViewport);
+                        break;
+                    }
+                }
+
+                if (this.rootState) {
+                    let s = body.style, t = this.rootState;
+                    for (let v of t.viewports) {
+                        head.appendChild(v);
+                    }
+                    s.top = t.top;
+                    s.bottom = t.bottom;
+                    s.left = t.left;
+                    s.right = t.right;
+
+                    s.width = t.width;
+                    s.height = t.height;
+                    s.maxWidth = t.maxWidth;
+                    s.maxHeight = t.maxHeight;
+                    s.margin = t.margin;
+                    s.marginLeft = t.marginLeft;
+                    s.marginRight = t.marginRight;
+                    s.marginTop = t.marginTop;
+                    s.marginBottom = t.marginBottom;
+
+                    s.position = t.position;
+                    s.overflow = t.overflow;
+                    let doc = this.getScrollElement();
+                    doc.scrollTop = t.scrollTop;
+                    doc.scrollLeft = t.scrollLeft;
+                    this.rootState = void 0;
+                    this.root.style.zIndex = t.zindex;
+                }
+            }
+        } catch (e) {
+            this.context.logger.error('Layout change error, you might have to reload the page.');
+            console.log('Layout change error, you might have to reload the page.', e);
+        }
+    }
+
+    updateTargets(targets: LayoutTarget[]) {
+        this.targets = targets;
+        this.dispatcher.schedule(() => CommonEvents.ComponentsChanged.dispatch(this.context, {}));
+    }
+
+    constructor(context: Context, public targets: LayoutTarget[], private root: HTMLElement) {
+        super(context, {
+            isExpanded: false,
+            hideControls: false,
+            collapsedControlsLayout: CollapsedControlsLayout.Outside,
+            regionStates: { }
+        });
+
+        LayoutEvents.SetState.getStream(this.context).subscribe(e => this.update(e.data));
+
+        // <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
+        this.expandedViewport = document.createElement('meta') as any;
+        this.expandedViewport.name = 'viewport';
+        this.expandedViewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0';
+    }
+}

+ 77 - 0
src/mol-app/controller/misc/jobs.ts

@@ -0,0 +1,77 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ */
+
+import { Map } from 'immutable'
+import { filter } from 'rxjs/operators';
+
+import { Controller } from '../controller'
+import { JobEvents } from '../../event/basic';
+import { Context } from '../../context/context';
+import { Job } from '../../service/job';
+
+
+export interface JobInfo {
+    name: string;
+    message: string;
+    abort?: () => void
+}
+
+export interface JobsState {
+    jobs: Map<number, JobInfo>
+}
+
+export class JobsController extends Controller<JobsState> {
+    private updated(state: Job.State) {
+        let isWatched = state.type === this.type;
+        let jobs = this.latestState.jobs!;
+
+        if (!isWatched) {
+            if (jobs.has(state.jobId)) {
+                jobs = jobs.delete(state.jobId);
+                this.setState({ jobs });
+            }
+            return;
+        }
+
+        jobs = jobs.set(state.jobId, {
+            name: state.name,
+            message: state.message,
+            abort: state.abort
+        });
+        this.setState({ jobs });
+    }
+
+    private started(job: Job.Info) {
+        this.setState({
+            jobs: this.latestState.jobs!.set(job.id, { name: job.name, message: 'Running...' })
+        });
+    }
+
+    private completed(taskId: number) {
+        if (!this.latestState.jobs!.has(taskId)) return;
+
+        this.setState({
+            jobs: this.latestState.jobs!.delete(taskId)
+        });
+    }
+
+    constructor(context: Context, private type: Job.Type) {
+        super(context, {
+            jobs: Map<number, JobInfo>()
+        });
+
+        JobEvents.StateUpdated.getStream(this.context)
+            .subscribe(e => this.updated(e.data));
+
+        JobEvents.Started.getStream(this.context).pipe(
+            filter(e => e.data.type === type))
+            .subscribe(e => this.started(e.data));
+
+        JobEvents.Completed.getStream(this.context)
+            .subscribe(e => this.completed(e.data));
+    }
+}

+ 22 - 0
src/mol-app/controller/misc/log.ts

@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ */
+
+import { List } from 'immutable'
+
+import { Controller } from '../controller'
+import { Context } from '../../context/context';
+import { LogEvent } from '../../event/basic';
+import { Logger } from '../../service/logger';
+
+export class LogController extends Controller<{ entries: List<Logger.Entry> }> {
+    constructor(context: Context) {
+        super(context, { entries: List<Logger.Entry>() });
+
+        LogEvent.getStream(this.context)
+            .subscribe(e => this.setState({ entries: this.latestState.entries.push(e.data) }))
+    }
+}

+ 27 - 0
src/mol-app/controller/transform/list.ts

@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ */
+
+import { Context } from '../../context/context'
+import { Controller } from '../controller';
+import { AnyTransform } from 'mol-view/state/transform';
+import { AnyEntity } from 'mol-view/state/entity';
+
+export interface TransformListState {
+    entity?: AnyEntity
+    transforms: AnyTransform[]
+}
+
+export class TransformListController extends Controller<TransformListState> {
+    constructor(context: Context) {
+        super(context, { transforms: [], entity: undefined });
+
+        context.currentTransforms.subscribe((transforms) => {
+            this.state.next({ transforms, entity: context.currentEntity.getValue() }) // TODO
+            this.setState({ transforms, entity: context.currentEntity.getValue() })
+        })
+    }
+}

+ 27 - 0
src/mol-app/controller/visualization/viewport.ts

@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ */
+
+// import { throttle } from 'rxjs/operators';
+// import { interval } from 'rxjs';
+
+import { shallowClone } from 'mol-util';
+import { Context } from '../../context/context'
+import { Controller } from '../controller';
+
+export const DefaultViewportOptions = {
+    clearColor: { r: 1, g: 1, b: 1 },
+    enableFog: true,
+    cameraFOV: 30,
+    cameraSpeed: 4
+}
+export type ViewportOptions = typeof DefaultViewportOptions
+
+export class ViewportController extends Controller<ViewportOptions> {
+    constructor(context: Context) {
+        super(context, shallowClone(DefaultViewportOptions));
+    }
+}

+ 33 - 0
src/mol-app/event/basic.ts

@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ */
+
+import { Event } from './event'
+import { Logger } from '../service/logger';
+import { Dispatcher } from '../service/dispatcher'
+import { LayoutState } from '../controller/layout';
+import { ViewportOptions } from '../controller/visualization/viewport';
+import { Job } from '../service/job';
+
+const Lane = Dispatcher.Lane;
+
+export const LogEvent = Event.create<Logger.Entry>('bs.Log', Lane.Log);
+
+export namespace CommonEvents {
+    export const LayoutChanged = Event.create('bs.Common.LayoutChanged', Lane.Slow);
+    export const ComponentsChanged = Event.create('bs.Common.ComponentsChanged', Lane.Slow);
+}
+
+export namespace JobEvents {
+    export const Started = Event.create<Job.Info>('bs.Jobs.Started', Lane.Job);
+    export const Completed = Event.create<number>('bs.Jobs.Completed', Lane.Job);
+    export const StateUpdated = Event.create<Job.State>('bs.Jobs.StateUpdated', Lane.Busy);
+}
+
+export namespace LayoutEvents {
+    export const SetState = Event.create<Partial<LayoutState>>('lm.cmd.Layout.SetState', Lane.Slow);
+    export const SetViewportOptions = Event.create<ViewportOptions>('bs.cmd.Layout.SetViewportOptions', Lane.Slow);
+}

+ 43 - 0
src/mol-app/event/event.ts

@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ */
+
+import { Observable } from 'rxjs';
+import { Context } from '../context/context'
+import { Dispatcher } from '../service/dispatcher'
+
+export interface Event<T> {
+    type: Event.Type<T>;
+    data: T;
+}
+
+export namespace Event {
+    export type Stream<T> = Observable<Event<T>>;
+
+    import Lane = Dispatcher.Lane
+
+    export type Any = Event<any>
+    export type AnyType = Type<any>
+
+    export interface Type<T> {
+        name: string,
+        lane: Lane,
+        dispatch(context: Context, data: T): void;
+        getStream(context: Context): Stream<T>;
+    }
+
+    const EventPrototype = {
+        dispatch<T>(this: any, context: Context, data: T) { context.dispatcher.dispatch({ type: this, data }) },
+        getStream(this: any, context: Context) { return context.dispatcher.getStream(this); }
+    }
+
+    export function create<T>(name: string, lane: Dispatcher.Lane): Type<T> {
+        return Object.create(EventPrototype, {
+            name: { writable: false, configurable: false, value: name },
+            lane: { writable: false, configurable: false, value: lane }
+        });
+    }
+}

+ 59 - 0
src/mol-app/service/dispatcher.ts

@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ */
+
+import { Subject } from 'rxjs';
+import { filter } from 'rxjs/operators';
+import { Event } from '../event/event'
+
+export class Dispatcher {
+    LOG_DISPATCH_STREAM = false;
+
+    private lanes: Subject<Event<any>>[] = [];
+    constructor() {
+        for (let i = 0; i <= Dispatcher.Lane.Job; i++) {
+            this.lanes.push(new Subject<Event<any>>());
+        }
+    }
+
+    dispatch<T>(event: Event<T>) {
+        if (this.LOG_DISPATCH_STREAM) console.log(event.type.name, Dispatcher.Lane[event.type.lane], event.data);
+        this.lanes[event.type.lane].next(event);
+    }
+
+    schedule(action: () => void, onError?: (e: string) => void, timeout = 1000 / 31) {
+        return setTimeout(() => {
+            if (onError) {
+                try {
+                    action.call(null)
+                } catch (e) {
+                    onError.call(null, '' + e);
+                }
+            } else {
+                action.call(null);
+            }
+        }, timeout);
+    }
+
+    getStream<T>(type: Event.Type<T>): Event.Stream<T> {
+        return this.lanes[type.lane].pipe(filter(e => e.type === type));
+    }
+
+    finished() {
+        this.lanes.forEach(l => l.complete());
+    }
+}
+
+export namespace Dispatcher {
+    export enum Lane {
+        Slow = 0,
+        Fast = 1,
+        Log = 2,
+        Busy = 3,
+        Transformer = 4,
+        Job = 5
+    }
+}

+ 132 - 0
src/mol-app/service/job.ts

@@ -0,0 +1,132 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ */
+
+import { Context } from '../context/context'
+import { JobEvents } from '../event/basic';
+import { PerformanceMonitor } from 'mol-util/performance-monitor';
+import { formatProgress } from 'mol-util';
+import { Progress, Task, Run } from 'mol-task';
+
+export class Job<T> {
+    private info: Job.Info;
+    get id() { return this.info.id; }
+    get reportTime() { return this.info.reportTime; }
+
+    run(context: Context) {
+        return this.runWithContext(context).result;
+    }
+
+    runWithContext(context: Context): Job.Running<T> {
+        return new Job.Running(context, this.task, this.info);
+    }
+
+    setReportTime(report: boolean) {
+        this.info.reportTime = report;
+        return this;
+    }
+
+    constructor(public name: string, public type: Job.Type, private task: Task<T>) {
+        this.info = {
+            id: serialJobId++,
+            name,
+            type,
+            reportTime: false
+        };
+    }
+}
+
+let serialJobId = 0;
+export namespace Job {
+    export let __DEBUG_MODE__ = false;
+
+    export type Type = 'Normal' | 'Background' | 'Silent';
+
+    export interface Info {
+        id: number,
+        type: Type,
+        name: string,
+        reportTime: boolean
+    }
+
+    export class Running<T> {
+        result: Promise<T>;
+
+        private progressUpdated(progress: Progress) {
+            JobEvents.StateUpdated.dispatch(this.context, {
+                jobId: this.info.id,
+                type: this.info.type,
+                name: this.info.name,
+                message: formatProgress(progress),
+                abort: progress.requestAbort
+            });
+        }
+
+        private resolved() {
+            try {
+                this.context.performance.end('job' + this.info.id);
+                if (this.info.reportTime) {
+                    let time = this.context.performance.time('job' + this.info.id);
+                    if (this.info.type !== 'Silent') this.context.logger.info(`${this.info.name} finished in ${PerformanceMonitor.format(time)}.`)
+                }
+            } finally {
+                JobEvents.Completed.dispatch(this.context, this.info.id);
+            }
+        }
+
+        private rejected(err: any) {
+            this.context.performance.end('job' + this.info.id);
+            this.context.performance.formatTime('job' + this.info.id);
+
+            if (__DEBUG_MODE__) {
+                console.error(err);
+            }
+
+            try {
+                if (this.info.type === 'Silent') {
+                    if (err.warn)  this.context.logger.warning(`Warning (${this.info.name}): ${err.message}`);
+                    else console.error(`Error (${this.info.name})`, err);
+                } else {
+                    if (err.warn) {
+                        this.context.logger.warning(`Warning (${this.info.name}): ${err.message}`);
+                    } else {
+                        let e = '' + err;
+                        if (e.indexOf('Aborted') >= 0) this.context.logger.info(`${this.info.name}: Aborted.`);
+                        else this.context.logger.error(`Error (${this.info.name}): ${err}`);
+                    }
+                }
+            } catch (e) {
+                console.error(e);
+            } finally {
+                JobEvents.Completed.dispatch(this.context, this.info.id);
+            }
+        }
+
+        private run() {
+            JobEvents.Started.dispatch(this.context, this.info);
+            this.context.performance.start('job' + this.info.id);
+
+            this.result = Run(this.task, (p: Progress) => this.progressUpdated(p), 250)
+            this.result.then(() => this.resolved()).catch(e => this.rejected(e));
+        }
+
+        constructor(private context: Context, private task: Task<T>, private info: Info) {
+            this.run();
+        }
+    }
+
+    export interface State {
+        jobId: number,
+        type: Type,
+        name: string,
+        message: string,
+        abort?: () => void
+    }
+
+    export function create<T>(name: string, type: Type, task: Task<T>) {
+        return new Job<T>(name, type, task);
+    }
+}

+ 51 - 0
src/mol-app/service/logger.ts

@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ */
+
+import { LogEvent } from '../event/basic'
+import { Context } from '../context/context'
+
+export class Logger {
+
+    private log(e: Logger.Entry) {
+        LogEvent.dispatch(this.context, e);
+    }
+
+    message(m: string) {
+        this.log({ type: Logger.EntryType.Message, timestamp: new Date(), message: m });
+    }
+
+    error(m: string) {
+        this.log({ type: Logger.EntryType.Error, timestamp: new Date(), message: m });
+    }
+
+    warning(m: string) {
+        this.log({ type: Logger.EntryType.Warning, timestamp: new Date(), message: m });
+    }
+
+    info(m: string) {
+        this.log({ type: Logger.EntryType.Info, timestamp: new Date(), message: m });
+    }
+
+    constructor(private context: Context) {
+
+    }
+}
+
+export namespace Logger {
+    export enum EntryType {
+        Message,
+        Error,
+        Warning,
+        Info
+    }
+
+    export interface Entry {
+        type: EntryType;
+        timestamp: Date;
+        message: any
+    }
+}

+ 45 - 0
src/mol-app/skin/base.scss

@@ -0,0 +1,45 @@
+
+@font-face {
+    font-family: 'fontello';
+    src: url('./fonts/fontello.eot');
+    src: url('./fonts/fontello.eot#iefix') format('embedded-opentype'),
+         url('./fonts/fontello.woff2') format('woff2'),
+         url('./fonts/fontello.woff') format('woff'),
+         url('./fonts/fontello.ttf') format('truetype'),
+         url('./fonts/fontello.svg#fontello') format('svg');
+    font-weight: normal;
+    font-style: normal;
+}
+
+@import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400italic,700);
+
+.molstar-plugin {
+    font-family: "Helvetica Neue", "Source Sans Pro", Helvetica, Arial, sans-serif;
+    font-size: 14px;
+    line-height: 1.42857143;
+
+    position: absolute;
+    left: 0;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    @import 'variables';
+
+    // for bootstrap
+    $border-radius-base:        0;
+    $border-radius-large:       0;
+    $border-radius-small:       0;
+
+    @import 'bootstrap';
+
+    @import 'icons';
+    @import 'layout';
+    @import 'ui';
+    @import 'logo';
+
+    .molstar-plugin-content {
+        color: $font-color;
+    }
+
+    background: $default-background;
+}

+ 25 - 0
src/mol-app/skin/bootstrap.scss

@@ -0,0 +1,25 @@
+/*!
+ * Bootstrap v3.3.6 (http://getbootstrap.com)
+ * Copyright 2011-2015 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+
+// Core variables and mixins
+@import "bootstrap/variables";
+@import "bootstrap/mixins";
+
+// Reset and dependencies
+@import "bootstrap/normalize";
+
+// Core CSS
+@import "bootstrap/scaffolding";
+@import "bootstrap/type";
+@import "bootstrap/forms";
+@import "bootstrap/buttons";
+
+// Components
+@import "bootstrap/button-groups";
+@import "bootstrap/input-groups";
+
+@import "bootstrap/labels";
+@import "bootstrap/badges";

+ 68 - 0
src/mol-app/skin/bootstrap/badges.scss

@@ -0,0 +1,68 @@
+//
+// Badges
+// --------------------------------------------------
+
+
+// Base class
+.badge {
+    display: inline-block;
+    min-width: 10px;
+    padding: 3px 7px;
+    font-size: $font-size-small;
+    font-weight: $badge-font-weight;
+    color: $badge-color;
+    line-height: $badge-line-height;
+    vertical-align: middle;
+    white-space: nowrap;
+    text-align: center;
+    background-color: $badge-bg;
+    border-radius: $badge-border-radius;
+
+    // Empty badges collapse automatically (not available in IE8)
+    &:empty {
+      display: none;
+    }
+
+    // Quick fix for badges in buttons
+    .molstar-btn & {
+      position: relative;
+      top: -1px;
+    }
+
+    .molstar-btn-xs &,
+    .molstar-btn-group-xs > .molstar-btn & {
+      top: 0;
+      padding: 1px 5px;
+    }
+
+    // [converter] extracted a& to a.badge
+
+    // Account for badges in navs
+    .list-group-item.active > &,
+    .nav-pills > .active > a > & {
+      color: $badge-active-color;
+      background-color: $badge-active-bg;
+    }
+
+    .list-group-item > & {
+      float: right;
+    }
+
+    .list-group-item > & + & {
+      margin-right: 5px;
+    }
+
+    .nav-pills > li > a > & {
+      margin-left: 3px;
+    }
+  }
+
+  // Hover state, but only for links
+  a.badge {
+    &:hover,
+    &:focus {
+      color: $badge-link-hover-color;
+      text-decoration: none;
+      cursor: pointer;
+    }
+  }

+ 244 - 0
src/mol-app/skin/bootstrap/button-groups.scss

@@ -0,0 +1,244 @@
+//
+// Button groups
+// --------------------------------------------------
+
+// Make the div behave like a button
+.molstar-btn-group,
+.molstar-btn-group-vertical {
+  position: relative;
+  display: inline-block;
+  vertical-align: middle; // match .molstar-btn alignment given font-size hack above
+  > .molstar-btn {
+    position: relative;
+    float: left;
+    // Bring the "active" button to the front
+    &:hover,
+    &:focus,
+    &:active,
+    &.active {
+      z-index: 2;
+    }
+  }
+}
+
+// Prevent double borders when buttons are next to each other
+.molstar-btn-group {
+  .molstar-btn + .molstar-btn,
+  .molstar-btn + .molstar-btn-group,
+  .molstar-btn-group + .molstar-btn,
+  .molstar-btn-group + .molstar-btn-group {
+    margin-left: -1px;
+  }
+}
+
+// Optional: Group multiple button groups together for a toolbar
+.molstar-btn-toolbar {
+  margin-left: -5px; // Offset the first child's margin
+  @include clearfix;
+
+  .molstar-btn,
+  .molstar-btn-group,
+  .input-group {
+    float: left;
+  }
+  > .molstar-btn,
+  > .molstar-btn-group,
+  > .input-group {
+    margin-left: 5px;
+  }
+}
+
+.molstar-btn-group > .molstar-btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {
+  border-radius: 0;
+}
+
+// Set corners individual because sometimes a single button can be in a .molstar-btn-group and we need :first-child and :last-child to both match
+.molstar-btn-group > .molstar-btn:first-child {
+  margin-left: 0;
+  &:not(:last-child):not(.dropdown-toggle) {
+    @include border-right-radius(0);
+  }
+}
+// Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it
+.molstar-btn-group > .molstar-btn:last-child:not(:first-child),
+.molstar-btn-group > .dropdown-toggle:not(:first-child) {
+  @include border-left-radius(0);
+}
+
+// Custom edits for including molstar-btn-groups within molstar-btn-groups (useful for including dropdown buttons within a molstar-btn-group)
+.molstar-btn-group > .molstar-btn-group {
+  float: left;
+}
+.molstar-btn-group > .molstar-btn-group:not(:first-child):not(:last-child) > .molstar-btn {
+  border-radius: 0;
+}
+.molstar-btn-group > .molstar-btn-group:first-child:not(:last-child) {
+  > .molstar-btn:last-child,
+  > .dropdown-toggle {
+    @include border-right-radius(0);
+  }
+}
+.molstar-btn-group > .molstar-btn-group:last-child:not(:first-child) > .molstar-btn:first-child {
+  @include border-left-radius(0);
+}
+
+// On active and open, don't show outline
+.molstar-btn-group .dropdown-toggle:active,
+.molstar-btn-group.open .dropdown-toggle {
+  outline: 0;
+}
+
+
+// Sizing
+//
+// Remix the default button sizing classes into new ones for easier manipulation.
+
+.molstar-btn-group-xs > .molstar-btn { @extend .molstar-btn-xs; }
+.molstar-btn-group-sm > .molstar-btn { @extend .molstar-btn-sm; }
+.molstar-btn-group-lg > .molstar-btn { @extend .molstar-btn-lg; }
+
+
+// Split button dropdowns
+// ----------------------
+
+// Give the line between buttons some depth
+.molstar-btn-group > .molstar-btn + .dropdown-toggle {
+  padding-left: 8px;
+  padding-right: 8px;
+}
+.molstar-btn-group > .molstar-btn-lg + .dropdown-toggle {
+  padding-left: 12px;
+  padding-right: 12px;
+}
+
+// The clickable button for toggling the menu
+// Remove the gradient and set the same inset shadow as the :active state
+.molstar-btn-group.open .dropdown-toggle {
+  @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125));
+
+  // Show no shadow for `.molstar-btn-link` since it has no other button styles.
+  &.molstar-btn-link {
+    @include box-shadow(none);
+  }
+}
+
+
+// Reposition the caret
+.molstar-btn .caret {
+  margin-left: 0;
+}
+// Carets in other button sizes
+.molstar-btn-lg .caret {
+  border-width: $caret-width-large $caret-width-large 0;
+  border-bottom-width: 0;
+}
+// Upside down carets for .dropup
+.dropup .molstar-btn-lg .caret {
+  border-width: 0 $caret-width-large $caret-width-large;
+}
+
+
+// Vertical button groups
+// ----------------------
+
+.molstar-btn-group-vertical {
+  > .molstar-btn,
+  > .molstar-btn-group,
+  > .molstar-btn-group > .molstar-btn {
+    display: block;
+    float: none;
+    width: 100%;
+    max-width: 100%;
+  }
+
+  // Clear floats so dropdown menus can be properly placed
+  > .molstar-btn-group {
+    @include clearfix;
+    > .molstar-btn {
+      float: none;
+    }
+  }
+
+  > .molstar-btn + .molstar-btn,
+  > .molstar-btn + .molstar-btn-group,
+  > .molstar-btn-group + .molstar-btn,
+  > .molstar-btn-group + .molstar-btn-group {
+    margin-top: -1px;
+    margin-left: 0;
+  }
+}
+
+.molstar-btn-group-vertical > .molstar-btn {
+  &:not(:first-child):not(:last-child) {
+    border-radius: 0;
+  }
+  &:first-child:not(:last-child) {
+    @include border-top-radius($molstar-btn-border-radius-base);
+    @include border-bottom-radius(0);
+  }
+  &:last-child:not(:first-child) {
+    @include border-top-radius(0);
+    @include border-bottom-radius($molstar-btn-border-radius-base);
+  }
+}
+.molstar-btn-group-vertical > .molstar-btn-group:not(:first-child):not(:last-child) > .molstar-btn {
+  border-radius: 0;
+}
+.molstar-btn-group-vertical > .molstar-btn-group:first-child:not(:last-child) {
+  > .molstar-btn:last-child,
+  > .dropdown-toggle {
+    @include border-bottom-radius(0);
+  }
+}
+.molstar-btn-group-vertical > .molstar-btn-group:last-child:not(:first-child) > .molstar-btn:first-child {
+  @include border-top-radius(0);
+}
+
+
+// Justified button groups
+// ----------------------
+
+.molstar-btn-group-justified {
+  display: table;
+  width: 100%;
+  table-layout: fixed;
+  border-collapse: separate;
+  > .molstar-btn,
+  > .molstar-btn-group {
+    float: none;
+    display: table-cell;
+    width: 1%;
+  }
+  > .molstar-btn-group .molstar-btn {
+    width: 100%;
+  }
+
+  > .molstar-btn-group .dropdown-menu {
+    left: auto;
+  }
+}
+
+
+// Checkbox and radio options
+//
+// In order to support the browser's form validation feedback, powered by the
+// `required` attribute, we have to "hide" the inputs via `clip`. We cannot use
+// `display: none;` or `visibility: hidden;` as that also hides the popover.
+// Simply visually hiding the inputs via `opacity` would leave them clickable in
+// certain cases which is prevented by using `clip` and `pointer-events`.
+// This way, we ensure a DOM element is visible to position the popover from.
+//
+// See https://github.com/twbs/bootstrap/pull/12794 and
+// https://github.com/twbs/bootstrap/pull/14559 for more information.
+
+[data-toggle="buttons"] {
+  > .molstar-btn,
+  > .molstar-btn-group > .molstar-btn {
+    input[type="radio"],
+    input[type="checkbox"] {
+      position: absolute;
+      clip: rect(0,0,0,0);
+      pointer-events: none;
+    }
+  }
+}

+ 168 - 0
src/mol-app/skin/bootstrap/buttons.scss

@@ -0,0 +1,168 @@
+//
+// Buttons
+// --------------------------------------------------
+
+
+// Base styles
+// --------------------------------------------------
+
+.molstar-btn {
+    display: inline-block;
+    margin-bottom: 0; // For input.molstar-btn
+    font-weight: $molstar-btn-font-weight;
+    text-align: center;
+    vertical-align: middle;
+    touch-action: manipulation;
+    cursor: pointer;
+    background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214
+    border: 1px solid transparent;
+    white-space: nowrap;
+    @include button-size($padding-base-vertical, $padding-base-horizontal, $font-size-base, $line-height-base, $molstar-btn-border-radius-base);
+    @include user-select(none);
+
+    &,
+    &:active,
+    &.active {
+      &:focus,
+      &.focus {
+        @include tab-focus;
+      }
+    }
+
+    &:hover,
+    &:focus,
+    &.focus {
+      color: $molstar-btn-default-color;
+      text-decoration: none;
+    }
+
+    &:active,
+    &.active {
+      outline: 0;
+      background-image: none;
+      @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125));
+    }
+
+    &.disabled,
+    &[disabled],
+    fieldset[disabled] & {
+      cursor: $cursor-disabled;
+      @include opacity(.65);
+      @include box-shadow(none);
+    }
+
+    // [converter] extracted a& to a.molstar-btn
+  }
+
+  a.molstar-btn {
+    &.disabled,
+    fieldset[disabled] & {
+      pointer-events: none; // Future-proof disabling of clicks on `<a>` elements
+    }
+  }
+
+
+  // Alternate buttons
+  // --------------------------------------------------
+
+  .molstar-btn-default {
+    @include button-variant($molstar-btn-default-color, $molstar-btn-default-bg, $molstar-btn-default-border);
+  }
+  .molstar-btn-primary {
+    @include button-variant($molstar-btn-primary-color, $molstar-btn-primary-bg, $molstar-btn-primary-border);
+  }
+  // Success appears as green
+  .molstar-btn-success {
+    @include button-variant($molstar-btn-success-color, $molstar-btn-success-bg, $molstar-btn-success-border);
+  }
+  // Info appears as blue-green
+  .molstar-btn-info {
+    @include button-variant($molstar-btn-info-color, $molstar-btn-info-bg, $molstar-btn-info-border);
+  }
+  // Warning appears as orange
+  .molstar-btn-warning {
+    @include button-variant($molstar-btn-warning-color, $molstar-btn-warning-bg, $molstar-btn-warning-border);
+  }
+  // Danger and error appear as red
+  .molstar-btn-danger {
+    @include button-variant($molstar-btn-danger-color, $molstar-btn-danger-bg, $molstar-btn-danger-border);
+  }
+
+
+  // Link buttons
+  // -------------------------
+
+  // Make a button look and behave like a link
+  .molstar-btn-link {
+    color: $link-color;
+    font-weight: normal;
+    border-radius: 0;
+
+    &,
+    &:active,
+    &.active,
+    &[disabled],
+    fieldset[disabled] & {
+      background-color: transparent;
+      @include box-shadow(none);
+    }
+    &,
+    &:hover,
+    &:focus,
+    &:active {
+      border-color: transparent;
+    }
+    &:hover,
+    &:focus {
+      color: $link-hover-color;
+      text-decoration: $link-hover-decoration;
+      background-color: transparent;
+    }
+    &[disabled],
+    fieldset[disabled] & {
+      &:hover,
+      &:focus {
+        color: $molstar-btn-link-disabled-color;
+        text-decoration: none;
+      }
+    }
+  }
+
+
+  // Button Sizes
+  // --------------------------------------------------
+
+  .molstar-btn-lg {
+    // line-height: ensure even-numbered height of button next to large input
+    @include button-size($padding-large-vertical, $padding-large-horizontal, $font-size-large, $line-height-large, $molstar-btn-border-radius-large);
+  }
+  .molstar-btn-sm {
+    // line-height: ensure proper height of button next to small input
+    @include button-size($padding-small-vertical, $padding-small-horizontal, $font-size-small, $line-height-small, $molstar-btn-border-radius-small);
+  }
+  .molstar-btn-xs {
+    @include button-size($padding-xs-vertical, $padding-xs-horizontal, $font-size-small, $line-height-small, $molstar-btn-border-radius-small);
+  }
+
+
+  // Block button
+  // --------------------------------------------------
+
+  .molstar-btn-block {
+    display: block;
+    width: 100%;
+  }
+
+  // Vertically space out multiple block buttons
+  .molstar-btn-block + .molstar-btn-block {
+    margin-top: 5px;
+  }
+
+  // Specificity overrides
+  input[type="submit"],
+  input[type="reset"],
+  input[type="button"] {
+    &.molstar-btn-block {
+      width: 100%;
+    }
+  }

+ 617 - 0
src/mol-app/skin/bootstrap/forms.scss

@@ -0,0 +1,617 @@
+//
+// Forms
+// --------------------------------------------------
+
+
+// Normalize non-controls
+//
+// Restyle and baseline non-control form elements.
+
+fieldset {
+    padding: 0;
+    margin: 0;
+    border: 0;
+    // Chrome and Firefox set a `min-width: min-content;` on fieldsets,
+    // so we reset that to ensure it behaves more like a standard block element.
+    // See https://github.com/twbs/bootstrap/issues/12359.
+    min-width: 0;
+  }
+
+  legend {
+    display: block;
+    width: 100%;
+    padding: 0;
+    margin-bottom: $line-height-computed;
+    font-size: ($font-size-base * 1.5);
+    line-height: inherit;
+    color: $legend-color;
+    border: 0;
+    border-bottom: 1px solid $legend-border-color;
+  }
+
+  label {
+    display: inline-block;
+    max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141)
+    margin-bottom: 5px;
+    font-weight: bold;
+  }
+
+
+  // Normalize form controls
+  //
+  // While most of our form styles require extra classes, some basic normalization
+  // is required to ensure optimum display with or without those classes to better
+  // address browser inconsistencies.
+
+  // Override content-box in Normalize (* isn't specific enough)
+  input[type="search"] {
+    @include box-sizing(border-box);
+  }
+
+  // Position radios and checkboxes better
+  input[type="radio"],
+  input[type="checkbox"] {
+    margin: 4px 0 0;
+    margin-top: 1px \9; // IE8-9
+    line-height: normal;
+  }
+
+  input[type="file"] {
+    display: block;
+  }
+
+  // Make range inputs behave like textual form controls
+  input[type="range"] {
+    display: block;
+    width: 100%;
+  }
+
+  // Make multiple select elements height not fixed
+  select[multiple],
+  select[size] {
+    height: auto;
+  }
+
+  // Focus for file, radio, and checkbox
+  input[type="file"]:focus,
+  input[type="radio"]:focus,
+  input[type="checkbox"]:focus {
+    @include tab-focus;
+  }
+
+  // Adjust output element
+  output {
+    display: block;
+    padding-top: ($padding-base-vertical + 1);
+    font-size: $font-size-base;
+    line-height: $line-height-base;
+    color: $input-color;
+  }
+
+
+  // Common form controls
+  //
+  // Shared size and type resets for form controls. Apply `.molstar-form-control` to any
+  // of the following form controls:
+  //
+  // select
+  // textarea
+  // input[type="text"]
+  // input[type="password"]
+  // input[type="datetime"]
+  // input[type="datetime-local"]
+  // input[type="date"]
+  // input[type="month"]
+  // input[type="time"]
+  // input[type="week"]
+  // input[type="number"]
+  // input[type="email"]
+  // input[type="url"]
+  // input[type="search"]
+  // input[type="tel"]
+  // input[type="color"]
+
+  .molstar-form-control {
+    display: block;
+    width: 100%;
+    height: $input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border)
+    padding: $padding-base-vertical $padding-base-horizontal;
+    font-size: $font-size-base;
+    line-height: $line-height-base;
+    color: $input-color;
+    background-color: $input-bg;
+    background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214
+    border: 1px solid $input-border;
+    border-radius: $input-border-radius; // Note: This has no effect on <select>s in some browsers, due to the limited stylability of <select>s in CSS.
+    //@include box-shadow(none);//inset 0 1px 1px rgba(0,0,0,.075));
+    //@include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s);
+
+    // Customize the `:focus` state to imitate native WebKit styles.
+    @include molstar-form-control-focus;
+
+    // Placeholder
+    @include placeholder;
+
+    // Unstyle the caret on `<select>`s in IE10+.
+    &::-ms-expand {
+      border: 0;
+      background-color: transparent;
+    }
+
+    // Disabled and read-only inputs
+    //
+    // HTML5 says that controls under a fieldset > legend:first-child won't be
+    // disabled if the fieldset is disabled. Due to implementation difficulty, we
+    // don't honor that edge case; we style them as disabled anyway.
+    &[disabled],
+    &[readonly],
+    fieldset[disabled] & {
+      background-color: $input-bg-disabled;
+      opacity: 1; // iOS fix for unreadable disabled content; see https://github.com/twbs/bootstrap/issues/11655
+    }
+
+    &[disabled],
+    fieldset[disabled] & {
+      cursor: $cursor-disabled;
+    }
+
+    // [converter] extracted textarea& to textarea.molstar-form-control
+  }
+
+  // Reset height for `textarea`s
+  textarea.molstar-form-control {
+    height: auto;
+  }
+
+
+  // Search inputs in iOS
+  //
+  // This overrides the extra rounded corners on search inputs in iOS so that our
+  // `.molstar-form-control` class can properly style them. Note that this cannot simply
+  // be added to `.molstar-form-control` as it's not specific enough. For details, see
+  // https://github.com/twbs/bootstrap/issues/11586.
+
+  input[type="search"] {
+    -webkit-appearance: none;
+  }
+
+
+  // Special styles for iOS temporal inputs
+  //
+  // In Mobile Safari, setting `display: block` on temporal inputs causes the
+  // text within the input to become vertically misaligned. As a workaround, we
+  // set a pixel line-height that matches the given height of the input, but only
+  // for Safari. See https://bugs.webkit.org/show_bug.cgi?id=139848
+  //
+  // Note that as of 8.3, iOS doesn't support `datetime` or `week`.
+
+  @media screen and (-webkit-min-device-pixel-ratio: 0) {
+    input[type="date"],
+    input[type="time"],
+    input[type="datetime-local"],
+    input[type="month"] {
+      &.molstar-form-control {
+        line-height: $input-height-base;
+      }
+
+      &.input-sm,
+      .input-group-sm & {
+        line-height: $input-height-small;
+      }
+
+      &.input-lg,
+      .input-group-lg & {
+        line-height: $input-height-large;
+      }
+    }
+  }
+
+
+  // Form groups
+  //
+  // Designed to help with the organization and spacing of vertical forms. For
+  // horizontal forms, use the predefined grid classes.
+
+  .form-group {
+    margin-bottom: $form-group-margin-bottom;
+  }
+
+
+  // Checkboxes and radios
+  //
+  // Indent the labels to position radios/checkboxes as hanging controls.
+
+  .radio,
+  .checkbox {
+    position: relative;
+    display: block;
+    margin-top: 10px;
+    margin-bottom: 10px;
+
+    label {
+      min-height: $line-height-computed; // Ensure the input doesn't jump when there is no text
+      padding-left: 20px;
+      margin-bottom: 0;
+      font-weight: normal;
+      cursor: pointer;
+    }
+  }
+  .radio input[type="radio"],
+  .radio-inline input[type="radio"],
+  .checkbox input[type="checkbox"],
+  .checkbox-inline input[type="checkbox"] {
+    position: absolute;
+    margin-left: -20px;
+    margin-top: 4px \9;
+  }
+
+  .radio + .radio,
+  .checkbox + .checkbox {
+    margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing
+  }
+
+  // Radios and checkboxes on same line
+  .radio-inline,
+  .checkbox-inline {
+    position: relative;
+    display: inline-block;
+    padding-left: 20px;
+    margin-bottom: 0;
+    vertical-align: middle;
+    font-weight: normal;
+    cursor: pointer;
+  }
+  .radio-inline + .radio-inline,
+  .checkbox-inline + .checkbox-inline {
+    margin-top: 0;
+    margin-left: 10px; // space out consecutive inline controls
+  }
+
+  // Apply same disabled cursor tweak as for inputs
+  // Some special care is needed because <label>s don't inherit their parent's `cursor`.
+  //
+  // Note: Neither radios nor checkboxes can be readonly.
+  input[type="radio"],
+  input[type="checkbox"] {
+    &[disabled],
+    &.disabled,
+    fieldset[disabled] & {
+      cursor: $cursor-disabled;
+    }
+  }
+  // These classes are used directly on <label>s
+  .radio-inline,
+  .checkbox-inline {
+    &.disabled,
+    fieldset[disabled] & {
+      cursor: $cursor-disabled;
+    }
+  }
+  // These classes are used on elements with <label> descendants
+  .radio,
+  .checkbox {
+    &.disabled,
+    fieldset[disabled] & {
+      label {
+        cursor: $cursor-disabled;
+      }
+    }
+  }
+
+
+  // Static form control text
+  //
+  // Apply class to a `p` element to make any string of text align with labels in
+  // a horizontal form layout.
+
+  .molstar-form-control-static {
+    // Size it appropriately next to real form controls
+    padding-top: ($padding-base-vertical + 1);
+    padding-bottom: ($padding-base-vertical + 1);
+    // Remove default margin from `p`
+    margin-bottom: 0;
+    min-height: ($line-height-computed + $font-size-base);
+
+    &.input-lg,
+    &.input-sm {
+      padding-left: 0;
+      padding-right: 0;
+    }
+  }
+
+
+  // Form control sizing
+  //
+  // Build on `.molstar-form-control` with modifier classes to decrease or increase the
+  // height and font-size of form controls.
+  //
+  // The `.form-group-* molstar-form-control` variations are sadly duplicated to avoid the
+  // issue documented in https://github.com/twbs/bootstrap/issues/15074.
+
+  @include input-size('.input-sm', $input-height-small, $padding-small-vertical, $padding-small-horizontal, $font-size-small, $line-height-small, $input-border-radius-small);
+  .form-group-sm {
+    .molstar-form-control {
+      height: $input-height-small;
+      padding: $padding-small-vertical $padding-small-horizontal;
+      font-size: $font-size-small;
+      line-height: $line-height-small;
+      border-radius: $input-border-radius-small;
+    }
+    select.molstar-form-control {
+      height: $input-height-small;
+      line-height: $input-height-small;
+    }
+    textarea.molstar-form-control,
+    select[multiple].molstar-form-control {
+      height: auto;
+    }
+    .molstar-form-control-static {
+      height: $input-height-small;
+      min-height: ($line-height-computed + $font-size-small);
+      padding: ($padding-small-vertical + 1) $padding-small-horizontal;
+      font-size: $font-size-small;
+      line-height: $line-height-small;
+    }
+  }
+
+  @include input-size('.input-lg', $input-height-large, $padding-large-vertical, $padding-large-horizontal, $font-size-large, $line-height-large, $input-border-radius-large);
+  .form-group-lg {
+    .molstar-form-control {
+      height: $input-height-large;
+      padding: $padding-large-vertical $padding-large-horizontal;
+      font-size: $font-size-large;
+      line-height: $line-height-large;
+      border-radius: $input-border-radius-large;
+    }
+    select.molstar-form-control {
+      height: $input-height-large;
+      line-height: $input-height-large;
+    }
+    textarea.molstar-form-control,
+    select[multiple].molstar-form-control {
+      height: auto;
+    }
+    .molstar-form-control-static {
+      height: $input-height-large;
+      min-height: ($line-height-computed + $font-size-large);
+      padding: ($padding-large-vertical + 1) $padding-large-horizontal;
+      font-size: $font-size-large;
+      line-height: $line-height-large;
+    }
+  }
+
+
+  // Form control feedback states
+  //
+  // Apply contextual and semantic states to individual form controls.
+
+  .has-feedback {
+    // Enable absolute positioning
+    position: relative;
+
+    // Ensure icons don't overlap text
+    .molstar-form-control {
+      padding-right: ($input-height-base * 1.25);
+    }
+  }
+  // Feedback icon (requires .glyphicon classes)
+  .molstar-form-control-feedback {
+    position: absolute;
+    top: 0;
+    right: 0;
+    z-index: 2; // Ensure icon is above input groups
+    display: block;
+    width: $input-height-base;
+    height: $input-height-base;
+    line-height: $input-height-base;
+    text-align: center;
+    pointer-events: none;
+  }
+  .input-lg + .molstar-form-control-feedback,
+  .input-group-lg + .molstar-form-control-feedback,
+  .form-group-lg .molstar-form-control + .molstar-form-control-feedback {
+    width: $input-height-large;
+    height: $input-height-large;
+    line-height: $input-height-large;
+  }
+  .input-sm + .molstar-form-control-feedback,
+  .input-group-sm + .molstar-form-control-feedback,
+  .form-group-sm .molstar-form-control + .molstar-form-control-feedback {
+    width: $input-height-small;
+    height: $input-height-small;
+    line-height: $input-height-small;
+  }
+
+  // Feedback states
+  .has-success {
+    @include molstar-form-control-validation($state-success-text, $state-success-text, $state-success-bg);
+  }
+  .has-warning {
+    @include molstar-form-control-validation($state-warning-text, $state-warning-text, $state-warning-bg);
+  }
+  .has-error {
+    @include molstar-form-control-validation($state-danger-text, $state-danger-text, $state-danger-bg);
+  }
+
+  // Reposition feedback icon if input has visible label above
+  .has-feedback label {
+
+    & ~ .molstar-form-control-feedback {
+      top: ($line-height-computed + 5); // Height of the `label` and its margin
+    }
+    &.sr-only ~ .molstar-form-control-feedback {
+      top: 0;
+    }
+  }
+
+
+  // Help text
+  //
+  // Apply to any element you wish to create light text for placement immediately
+  // below a form control. Use for general help, formatting, or instructional text.
+
+  .help-block {
+    display: block; // account for any element using help-block
+    margin-top: 5px;
+    margin-bottom: 10px;
+    color: lighten($text-color, 25%); // lighten the text some for contrast
+  }
+
+
+  // Inline forms
+  //
+  // Make forms appear inline(-block) by adding the `.form-inline` class. Inline
+  // forms begin stacked on extra small (mobile) devices and then go inline when
+  // viewports reach <768px.
+  //
+  // Requires wrapping inputs and labels with `.form-group` for proper display of
+  // default HTML form controls and our custom form controls (e.g., input groups).
+  //
+  // Heads up! This is mixin-ed into `.navbar-form` in navbars.less.
+
+  // [converter] extracted from `.form-inline` for libsass compatibility
+  @mixin form-inline {
+
+    // Kick in the inline
+    @media (min-width: $screen-sm-min) {
+      // Inline-block all the things for "inline"
+      .form-group {
+        display: inline-block;
+        margin-bottom: 0;
+        vertical-align: middle;
+      }
+
+      // In navbar-form, allow folks to *not* use `.form-group`
+      .molstar-form-control {
+        display: inline-block;
+        width: auto; // Prevent labels from stacking above inputs in `.form-group`
+        vertical-align: middle;
+      }
+
+      // Make static controls behave like regular ones
+      .molstar-form-control-static {
+        display: inline-block;
+      }
+
+      .input-group {
+        display: inline-table;
+        vertical-align: middle;
+
+        .input-group-addon,
+        .input-group-molstar-btn,
+        .molstar-form-control {
+          width: auto;
+        }
+      }
+
+      // Input groups need that 100% width though
+      .input-group > .molstar-form-control {
+        width: 100%;
+      }
+
+      .control-label {
+        margin-bottom: 0;
+        vertical-align: middle;
+      }
+
+      // Remove default margin on radios/checkboxes that were used for stacking, and
+      // then undo the floating of radios and checkboxes to match.
+      .radio,
+      .checkbox {
+        display: inline-block;
+        margin-top: 0;
+        margin-bottom: 0;
+        vertical-align: middle;
+
+        label {
+          padding-left: 0;
+        }
+      }
+      .radio input[type="radio"],
+      .checkbox input[type="checkbox"] {
+        position: relative;
+        margin-left: 0;
+      }
+
+      // Re-override the feedback icon.
+      .has-feedback .molstar-form-control-feedback {
+        top: 0;
+      }
+    }
+  }
+  // [converter] extracted as `@mixin form-inline` for libsass compatibility
+  .form-inline {
+    @include form-inline;
+  }
+
+
+
+  // Horizontal forms
+  //
+  // Horizontal forms are built on grid classes and allow you to create forms with
+  // labels on the left and inputs on the right.
+
+  .form-horizontal {
+
+    // Consistent vertical alignment of radios and checkboxes
+    //
+    // Labels also get some reset styles, but that is scoped to a media query below.
+    .radio,
+    .checkbox,
+    .radio-inline,
+    .checkbox-inline {
+      margin-top: 0;
+      margin-bottom: 0;
+      padding-top: ($padding-base-vertical + 1); // Default padding plus a border
+    }
+    // Account for padding we're adding to ensure the alignment and of help text
+    // and other content below items
+    .radio,
+    .checkbox {
+      min-height: ($line-height-computed + ($padding-base-vertical + 1));
+    }
+
+    // Make form groups behave like rows
+    .form-group {
+      @include make-row;
+    }
+
+    // Reset spacing and right align labels, but scope to media queries so that
+    // labels on narrow viewports stack the same as a default form example.
+    @media (min-width: $screen-sm-min) {
+      .control-label {
+        text-align: right;
+        margin-bottom: 0;
+        padding-top: ($padding-base-vertical + 1); // Default padding plus a border
+      }
+    }
+
+    // Validation states
+    //
+    // Reposition the icon because it's now within a grid column and columns have
+    // `position: relative;` on them. Also accounts for the grid gutter padding.
+    .has-feedback .molstar-form-control-feedback {
+      right: floor(($grid-gutter-width / 2));
+    }
+
+    // Form group sizes
+    //
+    // Quick utility class for applying `.input-lg` and `.input-sm` styles to the
+    // inputs and labels within a `.form-group`.
+    .form-group-lg {
+      @media (min-width: $screen-sm-min) {
+        .control-label {
+          padding-top: ($padding-large-vertical + 1);
+          font-size: $font-size-large;
+        }
+      }
+    }
+    .form-group-sm {
+      @media (min-width: $screen-sm-min) {
+        .control-label {
+          padding-top: ($padding-small-vertical + 1);
+          font-size: $font-size-small;
+        }
+      }
+    }
+  }

+ 171 - 0
src/mol-app/skin/bootstrap/input-groups.scss

@@ -0,0 +1,171 @@
+//
+// Input groups
+// --------------------------------------------------
+
+// Base styles
+// -------------------------
+.input-group {
+    position: relative; // For dropdowns
+    display: table;
+    border-collapse: separate; // prevent input groups from inheriting border styles from table cells when placed within a table
+
+    // Undo padding and float of grid classes
+    &[class*="col-"] {
+      float: none;
+      padding-left: 0;
+      padding-right: 0;
+    }
+
+    .molstar-form-control {
+      // Ensure that the input is always above the *appended* addon button for
+      // proper border colors.
+      position: relative;
+      z-index: 2;
+
+      // IE9 fubars the placeholder attribute in text inputs and the arrows on
+      // select elements in input groups. To fix it, we float the input. Details:
+      // https://github.com/twbs/bootstrap/issues/11561#issuecomment-28936855
+      float: left;
+
+      width: 100%;
+      margin-bottom: 0;
+
+      &:focus {
+        z-index: 3;
+      }
+    }
+  }
+
+  // Sizing options
+  //
+  // Remix the default form control sizing classes into new ones for easier
+  // manipulation.
+
+  .input-group-lg > .molstar-form-control,
+  .input-group-lg > .input-group-addon,
+  .input-group-lg > .input-group-molstar-btn > .molstar-btn {
+    @extend .input-lg;
+  }
+  .input-group-sm > .molstar-form-control,
+  .input-group-sm > .input-group-addon,
+  .input-group-sm > .input-group-molstar-btn > .molstar-btn {
+    @extend .input-sm;
+  }
+
+
+  // Display as table-cell
+  // -------------------------
+  .input-group-addon,
+  .input-group-molstar-btn,
+  .input-group .molstar-form-control {
+    display: table-cell;
+
+    &:not(:first-child):not(:last-child) {
+      border-radius: 0;
+    }
+  }
+  // Addon and addon wrapper for buttons
+  .input-group-addon,
+  .input-group-molstar-btn {
+    width: 1%;
+    white-space: nowrap;
+    vertical-align: middle; // Match the inputs
+  }
+
+  // Text input groups
+  // -------------------------
+  .input-group-addon {
+    padding: $padding-base-vertical $padding-base-horizontal;
+    font-size: $font-size-base;
+    font-weight: normal;
+    line-height: 1;
+    color: $input-color;
+    text-align: center;
+    background-color: $input-group-addon-bg;
+    border: 1px solid $input-group-addon-border-color;
+    border-radius: $input-border-radius;
+
+    // Sizing
+    &.input-sm {
+      padding: $padding-small-vertical $padding-small-horizontal;
+      font-size: $font-size-small;
+      border-radius: $input-border-radius-small;
+    }
+    &.input-lg {
+      padding: $padding-large-vertical $padding-large-horizontal;
+      font-size: $font-size-large;
+      border-radius: $input-border-radius-large;
+    }
+
+    // Nuke default margins from checkboxes and radios to vertically center within.
+    input[type="radio"],
+    input[type="checkbox"] {
+      margin-top: 0;
+    }
+  }
+
+  // Reset rounded corners
+  .input-group .molstar-form-control:first-child,
+  .input-group-addon:first-child,
+  .input-group-molstar-btn:first-child > .molstar-btn,
+  .input-group-molstar-btn:first-child > .molstar-btn-group > .molstar-btn,
+  .input-group-molstar-btn:first-child > .dropdown-toggle,
+  .input-group-molstar-btn:last-child > .molstar-btn:not(:last-child):not(.dropdown-toggle),
+  .input-group-molstar-btn:last-child > .molstar-btn-group:not(:last-child) > .molstar-btn {
+    @include border-right-radius(0);
+  }
+  .input-group-addon:first-child {
+    border-right: 0;
+  }
+  .input-group .molstar-form-control:last-child,
+  .input-group-addon:last-child,
+  .input-group-molstar-btn:last-child > .molstar-btn,
+  .input-group-molstar-btn:last-child > .molstar-btn-group > .molstar-btn,
+  .input-group-molstar-btn:last-child > .dropdown-toggle,
+  .input-group-molstar-btn:first-child > .molstar-btn:not(:first-child),
+  .input-group-molstar-btn:first-child > .molstar-btn-group:not(:first-child) > .molstar-btn {
+    @include border-left-radius(0);
+  }
+  .input-group-addon:last-child {
+    border-left: 0;
+  }
+
+  // Button input groups
+  // -------------------------
+  .input-group-molstar-btn {
+    position: relative;
+    // Jankily prevent input button groups from wrapping with `white-space` and
+    // `font-size` in combination with `inline-block` on buttons.
+    font-size: 0;
+    white-space: nowrap;
+
+    // Negative margin for spacing, position for bringing hovered/focused/actived
+    // element above the siblings.
+    > .molstar-btn {
+      position: relative;
+      + .molstar-btn {
+        margin-left: -1px;
+      }
+      // Bring the "active" button to the front
+      &:hover,
+      &:focus,
+      &:active {
+        z-index: 2;
+      }
+    }
+
+    // Negative margin to only have a 1px border between the two
+    &:first-child {
+      > .molstar-btn,
+      > .molstar-btn-group {
+        margin-right: -1px;
+      }
+    }
+    &:last-child {
+      > .molstar-btn,
+      > .molstar-btn-group {
+        z-index: 2;
+        margin-left: -1px;
+      }
+    }
+  }

+ 66 - 0
src/mol-app/skin/bootstrap/labels.scss

@@ -0,0 +1,66 @@
+//
+// Labels
+// --------------------------------------------------
+
+.label {
+    display: inline;
+    padding: .2em .6em .3em;
+    font-size: 75%;
+    font-weight: bold;
+    line-height: 1;
+    color: $label-color;
+    text-align: center;
+    white-space: nowrap;
+    vertical-align: baseline;
+    border-radius: .25em;
+
+    // [converter] extracted a& to a.label
+
+    // Empty labels collapse automatically (not available in IE8)
+    &:empty {
+      display: none;
+    }
+
+    // Quick fix for labels in buttons
+    .molstar-btn & {
+      position: relative;
+      top: -1px;
+    }
+  }
+
+  // Add hover effects, but only for links
+  a.label {
+    &:hover,
+    &:focus {
+      color: $label-link-hover-color;
+      text-decoration: none;
+      cursor: pointer;
+    }
+  }
+
+  // Colors
+  // Contextual variations (linked labels get darker on :hover)
+
+  .label-default {
+    @include label-variant($label-default-bg);
+  }
+
+  .label-primary {
+    @include label-variant($label-primary-bg);
+  }
+
+  .label-success {
+    @include label-variant($label-success-bg);
+  }
+
+  .label-info {
+    @include label-variant($label-info-bg);
+  }
+
+  .label-warning {
+    @include label-variant($label-warning-bg);
+  }
+
+  .label-danger {
+    @include label-variant($label-danger-bg);
+  }

+ 40 - 0
src/mol-app/skin/bootstrap/mixins.scss

@@ -0,0 +1,40 @@
+// Mixins
+// --------------------------------------------------
+
+// Utilities
+// @import "mixins/hide-text";
+@import "mixins/opacity";
+@import "mixins/image";
+@import "mixins/labels";
+// @import "mixins/reset-filter";
+// @import "mixins/resize";
+// @import "mixins/responsive-visibility";
+// @import "mixins/size";
+@import "mixins/tab-focus";
+// @import "mixins/reset-text";
+@import "mixins/text-emphasis";
+@import "mixins/text-overflow";
+@import "mixins/vendor-prefixes";
+
+// Components
+// @import "mixins/alerts";
+@import "mixins/buttons";
+// @import "mixins/panels";
+// @import "mixins/pagination";
+// @import "mixins/list-group";
+// @import "mixins/nav-divider";
+@import "mixins/forms";
+// @import "mixins/progress-bar";
+// @import "mixins/table-row";
+
+// Skins
+@import "mixins/background-variant";
+@import "mixins/border-radius";
+// @import "mixins/gradients";
+
+// Layout
+@import "mixins/clearfix";
+// @import "mixins/center-block";
+// @import "mixins/nav-vertical-align";
+// @import "mixins/grid-framework";
+@import "mixins/grid";

+ 12 - 0
src/mol-app/skin/bootstrap/mixins/background-variant.scss

@@ -0,0 +1,12 @@
+// Contextual backgrounds
+
+// [converter] $parent hack
+@mixin bg-variant($parent, $color) {
+    #{$parent} {
+        background-color: $color;
+    }
+    a#{$parent}:hover,
+    a#{$parent}:focus {
+        background-color: darken($color, 10%);
+    }
+}

+ 18 - 0
src/mol-app/skin/bootstrap/mixins/border-radius.scss

@@ -0,0 +1,18 @@
+// Single side border-radius
+
+@mixin border-top-radius($radius) {
+    border-top-right-radius: $radius;
+    border-top-left-radius: $radius;
+}
+@mixin border-right-radius($radius) {
+    border-bottom-right-radius: $radius;
+    border-top-right-radius: $radius;
+}
+@mixin border-bottom-radius($radius) {
+    border-bottom-right-radius: $radius;
+    border-bottom-left-radius: $radius;
+}
+@mixin border-left-radius($radius) {
+    border-bottom-left-radius: $radius;
+    border-top-left-radius: $radius;
+}

+ 65 - 0
src/mol-app/skin/bootstrap/mixins/buttons.scss

@@ -0,0 +1,65 @@
+// Button variants
+//
+// Easily pump out default styles, as well as :hover, :focus, :active,
+// and disabled options for all buttons
+
+@mixin button-variant($color, $background, $border) {
+    color: $color;
+    background-color: $background;
+    border-color: $border;
+
+    &:focus,
+    &.focus {
+      color: $color;
+      background-color: darken($background, 10%);
+          border-color: darken($border, 25%);
+    }
+    &:hover {
+      color: $color;
+      background-color: darken($background, 10%);
+          border-color: darken($border, 12%);
+    }
+    &:active,
+    &.active,
+    .open > &.dropdown-toggle {
+      color: $color;
+      background-color: darken($background, 10%);
+          border-color: darken($border, 12%);
+
+      &:hover,
+      &:focus,
+      &.focus {
+        color: $color;
+        background-color: darken($background, 17%);
+            border-color: darken($border, 25%);
+      }
+    }
+    &:active,
+    &.active,
+    .open > &.dropdown-toggle {
+      background-image: none;
+    }
+    &.disabled,
+    &[disabled],
+    fieldset[disabled] & {
+      &:hover,
+      &:focus,
+      &.focus {
+        background-color: $background;
+            border-color: $border;
+      }
+    }
+
+    .badge {
+      color: $background;
+      background-color: $color;
+    }
+  }
+
+  // Button sizes
+  @mixin button-size($padding-vertical, $padding-horizontal, $font-size, $line-height, $border-radius) {
+    padding: $padding-vertical $padding-horizontal;
+    font-size: $font-size;
+    line-height: $line-height;
+    border-radius: $border-radius;
+  }

+ 22 - 0
src/mol-app/skin/bootstrap/mixins/clearfix.scss

@@ -0,0 +1,22 @@
+// Clearfix
+//
+// For modern browsers
+// 1. The space content is one way to avoid an Opera bug when the
+//    contenteditable attribute is included anywhere else in the document.
+//    Otherwise it causes space to appear at the top and bottom of elements
+//    that are clearfixed.
+// 2. The use of `table` rather than `block` is only necessary if using
+//    `:before` to contain the top-margins of child elements.
+//
+// Source: http://nicolasgallagher.com/micro-clearfix-hack/
+
+@mixin clearfix() {
+    &:before,
+    &:after {
+        content: " "; // 1
+        display: table; // 2
+    }
+    &:after {
+        clear: both;
+    }
+}

+ 88 - 0
src/mol-app/skin/bootstrap/mixins/forms.scss

@@ -0,0 +1,88 @@
+// Form validation states
+//
+// Used in forms.less to generate the form validation CSS for warnings, errors,
+// and successes.
+
+@mixin molstar-form-control-validation($text-color: #555, $border-color: #ccc, $background-color: #f5f5f5) {
+    // Color the label and help text
+    .help-block,
+    .control-label,
+    .radio,
+    .checkbox,
+    .radio-inline,
+    .checkbox-inline,
+    &.radio label,
+    &.checkbox label,
+    &.radio-inline label,
+    &.checkbox-inline label  {
+      color: $text-color;
+    }
+    // Set the border and box shadow on specific inputs to match
+    .molstar-form-control {
+      border-color: $border-color;
+      @include box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work
+      &:focus {
+        border-color: darken($border-color, 10%);
+        $shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten($border-color, 20%);
+        @include box-shadow($shadow);
+      }
+    }
+    // Set validation states also for addons
+    .input-group-addon {
+      color: $text-color;
+      border-color: $border-color;
+      background-color: $background-color;
+    }
+    // Optional feedback icon
+    .molstar-form-control-feedback {
+      color: $text-color;
+    }
+  }
+
+
+  // Form control focus state
+  //
+  // Generate a customized focus state and for any input with the specified color,
+  // which defaults to the `$input-border-focus` variable.
+  //
+  // We highly encourage you to not customize the default value, but instead use
+  // this to tweak colors on an as-needed basis. This aesthetic change is based on
+  // WebKit's default styles, but applicable to a wider range of browsers. Its
+  // usability and accessibility should be taken into account with any change.
+  //
+  // Example usage: change the default blue border and shadow to white for better
+  // contrast against a dark gray background.
+  @mixin molstar-form-control-focus($color: $input-border-focus) {
+    $color-rgba: rgba(red($color), green($color), blue($color), .6);
+    &:focus {
+      border-color: $color;
+      outline: 0;
+      @include box-shadow(inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px $color-rgba);
+    }
+  }
+
+  // Form control sizing
+  //
+  // Relative text size, padding, and border-radii changes for form controls. For
+  // horizontal sizing, wrap controls in the predefined grid classes. `<select>`
+  // element gets special love because it's special, and that's a fact!
+  // [converter] $parent hack
+  @mixin input-size($parent, $input-height, $padding-vertical, $padding-horizontal, $font-size, $line-height, $border-radius) {
+    #{$parent} {
+      height: $input-height;
+      padding: $padding-vertical $padding-horizontal;
+      font-size: $font-size;
+      line-height: $line-height;
+      border-radius: $border-radius;
+    }
+
+    select#{$parent} {
+      height: $input-height;
+      line-height: $input-height;
+    }
+
+    textarea#{$parent},
+    select[multiple]#{$parent} {
+      height: auto;
+    }
+  }

+ 122 - 0
src/mol-app/skin/bootstrap/mixins/grid.scss

@@ -0,0 +1,122 @@
+// Grid system
+//
+// Generate semantic grid columns with these mixins.
+
+// Centered container element
+@mixin container-fixed($gutter: $grid-gutter-width) {
+    margin-right: auto;
+    margin-left: auto;
+    padding-left:  floor(($gutter / 2));
+    padding-right: ceil(($gutter / 2));
+    @include clearfix;
+  }
+
+  // Creates a wrapper for a series of columns
+  @mixin make-row($gutter: $grid-gutter-width) {
+    margin-left:  ceil(($gutter / -2));
+    margin-right: floor(($gutter / -2));
+    @include clearfix;
+  }
+
+  // Generate the extra small columns
+  @mixin make-xs-column($columns, $gutter: $grid-gutter-width) {
+    position: relative;
+    float: left;
+    width: percentage(($columns / $grid-columns));
+    min-height: 1px;
+    padding-left:  ($gutter / 2);
+    padding-right: ($gutter / 2);
+  }
+  @mixin make-xs-column-offset($columns) {
+    margin-left: percentage(($columns / $grid-columns));
+  }
+  @mixin make-xs-column-push($columns) {
+    left: percentage(($columns / $grid-columns));
+  }
+  @mixin make-xs-column-pull($columns) {
+    right: percentage(($columns / $grid-columns));
+  }
+
+  // Generate the small columns
+  @mixin make-sm-column($columns, $gutter: $grid-gutter-width) {
+    position: relative;
+    min-height: 1px;
+    padding-left:  ($gutter / 2);
+    padding-right: ($gutter / 2);
+
+    @media (min-width: $screen-sm-min) {
+      float: left;
+      width: percentage(($columns / $grid-columns));
+    }
+  }
+  @mixin make-sm-column-offset($columns) {
+    @media (min-width: $screen-sm-min) {
+      margin-left: percentage(($columns / $grid-columns));
+    }
+  }
+  @mixin make-sm-column-push($columns) {
+    @media (min-width: $screen-sm-min) {
+      left: percentage(($columns / $grid-columns));
+    }
+  }
+  @mixin make-sm-column-pull($columns) {
+    @media (min-width: $screen-sm-min) {
+      right: percentage(($columns / $grid-columns));
+    }
+  }
+
+  // Generate the medium columns
+  @mixin make-md-column($columns, $gutter: $grid-gutter-width) {
+    position: relative;
+    min-height: 1px;
+    padding-left:  ($gutter / 2);
+    padding-right: ($gutter / 2);
+
+    @media (min-width: $screen-md-min) {
+      float: left;
+      width: percentage(($columns / $grid-columns));
+    }
+  }
+  @mixin make-md-column-offset($columns) {
+    @media (min-width: $screen-md-min) {
+      margin-left: percentage(($columns / $grid-columns));
+    }
+  }
+  @mixin make-md-column-push($columns) {
+    @media (min-width: $screen-md-min) {
+      left: percentage(($columns / $grid-columns));
+    }
+  }
+  @mixin make-md-column-pull($columns) {
+    @media (min-width: $screen-md-min) {
+      right: percentage(($columns / $grid-columns));
+    }
+  }
+
+  // Generate the large columns
+  @mixin make-lg-column($columns, $gutter: $grid-gutter-width) {
+    position: relative;
+    min-height: 1px;
+    padding-left:  ($gutter / 2);
+    padding-right: ($gutter / 2);
+
+    @media (min-width: $screen-lg-min) {
+      float: left;
+      width: percentage(($columns / $grid-columns));
+    }
+  }
+  @mixin make-lg-column-offset($columns) {
+    @media (min-width: $screen-lg-min) {
+      margin-left: percentage(($columns / $grid-columns));
+    }
+  }
+  @mixin make-lg-column-push($columns) {
+    @media (min-width: $screen-lg-min) {
+      left: percentage(($columns / $grid-columns));
+    }
+  }
+  @mixin make-lg-column-pull($columns) {
+    @media (min-width: $screen-lg-min) {
+      right: percentage(($columns / $grid-columns));
+    }
+  }

+ 33 - 0
src/mol-app/skin/bootstrap/mixins/image.scss

@@ -0,0 +1,33 @@
+// Image Mixins
+// - Responsive image
+// - Retina image
+
+
+// Responsive image
+//
+// Keep images from scaling beyond the width of their parents.
+@mixin img-responsive($display: block) {
+    display: $display;
+    max-width: 100%; // Part 1: Set a maximum relative to the parent
+    height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching
+}
+
+
+// Retina image
+//
+// Short retina mixin for setting background-image and -size. Note that the
+// spelling of `min--moz-device-pixel-ratio` is intentional.
+@mixin img-retina($file-1x, $file-2x, $width-1x, $height-1x) {
+    background-image: url(if($bootstrap-sass-asset-helper, twbs-image-path("#{$file-1x}"), "#{$file-1x}"));
+
+    @media
+    only screen and (-webkit-min-device-pixel-ratio: 2),
+    only screen and (   min--moz-device-pixel-ratio: 2),
+    only screen and (     -o-min-device-pixel-ratio: 2/1),
+    only screen and (        min-device-pixel-ratio: 2),
+    only screen and (                min-resolution: 192dpi),
+    only screen and (                min-resolution: 2dppx) {
+      background-image: url(if($bootstrap-sass-asset-helper, twbs-image-path("#{$file-2x}"), "#{$file-2x}"));
+      background-size: $width-1x $height-1x;
+    }
+}

+ 12 - 0
src/mol-app/skin/bootstrap/mixins/labels.scss

@@ -0,0 +1,12 @@
+// Labels
+
+@mixin label-variant($color) {
+    background-color: $color;
+
+    &[href] {
+        &:hover,
+        &:focus {
+            background-color: darken($color, 10%);
+        }
+    }
+}

+ 8 - 0
src/mol-app/skin/bootstrap/mixins/opacity.scss

@@ -0,0 +1,8 @@
+// Opacity
+
+@mixin opacity($opacity) {
+    opacity: $opacity;
+    // IE8 filter
+    $opacity-ie: ($opacity * 100);
+    filter: alpha(opacity=$opacity-ie);
+}

+ 9 - 0
src/mol-app/skin/bootstrap/mixins/tab-focus.scss

@@ -0,0 +1,9 @@
+// WebKit-style focus
+
+@mixin tab-focus() {
+    // Default
+    outline: thin dotted;
+    // WebKit
+    outline: 5px auto -webkit-focus-ring-color;
+    outline-offset: -2px;
+}

+ 12 - 0
src/mol-app/skin/bootstrap/mixins/text-emphasis.scss

@@ -0,0 +1,12 @@
+// Typography
+
+// [converter] $parent hack
+@mixin text-emphasis-variant($parent, $color) {
+    #{$parent} {
+      color: $color;
+    }
+    a#{$parent}:hover,
+    a#{$parent}:focus {
+      color: darken($color, 10%);
+    }
+}

+ 8 - 0
src/mol-app/skin/bootstrap/mixins/text-overflow.scss

@@ -0,0 +1,8 @@
+// Text overflow
+// Requires inline-block or block for proper styling
+
+@mixin text-overflow() {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+}

+ 222 - 0
src/mol-app/skin/bootstrap/mixins/vendor-prefixes.scss

@@ -0,0 +1,222 @@
+// Vendor Prefixes
+//
+// All vendor mixins are deprecated as of v3.2.0 due to the introduction of
+// Autoprefixer in our Gruntfile. They have been removed in v4.
+
+// - Animations
+// - Backface visibility
+// - Box shadow
+// - Box sizing
+// - Content columns
+// - Hyphens
+// - Placeholder text
+// - Transformations
+// - Transitions
+// - User Select
+
+
+// Animations
+@mixin animation($animation) {
+    -webkit-animation: $animation;
+         -o-animation: $animation;
+            animation: $animation;
+  }
+  @mixin animation-name($name) {
+    -webkit-animation-name: $name;
+            animation-name: $name;
+  }
+  @mixin animation-duration($duration) {
+    -webkit-animation-duration: $duration;
+            animation-duration: $duration;
+  }
+  @mixin animation-timing-function($timing-function) {
+    -webkit-animation-timing-function: $timing-function;
+            animation-timing-function: $timing-function;
+  }
+  @mixin animation-delay($delay) {
+    -webkit-animation-delay: $delay;
+            animation-delay: $delay;
+  }
+  @mixin animation-iteration-count($iteration-count) {
+    -webkit-animation-iteration-count: $iteration-count;
+            animation-iteration-count: $iteration-count;
+  }
+  @mixin animation-direction($direction) {
+    -webkit-animation-direction: $direction;
+            animation-direction: $direction;
+  }
+  @mixin animation-fill-mode($fill-mode) {
+    -webkit-animation-fill-mode: $fill-mode;
+            animation-fill-mode: $fill-mode;
+  }
+
+  // Backface visibility
+  // Prevent browsers from flickering when using CSS 3D transforms.
+  // Default value is `visible`, but can be changed to `hidden`
+
+  @mixin backface-visibility($visibility) {
+    -webkit-backface-visibility: $visibility;
+       -moz-backface-visibility: $visibility;
+            backface-visibility: $visibility;
+  }
+
+  // Drop shadows
+  //
+  // Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's
+  // supported browsers that have box shadow capabilities now support it.
+
+  @mixin box-shadow($shadow...) {
+    -webkit-box-shadow: $shadow; // iOS <4.3 & Android <4.1
+            box-shadow: $shadow;
+  }
+
+  // Box sizing
+  @mixin box-sizing($boxmodel) {
+    -webkit-box-sizing: $boxmodel;
+       -moz-box-sizing: $boxmodel;
+            box-sizing: $boxmodel;
+  }
+
+  // CSS3 Content Columns
+  @mixin content-columns($column-count, $column-gap: $grid-gutter-width) {
+    -webkit-column-count: $column-count;
+       -moz-column-count: $column-count;
+            column-count: $column-count;
+    -webkit-column-gap: $column-gap;
+       -moz-column-gap: $column-gap;
+            column-gap: $column-gap;
+  }
+
+  // Optional hyphenation
+  @mixin hyphens($mode: auto) {
+    word-wrap: break-word;
+    -webkit-hyphens: $mode;
+       -moz-hyphens: $mode;
+        -ms-hyphens: $mode; // IE10+
+         -o-hyphens: $mode;
+            hyphens: $mode;
+  }
+
+  // Placeholder text
+  @mixin placeholder($color: $input-color-placeholder) {
+    // Firefox
+    &::-moz-placeholder {
+      color: $color;
+      opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526
+    }
+    &:-ms-input-placeholder { color: $color; } // Internet Explorer 10+
+    &::-webkit-input-placeholder  { color: $color; } // Safari and Chrome
+  }
+
+  // Transformations
+  @mixin scale($ratio...) {
+    -webkit-transform: scale($ratio);
+        -ms-transform: scale($ratio); // IE9 only
+         -o-transform: scale($ratio);
+            transform: scale($ratio);
+  }
+
+  @mixin scaleX($ratio) {
+    -webkit-transform: scaleX($ratio);
+        -ms-transform: scaleX($ratio); // IE9 only
+         -o-transform: scaleX($ratio);
+            transform: scaleX($ratio);
+  }
+  @mixin scaleY($ratio) {
+    -webkit-transform: scaleY($ratio);
+        -ms-transform: scaleY($ratio); // IE9 only
+         -o-transform: scaleY($ratio);
+            transform: scaleY($ratio);
+  }
+  @mixin skew($x, $y) {
+    -webkit-transform: skewX($x) skewY($y);
+        -ms-transform: skewX($x) skewY($y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+
+         -o-transform: skewX($x) skewY($y);
+            transform: skewX($x) skewY($y);
+  }
+  @mixin translate($x, $y) {
+    -webkit-transform: translate($x, $y);
+        -ms-transform: translate($x, $y); // IE9 only
+         -o-transform: translate($x, $y);
+            transform: translate($x, $y);
+  }
+  @mixin translate3d($x, $y, $z) {
+    -webkit-transform: translate3d($x, $y, $z);
+            transform: translate3d($x, $y, $z);
+  }
+  @mixin rotate($degrees) {
+    -webkit-transform: rotate($degrees);
+        -ms-transform: rotate($degrees); // IE9 only
+         -o-transform: rotate($degrees);
+            transform: rotate($degrees);
+  }
+  @mixin rotateX($degrees) {
+    -webkit-transform: rotateX($degrees);
+        -ms-transform: rotateX($degrees); // IE9 only
+         -o-transform: rotateX($degrees);
+            transform: rotateX($degrees);
+  }
+  @mixin rotateY($degrees) {
+    -webkit-transform: rotateY($degrees);
+        -ms-transform: rotateY($degrees); // IE9 only
+         -o-transform: rotateY($degrees);
+            transform: rotateY($degrees);
+  }
+  @mixin perspective($perspective) {
+    -webkit-perspective: $perspective;
+       -moz-perspective: $perspective;
+            perspective: $perspective;
+  }
+  @mixin perspective-origin($perspective) {
+    -webkit-perspective-origin: $perspective;
+       -moz-perspective-origin: $perspective;
+            perspective-origin: $perspective;
+  }
+  @mixin transform-origin($origin) {
+    -webkit-transform-origin: $origin;
+       -moz-transform-origin: $origin;
+        -ms-transform-origin: $origin; // IE9 only
+            transform-origin: $origin;
+  }
+
+
+  // Transitions
+
+  @mixin transition($transition...) {
+    -webkit-transition: $transition;
+         -o-transition: $transition;
+            transition: $transition;
+  }
+  @mixin transition-property($transition-property...) {
+    -webkit-transition-property: $transition-property;
+            transition-property: $transition-property;
+  }
+  @mixin transition-delay($transition-delay) {
+    -webkit-transition-delay: $transition-delay;
+            transition-delay: $transition-delay;
+  }
+  @mixin transition-duration($transition-duration...) {
+    -webkit-transition-duration: $transition-duration;
+            transition-duration: $transition-duration;
+  }
+  @mixin transition-timing-function($timing-function) {
+    -webkit-transition-timing-function: $timing-function;
+            transition-timing-function: $timing-function;
+  }
+  @mixin transition-transform($transition...) {
+    -webkit-transition: -webkit-transform $transition;
+       -moz-transition: -moz-transform $transition;
+         -o-transition: -o-transform $transition;
+            transition: transform $transition;
+  }
+
+
+  // User select
+  // For selecting text on the page
+
+  @mixin user-select($select) {
+    -webkit-user-select: $select;
+       -moz-user-select: $select;
+        -ms-user-select: $select; // IE10+
+            user-select: $select;
+  }

+ 424 - 0
src/mol-app/skin/bootstrap/normalize.scss

@@ -0,0 +1,424 @@
+/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
+
+//
+// 1. Set default font family to sans-serif.
+// 2. Prevent iOS and IE text size adjust after device orientation change,
+//    without disabling user zoom.
+//
+
+html {
+    font-family: sans-serif; // 1
+    -ms-text-size-adjust: 100%; // 2
+    -webkit-text-size-adjust: 100%; // 2
+  }
+
+  //
+  // Remove default margin.
+  //
+
+  body {
+    margin: 0;
+  }
+
+  // HTML5 display definitions
+  // ==========================================================================
+
+  //
+  // Correct `block` display not defined for any HTML5 element in IE 8/9.
+  // Correct `block` display not defined for `details` or `summary` in IE 10/11
+  // and Firefox.
+  // Correct `block` display not defined for `main` in IE 11.
+  //
+
+  article,
+  aside,
+  details,
+  figcaption,
+  figure,
+  footer,
+  header,
+  hgroup,
+  main,
+  menu,
+  nav,
+  section,
+  summary {
+    display: block;
+  }
+
+  //
+  // 1. Correct `inline-block` display not defined in IE 8/9.
+  // 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
+  //
+
+  audio,
+  canvas,
+  progress,
+  video {
+    display: inline-block; // 1
+    vertical-align: baseline; // 2
+  }
+
+  //
+  // Prevent modern browsers from displaying `audio` without controls.
+  // Remove excess height in iOS 5 devices.
+  //
+
+  audio:not([controls]) {
+    display: none;
+    height: 0;
+  }
+
+  //
+  // Address `[hidden]` styling not present in IE 8/9/10.
+  // Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22.
+  //
+
+  [hidden],
+  template {
+    display: none;
+  }
+
+  // Links
+  // ==========================================================================
+
+  //
+  // Remove the gray background color from active links in IE 10.
+  //
+
+  a {
+    background-color: transparent;
+  }
+
+  //
+  // Improve readability of focused elements when they are also in an
+  // active/hover state.
+  //
+
+  a:active,
+  a:hover {
+    outline: 0;
+  }
+
+  // Text-level semantics
+  // ==========================================================================
+
+  //
+  // Address styling not present in IE 8/9/10/11, Safari, and Chrome.
+  //
+
+  abbr[title] {
+    border-bottom: 1px dotted;
+  }
+
+  //
+  // Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
+  //
+
+  b,
+  strong {
+    font-weight: bold;
+  }
+
+  //
+  // Address styling not present in Safari and Chrome.
+  //
+
+  dfn {
+    font-style: italic;
+  }
+
+  //
+  // Address variable `h1` font-size and margin within `section` and `article`
+  // contexts in Firefox 4+, Safari, and Chrome.
+  //
+
+  h1 {
+    font-size: 2em;
+    margin: 0.67em 0;
+  }
+
+  //
+  // Address styling not present in IE 8/9.
+  //
+
+  mark {
+    background: #ff0;
+    color: #000;
+  }
+
+  //
+  // Address inconsistent and variable font size in all browsers.
+  //
+
+  small {
+    font-size: 80%;
+  }
+
+  //
+  // Prevent `sub` and `sup` affecting `line-height` in all browsers.
+  //
+
+  sub,
+  sup {
+    font-size: 75%;
+    line-height: 0;
+    position: relative;
+    vertical-align: baseline;
+  }
+
+  sup {
+    top: -0.5em;
+  }
+
+  sub {
+    bottom: -0.25em;
+  }
+
+  // Embedded content
+  // ==========================================================================
+
+  //
+  // Remove border when inside `a` element in IE 8/9/10.
+  //
+
+  img {
+    border: 0;
+  }
+
+  //
+  // Correct overflow not hidden in IE 9/10/11.
+  //
+
+  svg:not(:root) {
+    overflow: hidden;
+  }
+
+  // Grouping content
+  // ==========================================================================
+
+  //
+  // Address margin not present in IE 8/9 and Safari.
+  //
+
+  figure {
+    margin: 1em 40px;
+  }
+
+  //
+  // Address differences between Firefox and other browsers.
+  //
+
+  hr {
+    box-sizing: content-box;
+    height: 0;
+  }
+
+  //
+  // Contain overflow in all browsers.
+  //
+
+  pre {
+    overflow: auto;
+  }
+
+  //
+  // Address odd `em`-unit font size rendering in all browsers.
+  //
+
+  code,
+  kbd,
+  pre,
+  samp {
+    font-family: monospace, monospace;
+    font-size: 1em;
+  }
+
+  // Forms
+  // ==========================================================================
+
+  //
+  // Known limitation: by default, Chrome and Safari on OS X allow very limited
+  // styling of `select`, unless a `border` property is set.
+  //
+
+  //
+  // 1. Correct color not being inherited.
+  //    Known issue: affects color of disabled elements.
+  // 2. Correct font properties not being inherited.
+  // 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
+  //
+
+  button,
+  input,
+  optgroup,
+  select,
+  textarea {
+    color: inherit; // 1
+    font: inherit; // 2
+    margin: 0; // 3
+  }
+
+  //
+  // Address `overflow` set to `hidden` in IE 8/9/10/11.
+  //
+
+  button {
+    overflow: visible;
+  }
+
+  //
+  // Address inconsistent `text-transform` inheritance for `button` and `select`.
+  // All other form control elements do not inherit `text-transform` values.
+  // Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
+  // Correct `select` style inheritance in Firefox.
+  //
+
+  button,
+  select {
+    text-transform: none;
+  }
+
+  //
+  // 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
+  //    and `video` controls.
+  // 2. Correct inability to style clickable `input` types in iOS.
+  // 3. Improve usability and consistency of cursor style between image-type
+  //    `input` and others.
+  //
+
+  button,
+  html input[type="button"], // 1
+  input[type="reset"],
+  input[type="submit"] {
+    -webkit-appearance: button; // 2
+    cursor: pointer; // 3
+  }
+
+  //
+  // Re-set default cursor for disabled elements.
+  //
+
+  button[disabled],
+  html input[disabled] {
+    cursor: default;
+  }
+
+  //
+  // Remove inner padding and border in Firefox 4+.
+  //
+
+  button::-moz-focus-inner,
+  input::-moz-focus-inner {
+    border: 0;
+    padding: 0;
+  }
+
+  //
+  // Address Firefox 4+ setting `line-height` on `input` using `!important` in
+  // the UA stylesheet.
+  //
+
+  input {
+    line-height: normal;
+  }
+
+  //
+  // It's recommended that you don't attempt to style these elements.
+  // Firefox's implementation doesn't respect box-sizing, padding, or width.
+  //
+  // 1. Address box sizing set to `content-box` in IE 8/9/10.
+  // 2. Remove excess padding in IE 8/9/10.
+  //
+
+  input[type="checkbox"],
+  input[type="radio"] {
+    box-sizing: border-box; // 1
+    padding: 0; // 2
+  }
+
+  //
+  // Fix the cursor style for Chrome's increment/decrement buttons. For certain
+  // `font-size` values of the `input`, it causes the cursor style of the
+  // decrement button to change from `default` to `text`.
+  //
+
+  input[type="number"]::-webkit-inner-spin-button,
+  input[type="number"]::-webkit-outer-spin-button {
+    height: auto;
+  }
+
+  //
+  // 1. Address `appearance` set to `searchfield` in Safari and Chrome.
+  // 2. Address `box-sizing` set to `border-box` in Safari and Chrome.
+  //
+
+  input[type="search"] {
+    -webkit-appearance: textfield; // 1
+    box-sizing: content-box; //2
+  }
+
+  //
+  // Remove inner padding and search cancel button in Safari and Chrome on OS X.
+  // Safari (but not Chrome) clips the cancel button when the search input has
+  // padding (and `textfield` appearance).
+  //
+
+  input[type="search"]::-webkit-search-cancel-button,
+  input[type="search"]::-webkit-search-decoration {
+    -webkit-appearance: none;
+  }
+
+  //
+  // Define consistent border, margin, and padding.
+  //
+
+  fieldset {
+    border: 1px solid #c0c0c0;
+    margin: 0 2px;
+    padding: 0.35em 0.625em 0.75em;
+  }
+
+  //
+  // 1. Correct `color` not being inherited in IE 8/9/10/11.
+  // 2. Remove padding so people aren't caught out if they zero out fieldsets.
+  //
+
+  legend {
+    border: 0; // 1
+    padding: 0; // 2
+  }
+
+  //
+  // Remove default vertical scrollbar in IE 8/9/10/11.
+  //
+
+  textarea {
+    overflow: auto;
+  }
+
+  //
+  // Don't inherit the `font-weight` (applied by a rule above).
+  // NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
+  //
+
+  optgroup {
+    font-weight: bold;
+  }
+
+  // Tables
+  // ==========================================================================
+
+  //
+  // Remove most spacing between table cells.
+  //
+
+  table {
+    border-collapse: collapse;
+    border-spacing: 0;
+  }
+
+  td,
+  th {
+    padding: 0;
+  }

+ 161 - 0
src/mol-app/skin/bootstrap/scaffolding.scss

@@ -0,0 +1,161 @@
+//
+// Scaffolding
+// --------------------------------------------------
+
+
+// Reset the box-sizing
+//
+// Heads up! This reset may cause conflicts with some third-party widgets.
+// For recommendations on resolving such conflicts, see
+// http://getbootstrap.com/getting-started/#third-box-sizing
+* {
+    @include box-sizing(border-box);
+  }
+  *:before,
+  *:after {
+    @include box-sizing(border-box);
+  }
+
+
+  // Body reset
+
+  html {
+    font-size: 10px;
+    -webkit-tap-highlight-color: rgba(0,0,0,0);
+  }
+
+  body {
+    font-family: $font-family-base;
+    font-size: $font-size-base;
+    line-height: $line-height-base;
+    color: $text-color;
+    background-color: $body-bg;
+  }
+
+  // Reset fonts for relevant elements
+  input,
+  button,
+  select,
+  textarea {
+    font-family: inherit;
+    font-size: inherit;
+    line-height: inherit;
+  }
+
+
+  // Links
+
+  a {
+    color: $link-color;
+    text-decoration: none;
+
+    &:hover,
+    &:focus {
+      color: $link-hover-color;
+      text-decoration: $link-hover-decoration;
+    }
+
+    &:focus {
+      @include tab-focus;
+    }
+  }
+
+
+  // Figures
+  //
+  // We reset this here because previously Normalize had no `figure` margins. This
+  // ensures we don't break anyone's use of the element.
+
+  figure {
+    margin: 0;
+  }
+
+
+  // Images
+
+  img {
+    vertical-align: middle;
+  }
+
+  // Responsive images (ensure images don't scale beyond their parents)
+  .img-responsive {
+    @include img-responsive;
+  }
+
+  // Rounded corners
+  .img-rounded {
+    border-radius: $border-radius-large;
+  }
+
+  // Image thumbnails
+  //
+  // Heads up! This is mixin-ed into thumbnails.less for `.thumbnail`.
+  .img-thumbnail {
+    padding: $thumbnail-padding;
+    line-height: $line-height-base;
+    background-color: $thumbnail-bg;
+    border: 1px solid $thumbnail-border;
+    border-radius: $thumbnail-border-radius;
+    @include transition(all .2s ease-in-out);
+
+    // Keep them at most 100% wide
+    @include img-responsive(inline-block);
+  }
+
+  // Perfect circle
+  .img-circle {
+    border-radius: 50%; // set radius in percents
+  }
+
+
+  // Horizontal rules
+
+  hr {
+    margin-top:    $line-height-computed;
+    margin-bottom: $line-height-computed;
+    border: 0;
+    border-top: 1px solid $hr-border;
+  }
+
+
+  // Only display content to screen readers
+  //
+  // See: http://a11yproject.com/posts/how-to-hide-content/
+
+  .sr-only {
+    position: absolute;
+    width: 1px;
+    height: 1px;
+    margin: -1px;
+    padding: 0;
+    overflow: hidden;
+    clip: rect(0,0,0,0);
+    border: 0;
+  }
+
+  // Use in conjunction with .sr-only to only display content when it's focused.
+  // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1
+  // Credit: HTML5 Boilerplate
+
+  .sr-only-focusable {
+    &:active,
+    &:focus {
+      position: static;
+      width: auto;
+      height: auto;
+      margin: 0;
+      overflow: visible;
+      clip: auto;
+    }
+  }
+
+
+  // iOS "clickable elements" fix for role="button"
+  //
+  // Fixes "clickability" issue (and more generally, the firing of events such as focus as well)
+  // for traditionally non-focusable elements with role="button"
+  // see https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile
+
+  [role="button"] {
+    cursor: pointer;
+  }

+ 298 - 0
src/mol-app/skin/bootstrap/type.scss

@@ -0,0 +1,298 @@
+//
+// Typography
+// --------------------------------------------------
+
+
+// Headings
+// -------------------------
+
+h1, h2, h3, h4, h5, h6,
+.h1, .h2, .h3, .h4, .h5, .h6 {
+  font-family: $headings-font-family;
+  font-weight: $headings-font-weight;
+  line-height: $headings-line-height;
+  color: $headings-color;
+
+  small,
+  .small {
+    font-weight: normal;
+    line-height: 1;
+    color: $headings-small-color;
+  }
+}
+
+h1, .h1,
+h2, .h2,
+h3, .h3 {
+  margin-top: $line-height-computed;
+  margin-bottom: ($line-height-computed / 2);
+
+  small,
+  .small {
+    font-size: 65%;
+  }
+}
+h4, .h4,
+h5, .h5,
+h6, .h6 {
+  margin-top: ($line-height-computed / 2);
+  margin-bottom: ($line-height-computed / 2);
+
+  small,
+  .small {
+    font-size: 75%;
+  }
+}
+
+h1, .h1 { font-size: $font-size-h1; }
+h2, .h2 { font-size: $font-size-h2; }
+h3, .h3 { font-size: $font-size-h3; }
+h4, .h4 { font-size: $font-size-h4; }
+h5, .h5 { font-size: $font-size-h5; }
+h6, .h6 { font-size: $font-size-h6; }
+
+
+// Body text
+// -------------------------
+
+p {
+  margin: 0 0 ($line-height-computed / 2);
+}
+
+.lead {
+  margin-bottom: $line-height-computed;
+  font-size: floor(($font-size-base * 1.15));
+  font-weight: 300;
+  line-height: 1.4;
+
+  @media (min-width: $screen-sm-min) {
+    font-size: ($font-size-base * 1.5);
+  }
+}
+
+
+// Emphasis & misc
+// -------------------------
+
+// Ex: (12px small font / 14px base font) * 100% = about 85%
+small,
+.small {
+  font-size: floor((100% * $font-size-small / $font-size-base));
+}
+
+mark,
+.mark {
+  background-color: $state-warning-bg;
+  padding: .2em;
+}
+
+// Alignment
+.text-left           { text-align: left; }
+.text-right          { text-align: right; }
+.text-center         { text-align: center; }
+.text-justify        { text-align: justify; }
+.text-nowrap         { white-space: nowrap; }
+
+// Transformation
+.text-lowercase      { text-transform: lowercase; }
+.text-uppercase      { text-transform: uppercase; }
+.text-capitalize     { text-transform: capitalize; }
+
+// Contextual colors
+.text-muted {
+  color: $text-muted;
+}
+
+@include text-emphasis-variant('.text-primary', $brand-primary);
+
+@include text-emphasis-variant('.text-success', $state-success-text);
+
+@include text-emphasis-variant('.text-info', $state-info-text);
+
+@include text-emphasis-variant('.text-warning', $state-warning-text);
+
+@include text-emphasis-variant('.text-danger', $state-danger-text);
+
+// Contextual backgrounds
+// For now we'll leave these alongside the text classes until v4 when we can
+// safely shift things around (per SemVer rules).
+.bg-primary {
+  // Given the contrast here, this is the only class to have its color inverted
+  // automatically.
+  color: #fff;
+}
+@include bg-variant('.bg-primary', $brand-primary);
+
+@include bg-variant('.bg-success', $state-success-bg);
+
+@include bg-variant('.bg-info', $state-info-bg);
+
+@include bg-variant('.bg-warning', $state-warning-bg);
+
+@include bg-variant('.bg-danger', $state-danger-bg);
+
+
+// Page header
+// -------------------------
+
+.page-header {
+  padding-bottom: (($line-height-computed / 2) - 1);
+  margin: ($line-height-computed * 2) 0 $line-height-computed;
+  border-bottom: 1px solid $page-header-border-color;
+}
+
+
+// Lists
+// -------------------------
+
+// Unordered and Ordered lists
+ul,
+ol {
+  margin-top: 0;
+  margin-bottom: ($line-height-computed / 2);
+  ul,
+  ol {
+    margin-bottom: 0;
+  }
+}
+
+// List options
+
+// [converter] extracted from `.lm-list-unstyled` for libsass compatibility
+@mixin lm-list-unstyled {
+  padding-left: 0;
+  list-style: none;
+}
+// [converter] extracted as `@mixin lm-list-unstyled` for libsass compatibility
+.lm-list-unstyled {
+  @include lm-list-unstyled;
+}
+
+
+// Inline turns list items into inline-block
+.list-inline {
+  @include lm-list-unstyled;
+  margin-left: -5px;
+
+  > li {
+    display: inline-block;
+    padding-left: 5px;
+    padding-right: 5px;
+  }
+}
+
+// Description Lists
+dl {
+  margin-top: 0; // Remove browser default
+  margin-bottom: $line-height-computed;
+}
+dt,
+dd {
+  line-height: $line-height-base;
+}
+dt {
+  font-weight: bold;
+}
+dd {
+  margin-left: 0; // Undo browser default
+}
+
+// Horizontal description lists
+//
+// Defaults to being stacked without any of the below styles applied, until the
+// grid breakpoint is reached (default of ~768px).
+
+.dl-horizontal {
+  dd {
+    @include clearfix; // Clear the floated `dt` if an empty `dd` is present
+  }
+
+  @media (min-width: $dl-horizontal-breakpoint) {
+    dt {
+      float: left;
+      width: ($dl-horizontal-offset - 20);
+      clear: left;
+      text-align: right;
+      @include text-overflow;
+    }
+    dd {
+      margin-left: $dl-horizontal-offset;
+    }
+  }
+}
+
+
+// Misc
+// -------------------------
+
+// Abbreviations and acronyms
+abbr[title],
+// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257
+abbr[data-original-title] {
+  cursor: help;
+  border-bottom: 1px dotted $abbr-border-color;
+}
+.initialism {
+  font-size: 90%;
+  @extend .text-uppercase;
+}
+
+// Blockquotes
+blockquote {
+  padding: ($line-height-computed / 2) $line-height-computed;
+  margin: 0 0 $line-height-computed;
+  font-size: $blockquote-font-size;
+  border-left: 5px solid $blockquote-border-color;
+
+  p,
+  ul,
+  ol {
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  // Note: Deprecated small and .small as of v3.1.0
+  // Context: https://github.com/twbs/bootstrap/issues/11660
+  footer,
+  small,
+  .small {
+    display: block;
+    font-size: 80%; // back to default font-size
+    line-height: $line-height-base;
+    color: $blockquote-small-color;
+
+    &:before {
+      content: '\2014 \00A0'; // em dash, nbsp
+    }
+  }
+}
+
+// Opposite alignment of blockquote
+//
+// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0.
+.blockquote-reverse,
+blockquote.pull-right {
+  padding-right: 15px;
+  padding-left: 0;
+  border-right: 5px solid $blockquote-border-color;
+  border-left: 0;
+  text-align: right;
+
+  // Account for citation
+  footer,
+  small,
+  .small {
+    &:before { content: ''; }
+    &:after {
+      content: '\00A0 \2014'; // nbsp, em dash
+    }
+  }
+}
+
+// Addresses
+address {
+  margin-bottom: $line-height-computed;
+  font-style: normal;
+  line-height: $line-height-base;
+}

+ 353 - 0
src/mol-app/skin/bootstrap/variables.scss

@@ -0,0 +1,353 @@
+//== Colors
+//
+//## Gray and brand colors for use across Bootstrap.
+
+$gray-base:              #000 !default;
+$gray-darker:            lighten($gray-base, 13.5%) !default; // #222
+$gray-dark:              lighten($gray-base, 20%) !default;   // #333
+$gray:                   lighten($gray-base, 33.5%) !default; // #555
+$gray-light:             lighten($gray-base, 46.7%) !default; // #777
+$gray-lighter:           lighten($gray-base, 93.5%) !default; // #eee
+
+$brand-primary:         darken(#428bca, 6.5%) !default; // #337ab7
+$brand-success:         #5cb85c !default;
+$brand-info:            #5bc0de !default;
+$brand-warning:         #f0ad4e !default;
+$brand-danger:          #d9534f !default;
+
+
+//== Scaffolding
+//
+//## Settings for some of the most global styles.
+
+//** Background color for `<body>`.
+$body-bg:               #fff !default;
+//** Global text color on `<body>`.
+$text-color:            $gray-dark !default;
+
+//** Global textual link color.
+$link-color:            $brand-primary !default;
+//** Link hover color set via `darken()` function.
+$link-hover-color:      darken($link-color, 15%) !default;
+//** Link hover decoration.
+$link-hover-decoration: underline !default;
+
+
+//== Typography
+//
+//## Font, line-height, and color for body text, headings, and more.
+
+$font-family-sans-serif:  "Helvetica Neue", Helvetica, Arial, sans-serif !default;
+$font-family-serif:       Georgia, "Times New Roman", Times, serif !default;
+//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
+$font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace !default;
+$font-family-base:        $font-family-sans-serif !default;
+
+$font-size-base:          14px !default;
+$font-size-large:         ceil(($font-size-base * 1.25)) !default; // ~18px
+$font-size-small:         ceil(($font-size-base * 0.85)) !default; // ~12px
+
+$font-size-h1:            floor(($font-size-base * 2.6)) !default; // ~36px
+$font-size-h2:            floor(($font-size-base * 2.15)) !default; // ~30px
+$font-size-h3:            ceil(($font-size-base * 1.7)) !default; // ~24px
+$font-size-h4:            ceil(($font-size-base * 1.25)) !default; // ~18px
+$font-size-h5:            $font-size-base !default;
+$font-size-h6:            ceil(($font-size-base * 0.85)) !default; // ~12px
+
+//** Unit-less `line-height` for use in components like buttons.
+$line-height-base:        1.428571429 !default; // 20/14
+//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
+$line-height-computed:    floor(($font-size-base * $line-height-base)) !default; // ~20px
+
+//** By default, this inherits from the `<body>`.
+$headings-font-family:    inherit !default;
+$headings-font-weight:    500 !default;
+$headings-line-height:    1.1 !default;
+$headings-color:          inherit !default;
+
+
+//== Components
+//
+//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
+
+$padding-base-vertical:     6px !default;
+$padding-base-horizontal:   12px !default;
+
+$padding-large-vertical:    10px !default;
+$padding-large-horizontal:  16px !default;
+
+$padding-small-vertical:    5px !default;
+$padding-small-horizontal:  10px !default;
+
+$padding-xs-vertical:       1px !default;
+$padding-xs-horizontal:     5px !default;
+
+$line-height-large:         1.3333333 !default; // extra decimals for Win 8.1 Chrome
+$line-height-small:         1.5 !default;
+
+$border-radius-base:        4px !default;
+$border-radius-large:       6px !default;
+$border-radius-small:       3px !default;
+
+//** Global color for active items (e.g., navs or dropdowns).
+$component-active-color:    #fff !default;
+//** Global background color for active items (e.g., navs or dropdowns).
+$component-active-bg:       $brand-primary !default;
+
+//** Width of the `border` for generating carets that indicator dropdowns.
+$caret-width-base:          4px !default;
+//** Carets increase slightly in size for larger components.
+$caret-width-large:         5px !default;
+
+
+//== Buttons
+//
+//## For each of Bootstrap's buttons, define text, background and border color.
+
+$molstar-btn-font-weight:                normal !default;
+
+$molstar-btn-default-color:              #333 !default;
+$molstar-btn-default-bg:                 #fff !default;
+$molstar-btn-default-border:             #ccc !default;
+
+$molstar-btn-primary-color:              #fff !default;
+$molstar-btn-primary-bg:                 $brand-primary !default;
+$molstar-btn-primary-border:             darken($molstar-btn-primary-bg, 5%) !default;
+
+$molstar-btn-success-color:              #fff !default;
+$molstar-btn-success-bg:                 $brand-success !default;
+$molstar-btn-success-border:             darken($molstar-btn-success-bg, 5%) !default;
+
+$molstar-btn-info-color:                 #fff !default;
+$molstar-btn-info-bg:                    $brand-info !default;
+$molstar-btn-info-border:                darken($molstar-btn-info-bg, 5%) !default;
+
+$molstar-btn-warning-color:              #fff !default;
+$molstar-btn-warning-bg:                 $brand-warning !default;
+$molstar-btn-warning-border:             darken($molstar-btn-warning-bg, 5%) !default;
+
+$molstar-btn-danger-color:               #fff !default;
+$molstar-btn-danger-bg:                  $brand-danger !default;
+$molstar-btn-danger-border:              darken($molstar-btn-danger-bg, 5%) !default;
+
+$molstar-btn-link-disabled-color:        $gray-light !default;
+
+// Allows for customizing button radius independently from global border radius
+$molstar-btn-border-radius-base:         $border-radius-base !default;
+$molstar-btn-border-radius-large:        $border-radius-large !default;
+$molstar-btn-border-radius-small:        $border-radius-small !default;
+
+
+//== Media queries breakpoints
+//
+//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
+
+// Extra small screen / phone
+//** Deprecated `$screen-xs` as of v3.0.1
+$screen-xs:                  480px !default;
+//** Deprecated `$screen-xs-min` as of v3.2.0
+$screen-xs-min:              $screen-xs !default;
+//** Deprecated `$screen-phone` as of v3.0.1
+$screen-phone:               $screen-xs-min !default;
+
+// Small screen / tablet
+//** Deprecated `$screen-sm` as of v3.0.1
+$screen-sm:                  768px !default;
+$screen-sm-min:              $screen-sm !default;
+//** Deprecated `$screen-tablet` as of v3.0.1
+$screen-tablet:              $screen-sm-min !default;
+
+// Medium screen / desktop
+//** Deprecated `$screen-md` as of v3.0.1
+$screen-md:                  992px !default;
+$screen-md-min:              $screen-md !default;
+//** Deprecated `$screen-desktop` as of v3.0.1
+$screen-desktop:             $screen-md-min !default;
+
+// Large screen / wide desktop
+//** Deprecated `$screen-lg` as of v3.0.1
+$screen-lg:                  1200px !default;
+$screen-lg-min:              $screen-lg !default;
+//** Deprecated `$screen-lg-desktop` as of v3.0.1
+$screen-lg-desktop:          $screen-lg-min !default;
+
+// So media queries don't overlap when required, provide a maximum
+$screen-xs-max:              ($screen-sm-min - 1) !default;
+$screen-sm-max:              ($screen-md-min - 1) !default;
+$screen-md-max:              ($screen-lg-min - 1) !default;
+
+
+//== Grid system
+//
+//## Define your custom responsive grid.
+
+//** Number of columns in the grid.
+$grid-columns:              12 !default;
+//** Padding between columns. Gets divided in half for the left and right.
+$grid-gutter-width:         30px !default;
+// Navbar collapse
+//** Point at which the navbar becomes uncollapsed.
+$grid-float-breakpoint:     $screen-sm-min !default;
+//** Point at which the navbar begins collapsing.
+$grid-float-breakpoint-max: ($grid-float-breakpoint - 1) !default;
+
+
+//== Forms
+//
+//##
+
+//** `<input>` background color
+$input-bg:                       #fff !default;
+//** `<input disabled>` background color
+$input-bg-disabled:              $gray-lighter !default;
+
+//** Text color for `<input>`s
+$input-color:                    $gray !default;
+//** `<input>` border color
+$input-border:                   #ccc !default;
+
+// TODO: Rename `$input-border-radius` to `$input-border-radius-base` in v4
+//** Default `.lm-form-control` border radius
+// This has no effect on `<select>`s in some browsers, due to the limited stylability of `<select>`s in CSS.
+$input-border-radius:            $border-radius-base !default;
+//** Large `.lm-form-control` border radius
+$input-border-radius-large:      $border-radius-large !default;
+//** Small `.lm-form-control` border radius
+$input-border-radius-small:      $border-radius-small !default;
+
+//** Border color for inputs on focus
+$input-border-focus:             #66afe9 !default;
+
+//** Placeholder text color
+$input-color-placeholder:        #999 !default;
+
+//** Default `.lm-form-control` height
+$input-height-base:              ($line-height-computed + ($padding-base-vertical * 2) + 2) !default;
+//** Large `.lm-form-control` height
+$input-height-large:             (ceil($font-size-large * $line-height-large) + ($padding-large-vertical * 2) + 2) !default;
+//** Small `.lm-form-control` height
+$input-height-small:             (floor($font-size-small * $line-height-small) + ($padding-small-vertical * 2) + 2) !default;
+
+//** `.form-group` margin
+$form-group-margin-bottom:       15px !default;
+
+$legend-color:                   $gray-dark !default;
+$legend-border-color:            #e5e5e5 !default;
+
+//** Background color for textual input addons
+$input-group-addon-bg:           $gray-lighter !default;
+//** Border color for textual input addons
+$input-group-addon-border-color: $input-border !default;
+
+//** Disabled cursor for form controls and buttons.
+$cursor-disabled:                not-allowed !default;
+
+
+//== Thumbnails
+//
+//##
+
+//** Padding around the thumbnail image
+$thumbnail-padding:           4px !default;
+//** Thumbnail background color
+$thumbnail-bg:                $body-bg !default;
+//** Thumbnail border color
+$thumbnail-border:            #ddd !default;
+//** Thumbnail border radius
+$thumbnail-border-radius:     $border-radius-base !default;
+
+//** Custom text color for thumbnail captions
+$thumbnail-caption-color:     $text-color !default;
+//** Padding around the thumbnail caption
+$thumbnail-caption-padding:   9px !default;
+
+
+//== Type
+//
+//##
+
+//** Horizontal offset for forms and lists.
+$component-offset-horizontal: 180px !default;
+//** Text muted color
+$text-muted:                  $gray-light !default;
+//** Abbreviations and acronyms border color
+$abbr-border-color:           $gray-light !default;
+//** Headings small color
+$headings-small-color:        $gray-light !default;
+//** Blockquote small color
+$blockquote-small-color:      $gray-light !default;
+//** Blockquote font size
+$blockquote-font-size:        ($font-size-base * 1.25) !default;
+//** Blockquote border color
+$blockquote-border-color:     $gray-lighter !default;
+//** Page header border color
+$page-header-border-color:    $gray-lighter !default;
+//** Width of horizontal description list titles
+$dl-horizontal-offset:        $component-offset-horizontal !default;
+//** Point at which .dl-horizontal becomes horizontal
+$dl-horizontal-breakpoint:    $grid-float-breakpoint !default;
+//** Horizontal line color.
+$hr-border:                   $gray-lighter !default;
+
+
+//== Form states and alerts
+//
+//## Define colors for form feedback states and, by default, alerts.
+
+$state-success-text:             #3c763d !default;
+$state-success-bg:               #dff0d8 !default;
+$state-success-border:           darken(adjust-hue($state-success-bg, -10), 5%) !default;
+
+$state-info-text:                #31708f !default;
+$state-info-bg:                  #d9edf7 !default;
+$state-info-border:              darken(adjust-hue($state-info-bg, -10), 7%) !default;
+
+$state-warning-text:             #8a6d3b !default;
+$state-warning-bg:               #fcf8e3 !default;
+$state-warning-border:           darken(adjust-hue($state-warning-bg, -10), 5%) !default;
+
+$state-danger-text:              #a94442 !default;
+$state-danger-bg:                #f2dede !default;
+$state-danger-border:            darken(adjust-hue($state-danger-bg, -10), 5%) !default;
+
+
+//== Labels
+//
+//##
+
+//** Default label background color
+$label-default-bg:            $gray-light !default;
+//** Primary label background color
+$label-primary-bg:            $brand-primary !default;
+//** Success label background color
+$label-success-bg:            $brand-success !default;
+//** Info label background color
+$label-info-bg:               $brand-info !default;
+//** Warning label background color
+$label-warning-bg:            $brand-warning !default;
+//** Danger label background color
+$label-danger-bg:             $brand-danger !default;
+
+//** Default label text color
+$label-color:                 #fff !default;
+//** Default text color of a linked label
+$label-link-hover-color:      #fff !default;
+
+
+//== Badges
+//
+//##
+
+$badge-color:                 #fff !default;
+//** Linked badge text color on hover
+$badge-link-hover-color:      #fff !default;
+$badge-bg:                    $gray-light !default;
+
+//** Badge text color in active nav link
+$badge-active-color:          $link-color !default;
+//** Badge background color in active nav link
+$badge-active-bg:             #fff !default;
+
+$badge-font-weight:           bold !default;
+$badge-line-height:           1 !default;
+$badge-border-radius:         10px !default;

+ 24 - 0
src/mol-app/skin/colors/blue.scss

@@ -0,0 +1,24 @@
+$default-background:         #2D3E50;
+$font-color:                 #EDF1F2;
+$hover-font-color:           #3B9AD9;
+$entity-current-font-color:  #FFFFFF;
+$lm-btn-remove-background:      #BF3A31;
+$lm-btn-remove-hover-font-color:#ffffff;
+$lm-btn-commit-on-font-color:   #ffffff;
+$entity-badge-font-color:    #ccd4e0;
+
+// used in LOG
+$log-message:          #0CCA5D;
+$log-info:             #5E3673;
+$log-warning:          #FCC937;
+$log-error:            #FD354B;
+
+$logo-background: rgba(0,0,0,0.75);
+
+@function color-lower-contrast($color, $amount) {
+    @return darken($color, $amount);
+}
+
+@function color-increase-contrast($color, $amount) {
+    @return lighten($color, $amount);
+}

+ 22 - 0
src/mol-app/skin/colors/dark.scss

@@ -0,0 +1,22 @@
+$default-background:         #111318;
+$font-color:                 #ccd4e0;
+$hover-font-color:           #51A2FB;
+$entity-current-font-color:  #68BEFD;
+$molstar-btn-remove-background:      #DE0A28;
+$molstar-btn-remove-hover-font-color:#F2F4F7;
+$molstar-btn-commit-on-font-color:   #68BEFD;
+$entity-badge-font-color:    #ccd4e0;
+
+// used in LOG
+$log-message:          #0CCA5D;
+$log-info:             #5E3673;
+$log-warning:          #FCC937;
+$log-error:            #FD354B;
+
+@function color-lower-contrast($color, $amount) {
+    @return darken($color, $amount);
+}
+
+@function color-increase-contrast($color, $amount) {
+    @return lighten($color, $amount);
+}

+ 30 - 0
src/mol-app/skin/colors/light.scss

@@ -0,0 +1,30 @@
+// this is complement of the dark theme
+
+@function compl($color) {
+    @return rgb(255 - red($color), 255 - green($color), 255 - blue($color));
+}
+
+$default-background:         compl(#111318);
+$font-color:                 compl(#ccd4e0);
+$hover-font-color:           compl(#51A2FB);
+$entity-current-font-color:  compl(#68BEFD);
+$molstar-btn-commit-on-font-color:   compl(#68BEFD);
+$entity-badge-font-color:    lighten(#ccd4e0,10%);
+$molstar-btn-remove-background:      #DE0A28;
+$molstar-btn-remove-hover-font-color:#F2F4F7;
+
+// used in LOG
+$log-message:          #0CCA5D;
+$log-info:             #5E3673;
+$log-warning:          #FCC937;
+$log-error:            #FD354B;
+
+$logo-background: rgba(204,201,193,0.85);
+
+@function color-lower-contrast($color, $amount) {
+    @return lighten($color, $amount);
+}
+
+@function color-increase-contrast($color, $amount) {
+    @return darken($color, $amount);
+}

+ 144 - 0
src/mol-app/skin/components/controls-base.scss

@@ -0,0 +1,144 @@
+.molstar-btn {
+    padding: 0 $control-spacing;
+    line-height: $row-height;
+    border: none;
+    -moz-box-sizing: border-box;
+    box-sizing: border-box;
+}
+
+.molstar-btn, .molstar-btn:active, .molstar-btn-link:focus, .molstar-btn:hover {
+    outline: none !important;
+}
+
+.molstar-btn-icon {
+    height: $row-height;
+    width: $row-height;
+    line-height: $row-height;
+    padding: 0;
+    text-align: center;
+}
+
+.molstar-btn-link {
+    .molstar-icon {
+        font-size: 100%;
+    }
+}
+
+.molstar-btn-link, .molstar-btn-link:active, .molstar-btn-link:focus {
+    color: $molstar-btn-link-font-color;
+    text-decoration: none;
+}
+
+.molstar-btn-link:hover {
+    color: $hover-font-color;
+    text-decoration: none;
+}
+
+.molstar-btn-link-toggle-on {
+    color: $molstar-btn-link-toggle-on-font-color;
+}
+
+.molstar-btn-link-toggle-off, .molstar-btn-link-toggle-off:active, .molstar-btn-link-toggle-off:focus {
+    color: $molstar-btn-link-toggle-off-font-color;
+}
+
+.molstar-btn-link-toggle-off:hover,  .molstar-btn-link-toggle-on:hover {
+    color: $hover-font-color;
+}
+
+@mixin molstar-btn($name, $font, $bg) {
+    .molstar-btn-#{$name}, .molstar-btn-#{$name}:active, .molstar-btn-#{$name}:focus {
+        color: $font;
+        background: $bg;
+    }
+    .molstar-btn-#{$name}:hover {
+        color: $hover-font-color;
+        background: color-lower-contrast($bg, 2.5%);
+    }
+
+    .molstar-btn-#{$name}[disabled], .molstar-btn-#{$name}[disabled]:hover,
+    .molstar-btn-#{$name}[disabled]:active, .molstar-btn-#{$name}[disabled]:focus {
+        color: color-lower-contrast($font, 1%);
+    }
+}
+
+@include molstar-btn('remove', $molstar-btn-remove-font-color, $molstar-btn-remove-background);
+@include molstar-btn('action', $font-color, $molstar-btn-action-background);
+@include molstar-btn('commit-on', $molstar-btn-commit-on-font-color, $molstar-btn-commit-on-background);
+@include molstar-btn('commit-off', $molstar-btn-commit-off-font-color, $molstar-btn-commit-off-background);
+
+.molstar-btn-remove:hover {
+    color: $molstar-btn-remove-hover-font-color;
+}
+.molstar-btn-commit-on:hover {
+	color: $molstar-btn-commit-on-hover-font-color;
+}
+
+.molstar-btn-action {
+    height: $row-height;
+    line-height: $row-height;
+}
+
+.molstar-form-control {
+    width: 100%;
+    background: $molstar-form-control-background;
+    color: $font-color;
+    border: none !important;
+    padding: 0 $control-spacing;
+    line-height: $row-height - 2px;
+    height: $row-height;
+    -webkit-appearance: none;
+    -moz-appearance: none;
+    appearance: none;
+    box-shadow: none !important;
+
+    &:hover {
+        color: $hover-font-color;
+        background-color: color-increase-contrast($molstar-form-control-background, 5%);
+        border: none;
+        outline-offset: -1px;
+        outline: 1px solid color-increase-contrast($molstar-form-control-background, 20%);
+    }
+
+    &:active, &:focus {
+        color: $font-color;
+        background-color: $molstar-form-control-background;
+        border: none;
+        outline-offset: 0;
+        outline: none;
+    }
+}
+
+.molstar-btn-commit {
+    text-align: right;
+    padding-top: 0;
+    padding-bottom: 0;
+    padding-right: $control-spacing;
+    padding-left: 0;
+    line-height: $row-height;
+    border: none;
+    overflow: hidden;
+
+    .molstar-icon {
+        display: block-inline;
+        line-height: $row-height;
+        margin-right: $control-spacing;
+        width: $row-height;
+        text-align: center;
+        float: left;
+    }
+}
+
+select.molstar-form-control {
+    background: none;
+    background-color: $molstar-form-control-background;
+    background-size: 8px 12px;
+    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAUCAMAAACzvE1FAAAADFBMVEUzMzMzMzMzMzMzMzMKAG/3AAAAA3RSTlMAf4C/aSLHAAAAPElEQVR42q3NMQ4AIAgEQTn//2cLdRKppSGzBYwzVXvznNWs8C58CiussPJj8h6NwgorrKRdTvuV9v16Afn0AYFOB7aYAAAAAElFTkSuQmCC);
+    background-repeat: no-repeat;
+    background-position: right $control-spacing top (($row-height - 12px) / 2);
+}
+
+select.molstar-form-control:-moz-focusring {
+    color: transparent;
+    text-shadow: 0 0 0 $font-color;
+}

+ 197 - 0
src/mol-app/skin/components/controls.scss

@@ -0,0 +1,197 @@
+
+.molstar-control-row {
+    position: relative;
+    height: $row-height;
+    background: $default-background;
+    margin-top: 1px;
+
+    > span {
+        line-height: $row-height;
+        display: block;
+        width: $control-label-width + $control-spacing;
+        text-align: right;
+        padding: 0 $control-spacing;
+        color: color-lower-contrast($font-color, 15%);
+
+        @include non-selectable;
+    }
+
+    select, button, input[type=text] {
+        @extend .molstar-form-control;
+    }
+
+    button {
+        @extend .molstar-btn;
+        @extend .molstar-btn-block;
+    }
+
+    > div:nth-child(2) {
+        background: $molstar-form-control-background;
+        position: absolute;
+        left: $control-label-width + $control-spacing;
+        top: 0;
+        right: 0;
+        bottom: 0;
+    }
+}
+
+.molstar-control-group {
+    position: relative;
+}
+
+.molstar-toggle-button {
+    .molstar-icon {
+        display: inline-block;
+        margin-right: 6px;
+    }
+
+    > div > button:hover {
+        border-color: color-increase-contrast($molstar-form-control-background, 5%) !important;
+        border: none;
+        outline-offset: -1px  !important;
+        outline: 1px solid color-increase-contrast($molstar-form-control-background, 20%) !important;
+    }
+}
+
+.molstar-slider {
+    > div {
+        > div:first-child {
+            position: absolute;
+            top: 0;
+            left: 0;
+            bottom: 0;
+            right: 0;
+            width: 100%;
+            padding-right: 50px;
+            display: table;
+
+            > div {
+                height: $row-height;
+                display: table-cell;
+                vertical-align: middle;
+                padding: 0 ($control-spacing + 4px);
+            }
+        }
+        > div:last-child {
+            position: absolute;
+            height: $row-height;
+            right: 0;
+            width: 50px;
+            top: 0;
+            bottom: 0;
+        }
+    }
+
+    input[type=text] {
+        text-align: right;
+    }
+
+    input[type=range] {
+        width: 100%;
+    }
+}
+
+.molstar-toggle-color-picker {
+    button {
+        border: $control-spacing solid $molstar-form-control-background !important;
+        margin: 0;
+        text-align: center;
+        padding-right: $control-spacing;
+        padding-left: $control-spacing;
+
+        &:hover {
+            border-color: color-increase-contrast($molstar-form-control-background, 5%) !important;
+            border: none;
+            outline-offset: -1px  !important;
+            outline: 1px solid color-increase-contrast($molstar-form-control-background, 20%) !important;
+        }
+    }
+
+    .molstar-color-picker {
+        position: absolute;
+        z-index: 100000;
+        background: $default-background;
+        border-top: 1px solid $default-background;
+        padding-bottom: $control-spacing / 2;
+        width: 100%;
+
+        // input[type=text] {
+        //     background: $molstar-form-control-background !important;
+        // }
+    }
+}
+
+.molstar-toggle-color-picker-above {
+    .molstar-color-picker {
+        top: -2 * 32px - 16px - $control-spacing / 2;
+        height: 2 * 32px + 16px + $control-spacing / 2;
+    }
+}
+
+.molstar-toggle-color-picker-below {
+    .molstar-color-picker {
+        top: $row-height;
+        height: 2 * 32px + 16px;
+    }
+}
+
+
+.molstar-control-subgroup {
+    margin-top: 1px;
+
+    .molstar-control-row {
+        margin-left: $control-spacing !important;
+        > span {
+            width: $control-label-width !important;
+        }
+
+        > div:nth-child(2) {
+            left: $control-label-width !important;
+        }
+    }
+}
+
+.molstar-conrol-group-expander {
+    display: block;
+    position: absolute;
+    line-height: $row-height;
+    padding: 0;
+    left: 0;
+    top: 0;
+    width: $control-label-width + $control-spacing;
+    text-align: left;
+
+    .molstar-icon {
+        line-height: $row-height - 3;
+        width: $row-height - 1;
+        text-align: center;
+        display: inline-block;
+        font-size: 100%;
+    }
+}
+
+.molstar-plugin-layout_controls {
+    position: absolute;
+    left: $control-spacing;
+    top: $control-spacing;
+}
+
+.molstar-plugin-layout_controls > button:first-child {
+    margin-right: 6px;
+}
+
+.molstar-empty-control {
+    display: none;
+}
+
+.molstar-control .molstar-btn-block {
+    margin-bottom: 0px;
+    margin-top: 0px;
+}
+
+.molstar-row-text {
+    > div {
+        line-height: $row-height;
+        text-align: center;
+    }
+}

+ 225 - 0
src/mol-app/skin/components/entity.scss

@@ -0,0 +1,225 @@
+
+
+.molstar-entity-tree {
+    overflow: hidden;
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    top: 0;
+    padding-top: $control-spacing;
+    background: $control-background;
+
+    .molstar-entity-tree-children {
+        overflow-x: hidden;
+        overflow-y: auto;
+        position: absolute;
+        bottom: 0;
+        left: 0;
+        right: 0;
+        top: $row-height + $control-spacing + 1;
+        padding: $control-spacing 0;
+    }
+}
+
+.molstar-entity-store-header {
+    height: $row-height + 1;
+    position: relative;
+
+    > span {
+        margin-left: 6px;
+        display: inline-block;
+        line-height: $row-height;
+        font-weight: bold;
+
+        @include non-selectable
+    }
+
+    button {
+        display: block !important;
+        height: $row-height !important;
+        margin: 0 !important;
+        line-height: $row-height !important;
+        border: none !important;
+        position: absolute;
+        top: 0;
+    }
+
+    border-bottom: 1px solid $border-color;
+}
+
+.molstar-entity-store-root {
+    overflow-x: hidden;
+    overflow-y: auto;
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    top: $row-height + 1;
+    right: 0;
+}
+
+.molstar-entity-tree-entry {
+    height: $row-height + 1;
+    position: relative;
+    border-bottom: 1px solid $control-background;
+}
+
+.molstar-entity-tree-entry-current {
+    background: color-lower-contrast($default-background, 4%) !important;
+
+    .molstar-entity-tree-entry-label {
+        color: $entity-current-font-color;
+        font-weight: bold;
+        .molstar-entity-tree-entry-label-tag {
+            font-weight: normal;
+        }
+        &:hover {
+            color: $hover-font-color;
+        }
+    }
+}
+
+.molstar-entity-tree-entry-current-path {
+    background: color-lower-contrast($default-background, 2%) !important;
+    .molstar-entity-tree-entry-label {
+        color: color-lower-contrast($entity-current-font-color, 5%);
+        &:hover {
+            color: $hover-font-color;
+        }
+    }
+}
+
+.molstar-entity-tree-entry button, .molstar-entity-tree-entry > div {
+    display: block !important;
+    height: $row-height !important;
+    margin: 0 !important;
+    line-height: $row-height !important;
+    border: none !important;
+    position: absolute;
+    top: 0;
+}
+
+
+.molstar-entity-tree-entry-toggle-group {
+    width: $row-height;
+    height: $row-height;
+    padding: 0;
+    left: 0;
+}
+
+.molstar-entity-tree-entry-toggle-visible {
+    width: $row-height;
+    right: 0; //$row-height + 6;
+    padding: 0 !important;
+    font-size: 80%;
+}
+
+.molstar-entity-tree-entry-toggle-visible-full, .molstar-entity-tree-entry-toggle-visible-full:focus, .molstar-entity-tree-entry-toggle-visible-full:active {
+    color: $entity-color-fully-visible;
+}
+
+.molstar-entity-tree-entry-toggle-visible-partial, .molstar-entity-tree-entry-toggle-visible-partial:focus, .molstar-entity-tree-entry-toggle-visible-partial:active {
+    color: $entity-color-partialy-visible;
+}
+
+.molstar-entity-tree-entry-toggle-visible-none, .molstar-entity-tree-entry-toggle-visible-none:focus, .molstar-entity-tree-entry-toggle-visible-none:active {
+    //background: transparent !important;
+    color: $entity-color-not-visible;
+}
+
+.molstar-entity-tree-entry-remove {
+    width: $row-height;
+    height: $row-height;
+    right: $row-height;
+    padding: 0 !important;
+    text-align: center;
+    font-size: 80%;
+    color: color-lower-contrast($font-color, 66%)
+}
+
+.molstar-entity-tree-entry-body {
+    position: absolute;
+    left: $row-height;
+    border-radius: 0 0 0 $entity-subtree-offset;
+    right: 0;
+    background: $default-background;
+}
+
+.molstar-entity-tree-entry .molstar-entity-badge {
+    width: $row-height;
+    position: absolute;
+    height: $row-height;
+    left: 0;
+    top: 0;
+    border-radius: 0 $entity-subtree-offset 0 $entity-subtree-offset;
+}
+
+.molstar-entity-tree-entry-label-wrap {
+    right: 2 * $row-height;
+    overflow: hidden;
+    left: $row-height;
+    height: $row-height;
+    position: absolute;
+}
+
+.molstar-entity-tree-entry-label {
+    position: absolute;
+    right: 0;
+    top: 0;
+    left: 0;
+    text-align: left !important;
+    width: 100%;
+    padding: 0 $control-spacing !important;
+}
+
+.molstar-entity-tree-entry-label-tag {
+    color: $entity-tag-color;
+    font-size: 70%;
+    display: inline-block;
+    margin-left: 6px;
+}
+
+
+.molstar-entity-tree-children-wrap {
+    padding-left: $entity-subtree-offset;
+}
+
+.molstar-entity-tree-root {
+    > .molstar-entity-tree-entry {
+        .molstar-entity-badge {
+            border-top-right-radius: 0;
+        }
+        .molstar-entity-tree-entry-label {
+            font-weight: bold;
+        }
+        .molstar-entity-tree-entry-toggle-group {
+            display: none !important;
+        }
+        .molstar-entity-tree-entry-body {
+            left: $row-height - $entity-subtree-offset !important;
+        }
+        background: $default-background;
+        border-bottom: 1px solid $border-color;
+    }
+
+    > .molstar-entity-tree-children-wrap {
+       margin-top: $control-spacing;
+       padding-left: 0 !important;
+    }
+}
+
+.molstar-panel {
+    .molstar-entity-tree-entry-toggle-visible {
+        position: absolute;
+        top: 0;
+        right: 0;
+        height: $row-height;
+        font-size: 100%;
+
+        background: $default-background; //color-increase-contrast($default-background, 4%);
+    }
+
+    // .molstar-entity-tree-entry-toggle-visible-full {
+    //     background: color-increase-contrast($default-background, 8%);
+    // }
+}

+ 28 - 0
src/mol-app/skin/components/help.scss

@@ -0,0 +1,28 @@
+
+.molstar-help-row {
+    position: relative;
+    height: $row-height;
+    background: $default-background;
+    margin-top: 1px;
+    display: table;
+    width: 100%;
+
+    > span {
+        width: $control-label-width + $control-spacing;
+        text-align: right;
+        padding: $info-vertical-padding $control-spacing;
+        color: color-lower-contrast($font-color, 15%);
+        display: table-cell;
+        font-weight: bold;
+
+        @include non-selectable;
+    }
+
+    > div {
+        background: $molstar-form-control-background;
+        position: relative;
+        padding: $info-vertical-padding $control-spacing;
+        display: table-cell;
+        color: color-lower-contrast($font-color, 15%);
+    }
+}

+ 131 - 0
src/mol-app/skin/components/jobs.scss

@@ -0,0 +1,131 @@
+.molstar-job-state {
+
+    line-height: $row-height;
+    //height: $row-height;
+    //position: relative;
+    //margin-top: 1px;
+
+    > span {
+        @include non-selectable;
+        //display: inline-block;
+        //padding: 0 $control-spacing;
+    }
+
+    // > button {
+    //     margin-top: -2px;
+    //     float: left;
+    //     display: block;
+    //     line-height: $row-height;
+    //     height: $row-height;
+    // }
+}
+
+/* overlay */
+
+.molstar-overlay {
+    position: absolute;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    right: 0;
+    z-index: 1000;
+
+    .molstar-overlay-background {
+        position: absolute;
+        top: 0;
+        left: 0;
+        bottom: 0;
+        right: 0;
+        background: transparent;
+        //background: black;
+        //opacity: 0.5;
+    }
+
+    .molstar-overlay-content-wrap {
+        position: absolute;
+        top: 0;
+        left: 0;
+        bottom: 0;
+        right: 0;
+        display: block;
+        width: 100%;
+        height: 100%;
+    }
+
+    .molstar-overlay-content {
+        text-align: center;
+
+        > div {
+
+            padding-top: 2 * $row-height;
+
+            .molstar-job-state {
+                $size: $row-height;
+                text-align: center;
+
+                > div {
+                    height: $size;
+                    margin-top: $control-spacing;
+                    position: relative;
+                    text-align: center;
+                    width: 100%;
+
+                    > div {
+                        height: $size;
+                        line-height: $size;
+                        display: inline-block;
+                        background: $default-background;
+                        padding: 0 ($control-spacing);
+                        font-weight: bold;
+                        @include non-selectable;
+                    }
+
+                    > button {
+                        display: inline-block;
+                        margin-top: -3px;
+                        font-size: 140%;
+                    }
+                }
+            }
+        }
+    }
+}
+
+/* background */
+
+.molstar-background-jobs {
+    position: absolute;
+    left: 0;
+    bottom: 0;
+    z-index: 1000;
+
+    .molstar-job-state {
+        $size: $row-height;
+
+        > div {
+            height: $size;
+            margin-top: 1px;
+            position: relative;
+            width: 100%;
+            background: $default-background;
+
+            > div {
+                height: $size;
+                line-height: $size;
+                display: inline-block;
+                padding: 0 ($control-spacing);
+                @include non-selectable;
+            }
+
+            > button {
+                display: inline-block;
+                margin-top: -3px;
+                font-size: 140%;
+            }
+        }
+    }
+}
+
+// .molstar-background-jobs .molstar-job-state {
+//     color:
+// }

+ 97 - 0
src/mol-app/skin/components/log.scss

@@ -0,0 +1,97 @@
+
+.molstar-log-wrap {
+    position: absolute;
+    right: 0;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    overflow: hidden;
+}
+
+.molstar-log {
+    position: absolute;
+    right: -20px;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    overflow-y: scroll;
+    overflow-x: hidden;
+    font-size: 90%;
+    background: $control-background;
+}
+
+.molstar-log {
+    ul {
+        padding: 0;
+        margin: 0;
+    }
+
+    color: $log-font-color;
+
+    li {
+        clear: both;
+        margin: 0;
+        background: $default-background;
+        position: relative;
+
+        &:not(:last-child) {
+            border-bottom: 1px solid $border-color;
+        }
+    }
+
+
+    .molstar-log-entry {
+        margin-left: $control-label-width;
+        background: color-lower-contrast($control-background, 5%);
+        padding: $info-vertical-padding ($control-spacing + 15px) $info-vertical-padding $control-spacing ;
+    }
+
+    .molstar-log-timestamp {
+        padding: ($info-vertical-padding + 1) $control-spacing ($info-vertical-padding - 1) $control-spacing;
+        float: left;
+        text-align: right;
+        width: $control-label-width;
+        color: $log-timestamp-font-color;
+        //vertical-align: baseline;
+        //line-height: $row-height;
+        font-size: 90%;
+    }
+
+    .molstar-log-timestamp small {
+        font-size: 90%;
+    }
+}
+
+// .molstar-log hr {
+//     border-color: $separator-color;
+//     margin: 3px 3px 0 5px;
+// }
+
+.molstar-log .label {
+    margin-top: -3px;
+    font-size: 7pt;
+}
+
+.molstar-log-entry-badge {
+    position: absolute;
+    left: 0;
+    top: 0;
+    bottom: 0;
+    width: 6px;
+}
+
+.molstar-log-entry-message {
+    background: $log-message;
+}
+
+.molstar-log-entry-info {
+    background: $log-info;
+}
+
+.molstar-log-entry-error {
+    background: $log-error;
+}
+
+.molstar-log-entry-warning {
+    background: $log-warning;
+}

+ 69 - 0
src/mol-app/skin/components/misc.scss

@@ -0,0 +1,69 @@
+.molstar-description {
+    padding: $control-spacing;
+    font-size: 85%;
+    background: $default-background;
+    text-align: center;
+    //font-style: italic;
+
+    -webkit-user-select: none; /* Chrome/Safari */
+    -moz-user-select: none; /* Firefox */
+    -ms-user-select: none; /* IE10+ */
+
+    /* Rules below not implemented in browsers yet */
+    -o-user-select: none;
+    user-select: none;
+
+    font-weight: light;
+
+    cursor: default;
+}
+
+.molstar-description:not(:first-child) {
+    border-top: 1px solid $control-background;
+}
+
+.molstar-color-picker input {
+    color: black !important;
+}
+
+.molstar-no-webgl {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    left: 0;
+    top: 0;
+    display: table;
+    text-align: center;
+
+    > div {
+        b {
+            font-size: 120%;
+        }
+        display: table-cell;
+        vertical-align: middle;
+        text-align: center;
+        width: 100%;
+        height: 100%;
+    }
+}
+
+.molstar-loader-molstar-btn-file {
+    position: relative;
+    overflow: hidden;
+}
+
+.molstar-loader-molstar-btn-file input[type=file] {
+    position: absolute;
+    top: 0;
+    right: 0;
+    min-width: 100%;
+    min-height: 100%;
+    font-size: 100px;
+    text-align: right;
+    filter: alpha(opacity=0);
+    opacity: 0;
+    outline: none;
+    background: white;
+    cursor: inherit;
+    display: block;
+}

+ 142 - 0
src/mol-app/skin/components/panel.scss

@@ -0,0 +1,142 @@
+.molstar-panel-header .molstar-panel-expander {
+    display: block;
+    width: 100%;
+    text-align: left;
+}
+
+.molstar-panel-header {
+
+    //border-bottom-width: 1px;
+    //border-bottom-style: solid;
+    height: $row-height;
+    border-color: $border-color;
+    position: relative;
+
+    //border-radius: $control-spacing 0 0 0;
+
+    .molstar-panel-expander-wrapper {
+
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 2 * $row-height;
+
+        button {
+            // width: 100%;
+
+            display: block;
+            width: 100%;
+            text-align: left;
+
+            height: $row-height;
+            line-height: $row-height;
+            border: none;
+            font-weight: bold;
+            //color: $panel-header-font-color;
+            padding-left: 0;
+            background: color-lower-contrast($default-background, 4%);
+            //text-align: right!important;
+
+            .molstar-icon {
+                display: inline-block;
+                margin-right: $control-spacing;
+                width: $row-height;
+                text-align: center;
+            }
+
+            &:hover {
+                background: color-lower-contrast($default-background, 4%);
+            }
+        }
+    }
+
+    .molstar-panel-description-standalone {
+        > .molstar-icon {
+            margin-left: $row-height;
+        }
+
+        width: 2 * $row-height;
+    }
+
+    .molstar-panel-description-with-action {
+        width: $row-height;
+        margin-right: $row-height;
+    }
+
+    .molstar-panel-description {
+        color: $font-color;
+        float: right;
+        background: color-lower-contrast($default-background, 4%);
+        //margin-right: $row-height;
+
+        > .molstar-icon {
+            display: block;
+            width: $row-height;
+            height: $row-height;
+            line-height: $row-height;
+            text-align: center;
+            font-size: 70%;
+            cursor: default;
+            background: color-lower-contrast($default-background, 4%);
+            color: color-lower-contrast($font-color, 66%);
+        }
+
+        .molstar-panel-description-content {
+            @include non-selectable;
+
+            color: $font-color;
+            display: none;
+            position: absolute;
+            left: 0;
+            width: 100%;
+            background: color-increase-contrast($molstar-form-control-background, 20%);
+            min-height: $row-height;
+            z-index: 1000000;
+            padding: $info-vertical-padding  $control-spacing $info-vertical-padding ($row-height + $control-spacing);
+            text-align: left;
+            //border-bottom: 1px solid color-lower-contrast($default-background, 4%);
+
+            > .molstar-icon {
+                position: absolute;
+                width: $row-height;
+                height: $row-height;
+                line-height: $row-height;
+                text-align: center;
+                font-size: 80%;
+                cursor: default;
+                top: 0;
+                left: 0;
+            }
+        }
+
+        &:hover {
+            color: $hover-font-color;
+            > .molstar-icon {
+                color: $hover-font-color;
+            }
+            .molstar-panel-description-content {
+                display: block;
+            }
+        }
+    }
+}
+
+.molstar-panel-body {
+    background: $control-background;
+}
+
+.molstar-panel {
+    margin-bottom: $control-spacing;
+}
+
+.molstar-transform-view {
+    padding-top: $control-spacing;
+}
+
+.molstar-expandable-group-color-stripe {
+    position: absolute;
+    left: 0;
+    top: $row-height - 2px;
+    width: $control-label-width + $control-spacing;
+    height: 2px;
+}

+ 164 - 0
src/mol-app/skin/components/slider.scss

@@ -0,0 +1,164 @@
+@mixin borderBox {
+    box-sizing: border-box;
+    -webkit-tap-highlight-color: rgba(0, 0, 0, 0); //  remove tap highlight color for mobile safari
+
+    * {
+      box-sizing: border-box;
+      -webkit-tap-highlight-color: rgba(0, 0, 0, 0); //  remove tap highlight color for mobile safari
+    }
+  }
+
+  .molstar-slider-base {
+    position: relative;
+    height: 14px;
+    padding: 5px 0;
+    width: 100%;
+    border-radius: $slider-border-radius-base;
+    @include borderBox;
+
+    &-rail {
+      position: absolute;
+      width: 100%;
+      background-color: $border-color;
+      height: 4px;
+      border-radius: 2px;
+    }
+
+    &-track {
+      position: absolute;
+      left: 0;
+      height: 4px;
+      border-radius: $slider-border-radius-base;
+      background-color: tint($font-color, 60%);
+    }
+
+    &-handle {
+      position: absolute;
+      margin-left: -11px;
+      margin-top: -9px;
+      width: 22px;
+      height: 22px;
+      cursor: pointer;
+      border-radius: 50%;
+      background-color: $font-color;
+      border: 4px solid $border-color;
+
+      &:hover {
+        background-color: $hover-font-color;
+      }
+    }
+
+    &-mark {
+      position: absolute;
+      top: 18px;
+      left: 0;
+      width: 100%;
+      font-size: 12px;
+    }
+
+    &-mark-text {
+      position: absolute;
+      display: inline-block;
+      vertical-align: middle;
+      text-align: center;
+      cursor: pointer;
+      color: #999;
+
+      &-active {
+        color: #666;
+      }
+    }
+
+    &-step {
+      position: absolute;
+      width: 100%;
+      height: 4px;
+      background: transparent;
+    }
+
+    &-dot {
+      position: absolute;
+      bottom: -2px;
+      margin-left: -4px;
+      width: 8px;
+      height: 8px;
+      border: 2px solid #e9e9e9;
+      background-color: #fff;
+      cursor: pointer;
+      border-radius: 50%;
+      vertical-align: middle;
+      &:first-child {
+        margin-left: -4px;
+      }
+      &:last-child {
+        margin-left: -4px;
+      }
+      &-active {
+        border-color: tint($font-color, 50%);
+      }
+    }
+
+    &-disabled {
+      background-color: #e9e9e9;
+
+      .molstar-slider-base-track {
+        background-color: $slider-disabledColor;
+      }
+
+      .molstar-slider-base-handle, .molstar-slider-base-dot {
+        border-color: $slider-disabledColor;
+        background-color: #fff;
+        cursor: not-allowed;
+      }
+
+      .molstar-slider-base-mark-text, .molstar-slider-base-dot {
+        cursor: not-allowed!important;
+      }
+    }
+  }
+
+  .molstar-slider-base-vertical {
+    width: 14px;
+    height: 100%;
+    padding: 0 5px;
+
+    .molstar-slider-base {
+      &-rail {
+        height: 100%;
+        width: 4px;
+      }
+
+      &-track {
+        left: 5px;
+        bottom: 0;
+        width: 4px;
+      }
+
+      &-handle {
+        margin-left: -5px;
+        margin-bottom: -7px;
+      }
+
+      &-mark {
+        top: 0;
+        left: 18px;
+        height: 100%;
+      }
+
+      &-step {
+        height: 100%;
+        width: 4px;
+      }
+
+      &-dot {
+        left: 2px;
+        margin-bottom: -4px;
+        &:first-child {
+          margin-bottom: -4px;
+        }
+        &:last-child {
+          margin-bottom: -4px;
+        }
+      }
+    }
+  }

+ 93 - 0
src/mol-app/skin/components/viewport.scss

@@ -0,0 +1,93 @@
+
+.molstar-viewport {
+    position: absolute;
+    left: 0;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    background: black;
+
+    .molstar-btn-link {
+        background: rgba(0,0,0,0.2);
+    }
+
+}
+
+.molstar-viewport-expanded {
+    position: fixed;
+    z-index: 1000;
+}
+
+.molstar-viewport-container {
+    position: absolute;
+    left: 0;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    -webkit-user-select: none;
+    -webkit-tap-highlight-color: rgba(0,0,0,0);
+    -webkit-touch-callout: none;
+}
+
+.molstar-viewport-controls {
+    position: absolute;
+    right: $control-spacing;
+    top: $control-spacing;
+}
+
+.molstar-viewport-controls-buttons {
+    text-align: right;
+
+    > button {
+        padding: 0;
+        text-align: center;
+        width: $row-height;
+    }
+
+    > button:last-child {
+        margin-left: $control-spacing;
+    }
+
+    .molstar-btn-link, .molstar-btn-link-toggle-on {
+        color: #eee;
+    }
+
+    .molstar-btn-link-toggle-off {
+        color: $molstar-btn-link-toggle-off-font-color;
+    }
+
+    .molstar-btn-link:hover {
+        color: $hover-font-color;
+    }
+}
+
+.molstar-viewport-controls-scene-options {
+    width: 290px;
+    background: $control-background;
+}
+
+/* highlight */
+
+.molstar-highlight-info {
+
+    color: $highlight-info-font-color;
+    padding: $info-vertical-padding $control-spacing;
+    background: $default-background; //$highlight-info-background;
+
+    position: absolute;
+    top: $control-spacing;
+    left: $control-spacing;
+    text-align: left;
+    min-height: $row-height;
+    max-width: 95%;
+
+    //border-bottom-right-radius: 6px;
+    z-index: 10000;
+    @include non-selectable;
+}
+
+.molstar-highlight-info-additional {
+    font-size: 85%;
+    display: inline-block;
+    color: $highlight-info-additional-font-color;
+}

BIN
src/mol-app/skin/fonts/fontello.eot


+ 442 - 0
src/mol-app/skin/fonts/fontello.svg

@@ -0,0 +1,442 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata>Copyright (C) 2016 by original authors @ fontello.com</metadata>
+<defs>
+<font id="fontello" horiz-adv-x="1000" >
+<font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
+<missing-glyph horiz-adv-x="1000" />
+<glyph glyph-name="palette" unicode="&#xe800;" d="M857 622q72-48 101-110t20-104-35-48q-16-4-54 10t-80 10-80-46q-30-46-21-75t34-65 23-50q-2-26-36-63t-126-74-216-37q-186 0-291 101t-95 245q8 118 104 235t216 151q290 84 536-80z m-318-466q30 0 52 22t22 54-22 53-52 21q-32 0-54-21t-22-53 22-54 54-22z" horiz-adv-x="980" />
+
+<glyph glyph-name="search" unicode="&#xe803;" d="M772 78q30-34 6-62l-46-46q-36-32-68 0l-190 190q-74-42-156-42-128 0-223 95t-95 223 90 219 218 91 224-95 96-223q0-88-46-162z m-678 358q0-88 68-156t156-68 151 63 63 153q0 88-68 155t-156 67-151-63-63-151z" horiz-adv-x="789" />
+
+<glyph glyph-name="flashlight" unicode="&#xe804;" d="M807 706q62-62 85-130t-5-92l-134-134q-16-16-62-26t-96-4l-408-408q-18-18-57-6t-75 50q-36 36-49 74t5 56l408 408q-6 50 4 96t26 62l136 136q24 28 92 4t130-86z m-448-408q32-32 80 14 46 46 14 82-14 14-38 10t-44-24-23-43 11-39z m336 298q30-30 68-50t62-25 28-1q2 4-4 27t-26 60-50 67-66 50-59 26-27 2 1-28 25-62 48-66z" horiz-adv-x="902" />
+
+<glyph glyph-name="mail" unicode="&#xe805;" d="M30 586q-32 18-28 40 2 14 26 14l846 0q38 0 20-32-8-14-24-22-14-6-192-102t-182-98q-16-10-46-10-28 0-46 10-4 2-182 98t-192 102z m850-100q20 10 20-10l0-368q0-16-17-32t-33-16l-800 0q-16 0-33 16t-17 32l0 368q0 20 20 10l384-200q18-10 46-10t46 10z" horiz-adv-x="900" />
+
+<glyph glyph-name="heart" unicode="&#xe806;" d="M790 644q70-64 70-156t-70-158l-360-330-360 330q-70 66-70 158t70 156q62 58 151 58t153-58l56-52 58 52q62 58 150 58t152-58z" horiz-adv-x="860" />
+
+<glyph glyph-name="heart-empty" unicode="&#xe807;" d="M790 642q70-64 70-156t-70-156l-360-330-360 330q-70 64-70 156t70 156q64 58 152 58t150-58l58-52 56 52q64 58 152 58t152-58z m-54-260q42 40 42 104 0 66-38 100-38 38-102 38-52 0-104-48l-104-92-106 92q-48 48-102 48-64 0-104-38-38-36-38-100 0-66 44-104l306-286z" horiz-adv-x="860" />
+
+<glyph glyph-name="star" unicode="&#xe808;" d="M440 790l120-336 320 0-262-196 94-348-272 208-272-208 94 348-262 196 320 0z" horiz-adv-x="880" />
+
+<glyph glyph-name="star-empty" unicode="&#xe809;" d="M880 454l-262-196 94-348-272 208-272-208 94 348-262 196 320 0 120 336 120-336 320 0z m-440-238l150-124-62 178 144 114-176-4-56 202-54-202-176 4 142-114-62-178z" horiz-adv-x="880" />
+
+<glyph glyph-name="user" unicode="&#xe80a;" d="M736 128q204-72 204-122l0-106-940 0 0 106q0 50 204 122 94 34 128 69t34 95q0 22-22 49t-32 73q-2 12-9 18t-14 8-14 17-9 43q0 16 5 26t9 12l4 4q-8 50-12 88-4 54 41 112t157 58 158-58 40-112l-12-88q18-8 18-42-2-28-9-43t-14-17-14-8-9-18q-8-48-31-74t-23-48q0-60 35-95t127-69z" horiz-adv-x="940" />
+
+<glyph glyph-name="users" unicode="&#xe80b;" d="M1000-90l-224 0 0 150q0 54-30 81t-154 89q40 30 40 84 0 16-13 33t-19 51q-2 8-14 16t-14 42q0 24 12 30-6 34-8 60-4 38 23 78t95 40 96-40 24-78l-8-60q12-6 12-30-2-34-14-42t-14-16q-6-34-19-51t-13-33q0-42 21-66t77-48q112-46 130-80 6-8 9-61t5-101l0-48z m-488 262q182-78 182-124l0-138-694 0 0 184q0 44 84 78 76 32 104 64t28 88q0 20-19 44t-25 68q-2 10-18 22t-20 56q0 14 3 23t7 13l4 2q-6 46-10 82-4 50 33 103t127 53 127-53 33-103l-10-82q14-8 14-38-4-44-20-56t-18-22q-6-44-25-68t-19-44q0-56 28-88t104-64z" horiz-adv-x="1000" />
+
+<glyph glyph-name="user-add" unicode="&#xe80c;" d="M620 128q180-64 180-122l0-106-800 0 0 202q36 14 82 26 94 34 129 69t35 95q0 22-23 48t-31 74q-2 12-23 25t-25 61q0 16 5 26t9 12l4 4q-8 50-12 88-6 54 40 112t160 58 160-58 42-112l-14-88q18-8 18-42-2-28-9-43t-14-17-14-8-9-18q-10-46-33-73t-23-49q0-60 36-95t130-69z m230 272l150 0 0-100-150 0 0-150-100 0 0 150-150 0 0 100 150 0 0 150 100 0 0-150z" horiz-adv-x="1000" />
+
+<glyph glyph-name="video" unicode="&#xe80d;" d="M980 600l-100 0 0-100 100 0 0-100-100 0 0-100 100 0 0-100-100 0 0-100 100 0 0-60q0-16-12-28t-28-12l-900 0q-16 0-28 12t-12 28l0 60 100 0 0 100-100 0 0 100 100 0 0 100-100 0 0 100 100 0 0 100-100 0 0 60q0 18 12 29t28 11l900 0q16 0 28-11t12-29l0-60z m-600-400l250 150-250 150 0-300z" horiz-adv-x="980" />
+
+<glyph glyph-name="picture" unicode="&#xe80e;" d="M856 518l-100 0-124 150-214-150-180 0q-52 0-90-39t-38-91l0-160-108 296q-10 38 22 52l680 248q36 10 50-24z m106-90q16 0 27-12t11-28l0-472q0-16-11-28t-27-12l-724 0q-16 0-27 12t-11 28l0 472q0 16 11 28t27 12l724 0z m-56-452l0 162-72 160-166-60-130-132-138 170-92-214 0-86 598 0z" horiz-adv-x="1000" />
+
+<glyph glyph-name="camera" unicode="&#xe80f;" d="M500 450q64 0 107-44t43-106-44-106-106-44-106 44-44 106 44 106 106 44z m400 150q42 0 71-29t29-71l0-450q0-40-29-70t-71-30l-800 0q-40 0-70 30t-30 70l0 450q0 42 30 71t70 29l120 0q28 0 40 30l30 92q10 28 40 28l340 0q30 0 40-28l30-92q12-30 40-30l120 0z m-400-550q104 0 177 73t73 177-73 177-177 73-177-73-73-177 73-177 177-73z m366 380q14 0 24 11t10 25-10 24-24 10q-36 0-36-34 0-16 11-26t25-10z" horiz-adv-x="1000" />
+
+<glyph glyph-name="layout" unicode="&#xe810;" d="M170 650q80 0 80-80l0-90q0-80-80-80l-90 0q-80 0-80 80l0 90q0 80 80 80l90 0z m350 0q80 0 80-80l0-90q0-80-80-80l-90 0q-80 0-80 80l0 90q0 80 80 80l90 0z m-350-350q80 0 80-80l0-90q0-80-80-80l-90 0q-80 0-80 80l0 90q0 80 80 80l90 0z m350 0q80 0 80-80l0-90q0-80-80-80l-90 0q-80 0-80 80l0 90q0 80 80 80l90 0z" horiz-adv-x="600" />
+
+<glyph glyph-name="menu" unicode="&#xe811;" d="M650 400q22 0 36-15t14-35-15-35-35-15l-600 0q-20 0-35 15t-15 35 14 35 36 15l600 0z m-600 100q-20 0-35 15t-15 35 14 35 36 15l600 0q22 0 36-15t14-35-15-35-35-15l-600 0z m600-300q22 0 36-15t14-35-15-35-35-15l-600 0q-20 0-35 15t-15 35 14 35 36 15l600 0z" horiz-adv-x="700" />
+
+<glyph glyph-name="check" unicode="&#xe812;" d="M249 0q-34 0-56 28l-180 236q-16 24-12 52t26 46 51 14 47-28l118-154 296 474q16 24 43 30t53-8q24-16 30-43t-8-53l-350-560q-20-32-56-32z" horiz-adv-x="667" />
+
+<glyph glyph-name="cancel" unicode="&#xe813;" d="M452 194q18-18 18-43t-18-43q-18-16-43-16t-43 16l-132 152-132-152q-18-16-43-16t-43 16q-16 18-16 43t16 43l138 156-138 158q-16 18-16 43t16 43q18 16 43 16t43-16l132-152 132 152q18 16 43 16t43-16q18-18 18-43t-18-43l-138-158z" horiz-adv-x="470" />
+
+<glyph glyph-name="cancel-circled" unicode="&#xe814;" d="M420 770q174 0 297-123t123-297-123-297-297-123-297 123-123 297 123 297 297 123z m86-420l154 154-86 86-154-152-152 152-88-86 154-154-154-152 88-86 152 152 154-152 86 86z" horiz-adv-x="840" />
+
+<glyph glyph-name="cancel-squared" unicode="&#xe815;" d="M700 750q42 0 71-29t29-71l0-600q0-40-29-70t-71-30l-600 0q-40 0-70 30t-30 70l0 600q0 42 30 71t70 29l600 0z m-146-638l86 86-154 152 154 154-86 86-154-152-152 152-88-86 154-154-154-152 88-86 152 152z" horiz-adv-x="800" />
+
+<glyph glyph-name="plus" unicode="&#xe816;" d="M550 400q30 0 30-50t-30-50l-210 0 0-210q0-30-50-30t-50 30l0 210-210 0q-30 0-30 50t30 50l210 0 0 210q0 30 50 30t50-30l0-210 210 0z" horiz-adv-x="580" />
+
+<glyph glyph-name="plus-circled" unicode="&#xe817;" d="M420 770q174 0 297-123t123-297-123-297-297-123-297 123-123 297 123 297 297 123z m52-470l200 0 0 102-200 0 0 202-102 0 0-202-202 0 0-102 202 0 0-202 102 0 0 202z" horiz-adv-x="840" />
+
+<glyph glyph-name="plus-squared" unicode="&#xe818;" d="M700 750q42 0 71-29t29-71l0-600q0-40-29-70t-71-30l-600 0q-40 0-70 30t-30 70l0 600q0 42 30 71t70 29l600 0z m-50-450l0 100-200 0 0 200-100 0 0-200-200 0 0-100 200 0 0-200 100 0 0 200 200 0z" horiz-adv-x="800" />
+
+<glyph glyph-name="minus" unicode="&#xe819;" d="M550 400q30 0 30-50t-30-50l-520 0q-30 0-30 50t30 50l520 0z" horiz-adv-x="580" />
+
+<glyph glyph-name="minus-circled" unicode="&#xe81a;" d="M420 770q174 0 297-123t123-297-123-297-297-123-297 123-123 297 123 297 297 123z m252-368l-504 0 0-102 504 0 0 102z" horiz-adv-x="840" />
+
+<glyph glyph-name="minus-squared" unicode="&#xe81b;" d="M700 750q42 0 71-29t29-71l0-600q0-40-29-70t-71-30l-600 0q-40 0-70 30t-30 70l0 600q0 42 30 71t70 29l600 0z m-50-450l0 100-500 0 0-100 500 0z" horiz-adv-x="800" />
+
+<glyph glyph-name="help" unicode="&#xe81c;" d="M494 740q86-62 86-184 0-64-42-124-12-20-88-80l-46-30q-40-34-48-60-6-16-8-44 0-14-16-14l-128 0q-16 0-16 12 4 98 28 124 16 22 48 48t56 42l24 14q22 16 34 34 28 44 28 70 0 40-26 78-28 36-92 36-68 0-94-44-28-42-28-92l-166 0q6 162 114 232 70 42 166 42 130 0 214-60z m-216-636q44 0 73-30t27-74q-2-46-32-73t-74-25q-44 0-73 29t-27 75 32 73 74 25z" horiz-adv-x="580" />
+
+<glyph glyph-name="help-circled" unicode="&#xe81d;" d="M454 810q190 2 326-130t140-322q2-190-131-327t-323-141q-190-2-327 131t-139 323q-4 190 130 327t324 139z m-2-740q30 0 49 19t19 47q2 30-17 49t-49 19l-2 0q-28 0-47-18t-21-46q0-30 19-49t47-21l2 0z m166 328q26 34 26 78 0 78-54 116-52 38-134 38-64 0-104-26-68-42-72-146l0-4 110 0 0 4q0 26 16 54 16 24 54 24 40 0 52-20 16-20 16-44 0-18-16-40-8-12-20-20l-6-4q-6-4-16-11t-20-15-21-17-17-17q-14-20-18-78l0-8 108 0 0 4q0 12 4 28 6 20 28 36l28 18q46 34 56 50z" horiz-adv-x="920" />
+
+<glyph glyph-name="info" unicode="&#xe81e;" d="M352 850q48 0 74-27t26-69q0-50-39-88t-95-38q-48 0-74 26t-24 72q0 46 35 85t97 39z m-206-1000q-100 0-54 178l60 254q14 56 0 56-12 0-54-18t-72-38l-26 44q90 78 189 126t151 48q78 0 36-162l-70-266q-16-64 6-64 44 0 118 60l30-40q-84-86-175-132t-139-46z" horiz-adv-x="460" />
+
+<glyph glyph-name="info-circled" unicode="&#xe81f;" d="M454 810q190 2 326-130t140-322q2-190-131-327t-323-141q-190-2-327 131t-139 323q-4 190 130 327t324 139z m52-152q-42 0-65-24t-23-50q-2-28 15-44t49-16q38 0 61 22t23 54q0 58-60 58z m-120-594q30 0 84 26t106 78l-18 24q-48-36-72-36-14 0-4 38l42 160q26 96-22 96-30 0-89-29t-115-75l16-26q52 34 74 34 12 0 0-34l-36-152q-26-104 34-104z" horiz-adv-x="920" />
+
+<glyph glyph-name="back" unicode="&#xe820;" d="M750 540q40 0 70-29t30-71l0-290q0-40-30-70t-70-30l-690 0 0 140 650 0 0 210-500 0 0-110-210 180 210 180 0-110 540 0z" horiz-adv-x="850" />
+
+<glyph glyph-name="home" unicode="&#xe821;" d="M888 336q16-16 11-27t-27-11l-84 0 0-310q0-14-1-21t-8-13-23-6l-204 0 0 310-204 0 0-310-194 0q-28 0-35 10t-7 30l0 310-84 0q-22 0-27 11t11 27l400 402q16 16 38 16t38-16z" horiz-adv-x="900" />
+
+<glyph glyph-name="link" unicode="&#xe822;" d="M294 116q14 14 34 14t36-14q32-34 0-70l-42-40q-56-56-132-56-78 0-134 56t-56 132q0 78 56 134l148 148q70 68 144 77t128-43q16-16 16-36t-16-36q-36-32-70 0-50 48-132-34l-148-146q-26-26-26-64t26-62q26-26 63-26t63 26z m450 574q56-56 56-132 0-78-56-134l-158-158q-74-72-150-72-62 0-112 50-14 14-14 34t14 36q14 14 35 14t35-14q50-48 122 24l158 156q28 28 28 64 0 38-28 62-24 26-56 31t-60-21l-50-50q-16-14-36-14t-34 14q-34 34 0 70l50 50q54 54 127 51t129-61z" horiz-adv-x="800" />
+
+<glyph glyph-name="attach" unicode="&#xe823;" d="M244-140q-102 0-170 72-72 70-74 166t84 190l496 496q80 80 174 54 44-12 79-47t47-79q26-96-54-176l-474-474q-40-40-88-46-48-4-80 28-30 24-27 74t47 92l332 334q24 26 50 0t0-50l-332-332q-44-44-20-70 12-8 24-6 24 4 46 26l474 474q50 50 34 108-16 60-76 76-54 14-108-36l-494-494q-66-76-64-143t52-117q50-48 117-50t141 62l496 494q24 24 50 0 26-22 0-48l-496-496q-82-82-186-82z" horiz-adv-x="939" />
+
+<glyph glyph-name="lock" unicode="&#xe824;" d="M640 476q20 0 40-19t20-41l0-390q0-48-48-66l-60-18q-42-16-96-16l-290 0q-56 0-98 16l-60 18q-48 18-48 66l0 390q0 22 15 41t35 19l100 0 0 70q0 110 51 170t149 60 149-60 51-170l0-70 90 0z m-390 90l0-90 200 0 0 90q0 52-27 81t-73 29-73-29-27-81z" horiz-adv-x="700" />
+
+<glyph glyph-name="lock-open" unicode="&#xe825;" d="M640 450q20 0 40-20t20-40l0-390q0-20-14-39t-34-25l-60-20q-52-16-96-16l-290 0q-46 0-98 16l-60 20q-20 6-34 25t-14 39l0 390q0 22 15 41t35 19l400 0 0 140q0 110-100 110t-100-110l0-40-100 0 0 20q0 110 51 170t149 60q200 0 200-230l0-120 90 0z" horiz-adv-x="700" />
+
+<glyph glyph-name="eye" unicode="&#xe826;" d="M500 630q92 0 177-25t141-62 99-77 63-71 20-45-20-44-63-71-99-78-141-62-177-25-177 25-141 62-99 78-63 71-20 44 20 45 63 71 99 77 141 62 177 25z m0-494q92 0 157 63t65 151q0 90-65 153t-157 63-157-63-65-153q0-88 65-151t157-63z m0 214q8-8 37-2t50 11 25-9q0-44-33-75t-79-31-78 31-32 75q0 46 32 77t78 31q14 0 10-23t-12-47 2-38z" horiz-adv-x="1000" />
+
+<glyph glyph-name="tag" unicode="&#xe827;" d="M944 830q36-106-8-199t-128-157l18-24q16-28 6-54l-48-158q-12-30-36-46l-464-328q-42-30-64 4l-210 304q-12 18-9 39t21 33l464 328q26 18 54 18l158 0q30 0 48-26l28-40q168 130 114 286-10 28 18 40 32 8 38-20z m-216-468q40 32 34 80l-32-16q-8-4-12-4-18 0-28 18-12 30 16 40l24 14q-48 34-92 0-28-18-34-51t14-61q18-26 51-32t59 12z" horiz-adv-x="960" />
+
+<glyph glyph-name="bookmark" unicode="&#xe828;" d="M310 800q22 0 36-15t14-35l0-850-180 180-180-180 0 850q0 50 40 50l270 0z" horiz-adv-x="360" />
+
+<glyph glyph-name="bookmarks" unicode="&#xe829;" d="M500 850q20 0 35-15t15-35l0-850-150 180 0 620q0 20-15 35t-35 15l-100 0q0 50 40 50l210 0z m-250-150q20 0 35-15t15-35l0-800-150 180-150-180 0 800q0 50 40 50l210 0z" horiz-adv-x="550" />
+
+<glyph glyph-name="flag" unicode="&#xe82a;" d="M874 616q14 6 22-1t0-19q-96-138-164-213t-110-90-73-2-60 37-63 40-93-4-139-86l90-352-100 0-184 720 92 34q90 66 152 86t98 3 64-51 62-71 79-62 129-20 198 51z" horiz-adv-x="900" />
+
+<glyph glyph-name="thumbs-up" unicode="&#xe82b;" d="M582 480q2-6 58-13t108-24 52-47q0-72-61-284t-107-212q-144 0-288 42t-144 88l0 342q0 14 15 34t46 45 53 41 62 43 46 31q50 34 104 100t85 104 41 26q48-76 29-137t-59-119-40-60z m-432-4q14 0 0-14-50-50-50-104l0-318q0-50 52-104 10-10-2-10-26 0-55 8t-62 45-33 99l0 242q0 62 33 100t63 47 54 9z" horiz-adv-x="800" />
+
+<glyph glyph-name="thumbs-down" unicode="&#xe82c;" d="M218 218q-2 6-57 13t-108 24-53 47q0 72 62 285t106 213q144 0 288-43t144-89l0-342q0-10-8-24t-25-30-32-29-42-32-41-29-41-28l-33-22q-50-34-104-100t-85-104-41-26q-48 76-29 137t59 119 40 60z m432 4q-12 0 2 14 48 50 48 104l0 318q0 50-52 104-10 10 2 10 26 0 55-8t62-45 33-99l0-242q0-48-18-81t-45-48-48-21-39-6z" horiz-adv-x="800" />
+
+<glyph glyph-name="download" unicode="&#xe82d;" d="M968 198q18-10 27-32t3-40l-28-154q-4-20-22-33t-40-13l-816 0q-22 0-40 13t-22 33l-28 154q-10 48 32 72l158 108 98 0-170-130 178 0q8 0 12-8l40-110 300 0 40 110q8 8 12 8l178 0-170 130 98 0z m-208 322l-260-244-260 244 166 0 0 256 190 0 0-256 164 0z" horiz-adv-x="1000" />
+
+<glyph glyph-name="upload" unicode="&#xe82e;" d="M500 776l260-244-164 0 0-256-190 0 0 256-166 0z m468-578q18-10 27-32t3-40l-28-154q-4-20-22-33t-40-13l-816 0q-22 0-40 13t-22 33l-28 154q-10 48 32 72l158 108 98 0-170-130 178 0q8 0 12-8l40-110 300 0 40 110q8 8 12 8l178 0-170 130 98 0z" horiz-adv-x="1000" />
+
+<glyph glyph-name="upload-cloud" unicode="&#xe82f;" d="M760 494q100 0 170-68t70-166-70-166-170-68l-190 0 0 190 106 0-176 230-174-230 104 0 0-190-248 0q-74 0-128 52t-54 124q0 74 53 126t129 52q14 0 20-2-2 12-2 38 0 108 78 184t188 76q90 0 160-52t94-134q28 4 40 4z" horiz-adv-x="1000" />
+
+<glyph glyph-name="reply" unicode="&#xe830;" d="M900 10q-86 152-208 197t-330 45l0-218-362 334 362 322 0-192q90 0 168-27t131-70 96-95 69-104 44-95 24-69z" horiz-adv-x="900" />
+
+<glyph glyph-name="reply-all" unicode="&#xe831;" d="M362 556l-212-188 212-196 0-138-362 334 362 322 0-134z m250-58q104 0 182-50t115-122 60-144 27-122l4-50q-86 154-168 198t-220 44l0-218-362 334 362 322 0-192z" horiz-adv-x="1000" />
+
+<glyph glyph-name="forward" unicode="&#xe832;" d="M540 252q-210 0-332-45t-208-197q4 20 13 53t50 117 96 148 156 117 225 53l0 192 360-322-360-334 0 218z" horiz-adv-x="900" />
+
+<glyph glyph-name="quote" unicode="&#xe833;" d="M146 680q146 0 184-146 38-140-40-302-80-168-224-204-32-8-66-8l0 70q112 0 182 108 54 86 26 146-16 36-62 36-60 0-103 44t-43 106 43 106 103 44z m420 0q146 0 184-146 38-140-40-302-80-168-224-204-32-8-66-8l0 70q112 0 182 108 54 86 26 146-16 36-62 36-60 0-103 44t-43 106 43 106 103 44z" horiz-adv-x="762" />
+
+<glyph glyph-name="code" unicode="&#xe834;" d="M380 636q16-14 16-32t-16-30l-246-224 246-226q16-12 16-30t-16-32q-30-30-60 0l-320 288 320 286q30 30 60 0z m302 0l318-286-318-288q-32-30-62 0-32 32 0 62l248 226-248 224q-32 30 0 62 30 30 62 0z" horiz-adv-x="1000" />
+
+<glyph glyph-name="export" unicode="&#xe835;" d="M750 60l0 56 100 82 0-188q0-20-15-35t-35-15l-750 0q-20 0-35 15t-15 35l0 550q0 22 14 36t36 14l288 0q-32-24-59-49t-39-39l-10-12-130 0 0-450 650 0z m-82 348q-166 0-242-41t-160-181q0 8 1 22t9 56 22 79 44 83 70 79 107 56 149 23l0 156 332-250-332-260 0 178z" horiz-adv-x="1000" />
+
+<glyph glyph-name="pencil" unicode="&#xe836;" d="M718 680q32-32 47-64t15-48l0-16-252-252-290-288-238-52 50 240 290 288 252 252q54 12 126-60z m-494-640l24 24q-2 44-52 94-22 22-45 35t-35 13l-14 2-22-24-18-80q28-16 46-34 24-24 36-48z" horiz-adv-x="780" />
+
+<glyph glyph-name="feather" unicode="&#xe837;" d="M60-138q-6-20-26-8-18 8-16 34 4 100 50 226-100 154-52 316 10-32 32-78t44-80 32-30q8 4 0 83t-11 166 25 157q22 44 80 94t104 70q-24-46-33-94t-4-78 21-32q12 0 84 120t106 122q46 4 114-29t82-65q12-24 0-79t-40-83q-44-44-146-62t-114-24q-16-10 12-34 54-48 176-20-56-80-136-114t-132-38-54-10q-4-24 49-54t101-14q-30-56-63-84t-54-35-76-11-85-8z" horiz-adv-x="698" />
+
+<glyph glyph-name="print" unicode="&#xe838;" d="M66 526q-26 0-22 22 4 10 12 14 2 0 49 17t93 32 58 15l44 0 0 150 380 0 0-150 46 0q12 0 57-15t92-32 49-17q18-8 12-26-4-10-20-10l-850 0z m860-56q20 0 37-19t17-41l0-174q0-22-17-41t-37-19l-100 0 44-250-760 0 44 250-98 0q-20 0-38 19t-18 41l0 174q0 22 18 41t38 19l870 0z m-716-444l560 0-70 324-420 0z" horiz-adv-x="980" />
+
+<glyph glyph-name="retweet" unicode="&#xe839;" d="M250 190l272 0 128-140-448 0q-42 0-71 30t-29 70l0 302-102 0 176 198 174-198-100 0 0-262z m650 60l100 0-174-200-176 200 102 0 0 260-274 0-128 140 450 0q40 0 70-29t30-71l0-300z" horiz-adv-x="1000" />
+
+<glyph glyph-name="keyboard" unicode="&#xe83a;" d="M930 650q28 0 49-21t21-49l0-460q0-30-21-50t-49-20l-860 0q-28 0-49 20t-21 50l0 460q0 28 21 49t49 21l860 0z m-380-100l0-100 100 0 0 100-100 0z m150-150l-100 0 0-100 100 0 0 100z m-300 150l0-100 100 0 0 100-100 0z m150-150l-100 0 0-100 100 0 0 100z m-300 150l0-100 100 0 0 100-100 0z m150-150l-100 0 0-100 100 0 0 100z m-300 150l0-100 100 0 0 100-100 0z m150-150l-100 0 0-100 100 0 0 100z m-50-250l0 100-100 0 0-100 100 0z m550 0l0 100-500 0 0-100 500 0z m150 0l0 100-100 0 0-100 100 0z m-150 150l100 0 0 100-100 0 0-100z m150 150l0 100-200 0 0-100 200 0z" horiz-adv-x="1000" />
+
+<glyph glyph-name="comment" unicode="&#xe83b;" d="M700 700q42 0 71-29t29-71l0-350q0-40-29-70t-71-30l-200 0 0-150-200 150-200 0q-40 0-70 30t-30 70l0 350q0 42 30 71t70 29l600 0z" horiz-adv-x="800" />
+
+<glyph glyph-name="chat" unicode="&#xe83c;" d="M290 240l350 0q2 0 6 2l4 0 0-92q0-40-29-70t-71-30l-250 0-150-150 0 150-50 0q-40 0-70 30t-30 70l0 300q0 42 30 71t70 29l190 0 0-310z m610 560q42 0 71-29t29-71l0-300q0-40-29-70t-71-30l-50 0 0-150-150 150-350 0 0 400q0 42 30 71t70 29l450 0z" horiz-adv-x="1000" />
+
+<glyph glyph-name="bell" unicode="&#xe83d;" d="M632 426q16-34 40-52t45-22 44-23 35-55q22-62-74-161t-252-157q-164-58-297-45t-155 75q-20 54 12 111t18 111q-56 192-47 300t113 192q26 22 29 51t29 39q24 8 46-12t56-18q132 2 198-66t160-268z m-186-404q88 32 159 85t100 91 25 50q-8 22-49 33t-124 1-187-48q-102-38-173-87t-94-84-17-53q4-12 50-22t134-4 176 38z m-62 174q8 2 21 7t17 7l2-2q14-40-17-83t-89-63q-96-36-152 14 78 68 218 120z" horiz-adv-x="800" />
+
+<glyph glyph-name="attention" unicode="&#xe83e;" d="M957-24q10-16 0-34-10-16-30-16l-892 0q-18 0-28 16-13 18-2 34l446 782q8 18 30 18t30-18z m-420 50l0 100-110 0 0-100 110 0z m0 174l0 300-110 0 0-300 110 0z" horiz-adv-x="962" />
+
+<glyph glyph-name="alert" unicode="&#xe83f;" d="M885 234q20-16 16-33t-28-23l-78-22q-24-6-40-28t-14-48l4-82q2-24-14-34t-38 0l-86 44q-22 12-47 4t-35-30l-46-88q-12-22-29-23t-33 19l-50 78q-34 48-88 20l-122-70q-22-14-32-6t-2 32l54 164q8 24-4 44t-36 22l-106 12q-24 4-29 18t15 30l86 76q20 16 20 41t-20 41l-86 76q-20 16-16 33t28 23l78 22q24 6 41 28t15 48l-6 82q0 26 15 36t37 0l80-38q24-10 49-2t37 30l46 80q12 22 30 21t30-23l50-86q12-22 35-29t45 7l136 84q22 14 30 6t0-32l-60-170q-10-22 2-41t38-21l114-12q26-2 30-16t-16-30l-86-76q-18-16-18-41t18-41z m-384-92l0 104-100 0 0-104 100 0z m0 160l0 260-100 0 0-260 100 0z" horiz-adv-x="901" />
+
+<glyph glyph-name="vcard" unicode="&#xe840;" d="M900 750q42 0 71-29t29-71l0-600q0-40-29-70t-71-30l-800 0q-40 0-70 30t-30 70l0 600q0 42 30 71t70 29l800 0z m0-700l0 600-800 0 0-600 800 0z m-450 196l0-90-250 0 0 90 250 0z m0 150l0-90-250 0 0 90 250 0z m0 150l0-90-250 0 0 90 250 0z m346-320l4-70-250 0q0 70 6 70 84 22 84 66 0 16-27 56t-27 88q0 110 90 110t90-110q0-48-28-88t-28-56q0-20 21-36t43-22z" horiz-adv-x="1000" />
+
+<glyph glyph-name="address" unicode="&#xe841;" d="M426 800q20 0 20-20l0-860q0-20-20-20l-46 0q-20 0-20 20l0 440-176 0q-16 0-28 6-12 2-26 12l-120 82q-10 6-10 16t10 16l120 82q14 10 26 12 8 4 28 4l176 0 0 190q0 20 20 20l46 0z m564-208q10-6 10-16t-10-16l-118-82q-22-12-26-12-14-6-28-6l-302 0-40 230 342 0q18 0 28-4t26-12z" horiz-adv-x="1000" />
+
+<glyph glyph-name="location" unicode="&#xe842;" d="M250 750q104 0 177-73t73-177q0-106-62-243t-126-223l-62-84q-10 12-27 35t-60 89-76 130-60 147-27 149q0 104 73 177t177 73z m0-388q56 0 96 40t40 96-40 95-96 39-95-39-39-95 39-96 95-40z" horiz-adv-x="500" />
+
+<glyph glyph-name="map" unicode="&#xe843;" d="M984 600q16-10 16-30l0-584q0-20-16-30-8-6-16-6t-18 6l-216 136-216-136q-18-10-34 0l-218 136-216-136q-16-10-34 0-16 10-16 30l0 584q0 20 16 30l234 146q18 10 34 0l216-136 218 136q16 10 32 0z m-750-450l0 506-168-104 0-506z m234-104l0 506-168 104 0-506z m234 104l0 506-170-104 0-506z m232-104l0 506-168 104 0-506z" horiz-adv-x="1000" />
+
+<glyph glyph-name="direction" unicode="&#xe844;" d="M848 768q8-8 11-16t-2-22-10-26-19-39-24-49q-54-112-147-286t-157-292l-66-118-54 380-380 56q442 246 696 368 20 10 48 25t39 20 25 9 23 1 17-11z m-92-96l-304-280 28-234z" horiz-adv-x="860" />
+
+<glyph glyph-name="compass" unicode="&#xe845;" d="M474 830q198 2 340-136t146-336q2-200-136-342t-338-146q-198-2-341 137t-145 337q-4 200 135 342t339 144z m12-858q156 2 266 114t108 270-115 267-269 107q-158-2-267-114t-107-270 114-267 270-107z m-234 154q4 26 12 66t41 128 77 132 125 76 141 42l60 10q-4-26-12-66t-41-128-77-132q-42-42-124-74t-142-42z m180 276q-22-20-22-48t22-50q20-22 49-22t49 22q52 52 88 186-136-36-186-88z" horiz-adv-x="960" />
+
+<glyph glyph-name="cup" unicode="&#xe846;" d="M340 760q152 0 249-41t91-87l-72-594q-2-14-34-36t-97-42-137-20-136 20-97 42-35 36l-72 594q-4 28 36 57t121 50 183 21z m0-216q72 0 137 15t98 33 33 30-33 29-98 32-137 15-137-15-98-32-33-29 33-30 98-33 137-15z" horiz-adv-x="681" />
+
+<glyph glyph-name="trash" unicode="&#xe847;" d="M50 458q122-70 330-70t330 70l-54-486q-2-14-35-36t-100-43-141-21-140 21-100 43-36 36z m488 300q94-18 158-55t64-71l0-10q0-58-112-99t-268-41-268 41-112 99l0 10q0 34 64 71t158 55l42 48q22 26 70 26l92 0q52 0 70-26z m-54-112l84 0q-92 110-104 126-14 16-32 16l-102 0q-22 0-32-16l-106-126 84 0 64 66 82 0z" horiz-adv-x="760" />
+
+<glyph glyph-name="doc" unicode="&#xe848;" d="M600 800q42 0 71-29t29-71l0-700q0-40-29-70t-71-30l-500 0q-40 0-70 30t-30 70l0 700q0 42 30 71t70 29l500 0z m0-800l0 700-500 0 0-700 500 0z" horiz-adv-x="700" />
+
+<glyph glyph-name="docs" unicode="&#xe849;" d="M970 480q38-10 30-46l-150-556q-4-16-18-23t-30-3l-406 110q-16 4-24 18t-4 28l24 92-180-48q-40-10-50 26l-160 602q-10 36 28 48l454 122q16 4 30-3t18-23l66-244z m-888 190l144-542 392 106-144 540z m702-742l132 492-298 82 76-282q10-34-28-46l-196-52-26-102z" horiz-adv-x="1001" />
+
+<glyph glyph-name="doc-landscape" unicode="&#xe84a;" d="M0 600q0 42 30 71t70 29l800 0q42 0 71-29t29-71l0-500q0-40-29-70t-71-30l-800 0q-40 0-70 30t-30 70l0 500z m900 0l-800 0 0-500 800 0 0 500z" horiz-adv-x="1000" />
+
+<glyph glyph-name="doc-text" unicode="&#xe84b;" d="M212 308l0 90 280 0 0-90-280 0z m388 492q42 0 71-29t29-71l0-700q0-40-29-70t-71-30l-500 0q-40 0-70 30t-30 70l0 700q0 42 30 71t70 29l500 0z m0-800l0 700-500 0 0-700 500 0z m-110 592l0-88-280 0 0 88 280 0z m0-392l0-88-280 0 0 88 280 0z" horiz-adv-x="700" />
+
+<glyph glyph-name="doc-text-inv" unicode="&#xe84c;" d="M600 800q42 0 71-29t29-71l0-700q0-40-29-70t-71-30l-500 0q-40 0-70 30t-30 70l0 700q0 42 30 71t70 29l500 0z m-460-208l0-88 420 0 0 88-420 0z m420-480l0 88-420 0 0-88 420 0z m0 196l0 90-418 0 0-90 418 0z" horiz-adv-x="700" />
+
+<glyph glyph-name="newspaper" unicode="&#xe84d;" d="M700 800q42 0 71-29t29-71l0-700q0-40-29-70t-71-30l-600 0q-40 0-70 30t-30 70l0 700q0 42 30 71t70 29l600 0z m0-800l0 700-600 0 0-700 600 0z m-250 250l0-50-250 0 0 50 250 0z m150 200l0-50-200 0 0 50 200 0z m-200 50l0 100 200 0 0-100-200 0z m-50 100l0-200-150 0 0 200 150 0z m-50-250l0-50-100 0 0 50 100 0z m50-50l0 50 250 0 0-50-250 0z m250-150l0-50-400 0 0 50 400 0z m-100 50l0 50 100 0 0-50-100 0z" horiz-adv-x="800" />
+
+<glyph glyph-name="book-open" unicode="&#xe84e;" d="M340 238l0-68-200 80 0 68z m0 208l0-68-200 80 0 68z m538 346q22-12 22-42l0-640q0-34-32-46l-398-160q-8-2-10-2t-5-1-5-1-5 1-5 1l-10 2-398 160q-32 12-32 46l0 640q0 30 22 42 22 16 46 6l382-154 382 154q24 10 46-6z m-478-788l0 560-320 128 0-560z m420 128l0 560-320-128 0-560z m-60 186l0-68-200-80 0 68z m0 208l0-68-200-80 0 68z" horiz-adv-x="900" />
+
+<glyph glyph-name="book" unicode="&#xe84f;" d="M682 594q18-8 18-28l0-562q0-14-12-25t-28-11q-46 0-46 36l0 522q0 12-12 18l-404 216q-32 10-68-10-44-20-56-44l408-228q18-8 18-28l0-550q0-22-18-28-6-4-16-4-14 0-20 4-8 6-202 127t-212 131q-26 18-26 34l-6 524q0 28 14 52 28 46 102 77t116 9z" horiz-adv-x="700" />
+
+<glyph glyph-name="folder" unicode="&#xe850;" d="M954 500q32 0 40-12t6-36l-42-452q-2-24-12-37t-42-13l-806 0q-52 0-56 50l-42 452q-2 24 6 36t40 12l908 0z m-34 110l10-40-846 0 14 132q4 20 20 34t36 14l164 0q52 0 86-34l30-30q32-36 86-36l340 0q20 0 38-12t22-28z" horiz-adv-x="1001" />
+
+<glyph glyph-name="archive" unicode="&#xe851;" d="M840 600l0-50-696 0 0 50q0 22 13 35t25 15l608 0q6 0 14-1t22-14 14-35z m-148 150q6 0 14-1t22-14 14-35l-498 0q0 22 13 35t25 15l410 0z m248-200q34-32 38-46 6-18 0-54l-76-450q-4-22-20-35t-28-15l-710 0q-52 0-60 50-6 26-39 223t-39 227q-10 22-3 44t10 26 21 20l10 10 30 30 0-80 836 0 0 80z m-248-270l0 100-70 0 0-80-260 0 0 80-68 0 0-100q0-50 48-50l300 0q22 0 35 12t13 24z" horiz-adv-x="981" />
+
+<glyph glyph-name="box" unicode="&#xe852;" d="M870 750q12 0 21-9t9-21l0-120-900 0 0 120q0 12 9 21t21 9l840 0z m-820-730l0 530 800 0 0-530q0-30-21-50t-49-20l-660 0q-28 0-49 20t-21 50z m250 430l0-100 300 0 0 100-300 0z" horiz-adv-x="900" />
+
+<glyph glyph-name="rss" unicode="&#xe853;" d="M0 730q314 0 537-223t223-537l-118 0q0 266-188 453t-454 187l0 120z m0-238q218 0 371-153t153-369l-118 0q0 166-119 285t-287 119l0 118z m114-296q46 0 80-33t34-81q0-46-34-79t-80-33-80 33-34 79q0 48 34 81t80 33z" horiz-adv-x="760" />
+
+<glyph glyph-name="phone" unicode="&#xe854;" d="M461 290q162 162 118 206l-8 8q-30 30-41 48t-4 54 49 88q20 24 37 39t35 16 30 1 29-13 24-18 26-25 21-22q48-48-6-194t-204-294q-150-150-295-205t-193-7q-2 2-23 22t-25 25-18 24-13 31 2 30 15 35 38 37q42 34 70 47t54 2 35-18 39-37q44-44 208 120z" horiz-adv-x="800" />
+
+<glyph glyph-name="cog" unicode="&#xe855;" d="M760 350q0-72 80-122-12-40-34-82-70 18-136-44-54-58-34-136-40-20-84-36-46 82-132 82t-132-82q-44 16-84 36 20 80-34 136-54 54-136 34-14 26-34 82 82 52 82 132 0 72-82 124 20 56 34 82 74-18 136 44 54 56 34 136 42 22 84 34 46-80 132-80t132 80q42-12 84-34-20-78 34-136 66-62 136-44 22-42 34-82-80-50-80-124z m-340-182q76 0 129 53t53 129-53 130-129 54-129-54-53-130 53-129 129-53z" horiz-adv-x="840" />
+
+<glyph glyph-name="tools" unicode="&#xe856;" d="M155 506q-8-8-11-22t-3-25-2-11q-2-2-17-15t-19-17q-16-14-28 4l-70 76q-11 12 2 24 2 2 18 14t20 16q6 6 27 6t37 14q14 14 18 38t10 30q2 0 9 7t26 22 41 31q134 90 186 96 122 0 148-2 12 0-8-8-120-52-152-76-80-56-36-114 34-46 38-48 8-8-2-14-2-2-38-35t-38-35q-14-8-18-4-42 48-71 60t-67-12z m286-26l410-476q18-22-2-38l-48-42q-22-14-38 4l-414 472q-8 8 0 20l72 62q12 8 20-2z m554 202q16-104-16-166-50-88-154-62-56 12-100-32l-82-78-68 78 68 70q24 24 31 53t6 65 5 58q12 56 140 112 12 6 18-3t2-15q-12-12-46-80-14-10-12-35t40-53q58-40 96 22 6 12 26 41t22 33q4 10 13 9t11-17z m-858-684l254 248 76-86-246-242q-20-20-38-4l-46 46q-22 18 0 38z" horiz-adv-x="1000" />
+
+<glyph glyph-name="share" unicode="&#xe857;" d="M650 200q62 0 106-43t44-107q0-62-44-106t-106-44-106 44-44 106q0 6 1 14t1 12l-260 156q-42-32-92-32-62 0-106 44t-44 106 44 106 106 44q54 0 92-30l260 156q0 4-1 12t-1 12q0 62 44 106t106 44 106-43 44-107q0-62-44-106t-106-44q-52 0-90 32l-262-156q2-8 2-26 0-16-2-24l262-156q36 30 90 30z" horiz-adv-x="800" />
+
+<glyph glyph-name="shareable" unicode="&#xe858;" d="M340 350q0 68 47 114t113 46 113-46 47-114q0-66-47-113t-113-47-113 47-47 113z m-114 60q-14-60-66-60l-160 0 0 120 118 0q40 124 145 202t237 78q164 0 284-116 16-18 16-43t-16-43q-18-16-43-16t-43 16q-78 82-198 82-100 0-176-62t-98-158z m614-60l160 0 0-120-118 0q-40-124-144-202t-238-78q-164 0-282 118-18 18-18 43t18 41q16 18 41 18t43-18q82-82 198-82 100 0 176 63t98 157q12 60 66 60z" horiz-adv-x="1000" />
+
+<glyph glyph-name="basket" unicode="&#xe859;" d="M150 0q0 40 30 70t70 30q42 0 71-30t29-70q0-42-29-71t-71-29q-40 0-70 29t-30 71z m500 0q0 40 30 70t70 30q42 0 71-30t29-70q0-42-29-71t-71-29q-40 0-70 29t-30 71z m-322 236q-36-10-34-23t44-13l562 0 0-76q0-20-20-20l-654 0q-20 0-20 20l0 76-10 46-98 454-98 0 0 80q0 20 20 20l156 0q20 0 20-20l0-86 704 0 0-274q0-22-18-26z" horiz-adv-x="900" />
+
+<glyph glyph-name="bag" unicode="&#xe85a;" d="M835 668q28-26 24-60l-98-648q-8-30-38-30l-586 0q-28 0-40 30-94 620-96 648-5 34 22 60 6 6 54 43t56 43q18 16 56 16l480 0q38 0 56-16 78-58 110-86z m-406-436q56 0 98 34t63 89 30 89 13 66l-92 0q-38-188-112-188t-112 188l-92 0q46-278 204-278z m-352 368l704 0-110 116-484 0z" horiz-adv-x="859" />
+
+<glyph glyph-name="calendar" unicode="&#xe85b;" d="M800 700q42 0 71-29t29-71l0-600q0-40-29-70t-71-30l-700 0q-40 0-70 30t-30 70l0 600q0 42 30 71t70 29l46 0 0-100 160 0 0 100 290 0 0-100 160 0 0 100 44 0z m0-700l0 400-700 0 0-400 700 0z m-540 800l0-170-70 0 0 170 70 0z m450 0l0-170-70 0 0 170 70 0z" horiz-adv-x="900" />
+
+<glyph glyph-name="login" unicode="&#xe85c;" d="M800 800q42 0 71-29t29-71l0-700q0-40-29-70t-71-30l-450 0q-40 0-69 30t-29 70l0 100 98 0 0-100 450 0 0 700-450 0 0-150-98 0 0 150q0 42 29 71t69 29l450 0z m-350-670l0 120-450 0 0 150 450 0 0 120 200-194z" horiz-adv-x="900" />
+
+<glyph glyph-name="logout" unicode="&#xe85d;" d="M502 0l0 100 98 0 0-100q0-40-29-70t-71-30l-400 0q-40 0-70 30t-30 70l0 700q0 42 30 71t70 29l400 0q42 0 71-29t29-71l0-150-98 0 0 150-402 0 0-700 402 0z m398 326l-198-196 0 120-450 0 0 150 450 0 0 120z" horiz-adv-x="900" />
+
+<glyph glyph-name="mic" unicode="&#xe85e;" d="M620 488q20 0 20-20l0-138q0-92-69-164t-201-84l0-132 130 0q20 0 20-20l0-60q0-20-20-20l-360 0q-20 0-20 20l0 60q0 20 20 20l130 0 0 132q-132 12-201 84t-69 164l0 138q0 20 20 20l30 0q20 0 20-20l0-138q0-66 59-123t191-57 191 57 59 123l0 138q0 20 20 20l30 0z m-300-238q-80 0-115 25t-35 55l0 158 300 0 0-158q0-30-35-55t-115-25z m150 520l0-212-300 0 0 212q0 30 35 55t115 25 115-25 35-55z" horiz-adv-x="640" />
+
+<glyph glyph-name="mute" unicode="&#xe85f;" d="M868 778q16-16 16-36t-16-36l-782-782q-18-14-34-14-18 0-36 14-16 14-16 36t16 36l782 782q34 32 70 0z m-216-386l50 50q74-92 101-172t-7-116q-24-24-75-57t-131-71-161-45-165 23l278 276q44-32 88-54t67-25 33 1q6 10 2 34t-26 68-54 88z m-276 62l-270-270q-40 132 28 283t132 215q34 32 105 11t159-85l-52-50q-58 38-105 53t-57 5q-4-8-2-28t19-58 43-76z" horiz-adv-x="884" />
+
+<glyph glyph-name="sound" unicode="&#xe860;" d="M176 588q42 42 149-5t217-157 157-217 5-149q-28-28-92-67t-156-78-194-29-176 84-84 176 29 194 78 156 67 92z m464-480q8 10-3 49t-49 101-96 118q-56 58-118 96t-101 49-49 3q-8-10 3-49t49-101 94-120q58-56 120-94t101-49 49-3z m6 394q-18 0-34 16-16 14-16 35t16 35l94 96q36 32 72 0 32-36 0-72l-96-94q-16-16-36-16z m-180 124q-18 10-23 30t5 38l54 96q26 44 68 20 18-10 23-30t-5-38l-54-96q-14-26-42-26-14 0-26 6z m438-150q10-18 4-38t-24-30l-96-54q-16-8-24-8-28 0-44 26-10 18-4 38t24 30l96 54q18 10 38 5t30-23z" horiz-adv-x="910" />
+
+<glyph glyph-name="volume" unicode="&#xe861;" d="M896 180q0-34-24-57t-56-23l-780 0q-22 0-31 5t-3 15 24 20l802 452q28 18 48 7t20-45l0-374z" horiz-adv-x="896" />
+
+<glyph glyph-name="clock" unicode="&#xe862;" d="M460 810q190 0 325-135t135-325-135-325-325-135-325 135-135 325 135 325 325 135z m0-820q150 0 255 106t105 254q0 150-105 255t-255 105q-148 0-254-105t-106-255q0-148 106-254t254-106z m36 620l0-244 150-150-50-50-170 170 0 274 70 0z" horiz-adv-x="920" />
+
+<glyph glyph-name="hourglass" unicode="&#xe863;" d="M560 622q0-44-48-96t-97-99-49-77 49-76 97-97 48-97l0-118q0-34-86-73t-194-39-194 39-86 73l0 118q0 46 48 97t97 97 49 76-49 77-97 99-48 96l0 118q0 32 87 71t193 39 193-39 87-71l0-118z m-482 112l-18-14q-4-8 4-14 92-52 216-52 132 0 220 50 14 10-16 30-96 54-202 54-120 0-204-54z m228-384q0 18 4 33t18 33 20 25 31 31 29 28q92 92 92 122l2 50q-100-54-222-54t-222 54l4-50q0-32 90-122 6-6 22-21t23-22l19-19t17-21 11-20 9-23 3-24q0-10-1-19t-6-18-8-16-11-17l-12-15t-15-16-16-15-18-16-17-16q-90-90-90-122l0-66q8 4 66 23t92 43 34 58q0 30 26 30t26-30q0-34 33-58t94-43 67-23l0 66q0 30-92 122-4 4-21 20t-22 21-18 19-18 22-12 20-9 23-2 23z" horiz-adv-x="560" />
+
+<glyph glyph-name="lamp" unicode="&#xe864;" d="M209-110l0 104 282 0 0-104q-70-42-142-40-70-2-140 40z m276 164l-270 0q0 72-36 140t-78 113-74 112-26 139q8 120 94 206t254 86q170 0 255-86t95-206q4-60-16-113t-52-96-65-85-57-96-24-114z m-378 496q-4-4 0-20t2-20 5-19 6-18 8-18 11-19 13-19 14-19 15-21 16-23q88-122 112-212l82 0q24 94 112 212 4 6 25 35t25 36 17 29 16 33 6 28 1 35q-16 196-244 196-226 0-242-196z" horiz-adv-x="700" />
+
+<glyph glyph-name="light-down" unicode="&#xe865;" d="M350 510q68 0 114-47t46-113q0-68-46-114t-114-46q-66 0-113 46t-47 114q0 66 47 113t113 47z m0-264q44 0 73 30t29 74q0 42-29 72t-73 30q-42 0-72-30t-30-72q0-44 30-74t72-30z m-300 144q20 0 35-12t15-28q0-40-50-40t-50 40q0 16 15 28t35 12z m546 204q28-28-8-64-14-14-33-16t-29 10q-12 12-10 31t16 33q36 34 64 6z m54-204q20 0 35-12t15-28q0-40-50-40-48 0-48 40 0 16 14 28t34 12z m-300-290q16 0 28-15t12-35-12-35-28-15-28 15-12 35 12 35 28 15z m-238 62q36 36 64 8t-8-64q-14-14-33-16t-29 8q-30 28 6 64z m-10 430q28 28 64-8 14-14 16-33t-8-29q-30-28-64 6-36 36-8 64z m432-484q-34 36-6 64t64-8q14-14 16-33t-10-29q-30-28-64 6z m-184 492q-16 0-28 15t-12 35 12 35 28 15 28-15 12-35-12-35-28-15z" horiz-adv-x="700" />
+
+<glyph glyph-name="light-up" unicode="&#xe866;" d="M950 390q20 0 35-12t15-28q0-40-50-40l-48 0q-50 0-50 40 0 16 15 28t35 12l48 0z m-450 234q114 0 195-80t81-194q0-116-81-196t-195-80-194 80-80 196q0 114 80 194t194 80z m0-474q82 0 141 58t59 142q0 82-59 141t-141 59-141-59-59-141q0-84 59-142t141-58z m-350 200q0-40-50-40l-50 0q-50 0-50 40 0 16 15 28t35 12l50 0q20 0 35-12t15-28z m350 350q-16 0-28 15t-12 35l0 50q0 20 12 35t28 15 28-15 12-35l0-50q0-20-12-35t-28-15z m0-700q16 0 28-15t12-35l0-50q0-20-12-35t-28-15-28 15-12 35l0 50q0 20 12 35t28 15z m368 660l-34-34q-34-34-64-8-28 28 8 64 4 6 34 36 36 34 64 6t-8-64z m-700-588q14 16 33 18t29-10q12-12 10-31t-16-33l-36-36q-14-14-33-16t-29 10q-30 28 6 64 6 4 36 34z m20 646l36-36q36-36 6-64-10-10-29-8t-33 16q-30 30-36 34-14 14-16 33t10 31q10 12 29 10t33-16z m590-702q-36 36-8 64t64-8l34-34q36-36 8-64t-64 6q-30 30-34 36z" horiz-adv-x="1000" />
+
+<glyph glyph-name="adjust" unicode="&#xe867;" d="M950 390q20 0 35-12t15-28q0-40-50-40l-48 0q-50 0-50 40 0 16 15 28t35 12l48 0z m-450 234q114 0 195-80t81-194q0-116-81-196t-195-80-194 80-80 196q0 114 80 194t194 80z m6-474l0 400q-86 0-146-59t-60-141q0-84 60-142t146-58z m-356 200q0-40-50-40l-50 0q-50 0-50 40 0 16 15 28t35 12l50 0q20 0 35-12t15-28z m350 350q-16 0-28 15t-12 35l0 50q0 20 12 35t28 15 28-15 12-35l0-50q0-20-12-35t-28-15z m0-700q16 0 28-15t12-35l0-50q0-20-12-35t-28-15-28 15-12 35l0 50q0 20 12 35t28 15z m368 660l-34-34q-34-34-64-8-28 28 8 64 4 6 34 36 36 34 64 6t-8-64z m-700-588q14 16 33 18t29-10q12-12 10-31t-16-33l-36-36q-14-14-33-16t-29 10q-30 28 6 64 6 4 36 34z m20 646l36-36q36-36 6-64-10-10-29-8t-33 16q-30 30-36 34-14 14-16 33t10 31q10 12 29 10t33-16z m590-702q-36 36-8 64t64-8l34-34q36-36 8-64t-64 6q-30 30-34 36z" horiz-adv-x="1000" />
+
+<glyph glyph-name="block" unicode="&#xe868;" d="M480 830q200 0 340-140t140-340q0-198-140-339t-340-141q-198 0-339 141t-141 339q0 200 141 340t339 140z m258-220z m-622-260q0-132 82-230l514 514q-100 82-232 82-152 0-258-107t-106-259z m106-258z m258-106q152 0 259 107t107 257q0 130-82 232l-514-514q98-82 230-82z" horiz-adv-x="960" />
+
+<glyph glyph-name="resize-full" unicode="&#xe869;" d="M476 746l316 0 0-316-100 124-146-152-100 100 152 146z m-230-444l100-100-152-146 122-100-316 0 0 316 100-122z" horiz-adv-x="792" />
+
+<glyph glyph-name="resize-small" unicode="&#xe86a;" d="M156 146l-106 100 296 0 0-296-100 106-146-156-100 100z m744 554l-154-144 104-100-294 0 0 294 100-104 144 154z" horiz-adv-x="900" />
+
+<glyph glyph-name="popup" unicode="&#xe86b;" d="M700 750q42 0 71-29t29-71l0-400q0-40-29-70t-71-30l-400 0q-40 0-70 30t-30 70l0 402q0 40 29 69t71 29l400 0z m0-500l0 400-400 0 0-400 400 0z m-600 100l0-300 300 0 0-100-300 0q-40 0-70 30t-30 70l0 300 100 0z" horiz-adv-x="800" />
+
+<glyph glyph-name="publish" unicode="&#xe86c;" d="M900 800q42 0 71-30t29-70l0-600q0-42-29-71t-71-29l-198 0 0 98 200 0 0 462-802 0 0-462 200 0 0-98-200 0q-40 0-70 29t-30 71l0 600q0 40 30 70t70 30l800 0z m-770-168q38 0 38 38 0 16-11 26t-27 10-27-11-11-25q0-16 11-27t27-11z m100 0q38 0 38 38 0 16-11 26t-27 10-27-11-11-25q0-16 11-27t27-11z m672 6l0 62-602 0 0-62 602 0z m-404-198l242-240-150 0 0-300-184 0 0 300-150 0z" horiz-adv-x="1000" />
+
+<glyph glyph-name="window" unicode="&#xe86d;" d="M900 750q42 0 71-30t29-70l0-600q0-42-29-71t-71-29l-800 0q-40 0-70 29t-30 71l0 600q0 40 30 70t70 30l800 0z m-670-94q-16 0-27-11t-11-25q0-16 11-27t27-11q38 0 38 38 0 16-11 26t-27 10z m-138-36q0-16 11-27t27-11q38 0 38 38 0 16-11 26t-27 10-27-11-11-25z m810-570l0 460-802 0 0-460 802 0z m0 540l0 60-602 0 0-60 602 0z" horiz-adv-x="1000" />
+
+<glyph glyph-name="arrow-combo" unicode="&#xe86e;" d="M230 850l230-364-460 0z m0-1000l-230 366 460 0z" horiz-adv-x="460" />
+
+<glyph glyph-name="down-circled" unicode="&#xe86f;" d="M460 810q190 0 325-135t135-325-135-325-325-135-325 135-135 325 135 325 325 135z m0-820q148 0 254 106t106 254q0 150-106 255t-254 105-254-105-106-255q0-148 106-254t254-106z m90 554l0-206 112 0-202-190-202 190 112 0 0 206 180 0z" horiz-adv-x="920" />
+
+<glyph glyph-name="left-circled" unicode="&#xe870;" d="M920 350q0-190-135-325t-325-135-325 135-135 325q0 192 135 326t325 134 325-134 135-326z m-820 0q0-148 106-254t254-106 254 106 106 254q0 150-106 255t-254 105-254-105-106-255z m552-90l-204 0 0-112-190 202 190 204 0-114 204 0 0-180z" horiz-adv-x="920" />
+
+<glyph glyph-name="right-circled" unicode="&#xe871;" d="M0 350q0 190 135 325t325 135 325-135 135-325-135-325-325-135-325 135-135 325z m820 0q0 150-105 255t-255 105q-148 0-254-105t-106-255q0-148 106-254t254-106q150 0 255 106t105 254z m-552 90l204 0 0 114 190-204-190-202 0 112-204 0 0 180z" horiz-adv-x="920" />
+
+<glyph glyph-name="up-circled" unicode="&#xe872;" d="M460-110q-190 0-325 135t-135 325q0 192 135 326t325 134 325-134 135-326q0-190-135-325t-325-135z m0 820q-148 0-254-105t-106-255q0-148 106-254t254-106q150 0 255 106t105 254q0 150-105 255t-255 105z m-90-552l0 204-112 0 202 192 202-192-112 0 0-204-180 0z" horiz-adv-x="920" />
+
+<glyph glyph-name="down-open" unicode="&#xe873;" d="M564 422l-234-224q-18-18-40-18t-40 18l-234 224q-16 16-16 41t16 41q38 38 78 0l196-188 196 188q40 38 78 0 16-16 16-41t-16-41z" horiz-adv-x="580" />
+
+<glyph glyph-name="left-open" unicode="&#xe874;" d="M242 626q14 16 39 16t41-16q38-36 0-80l-186-196 186-194q38-44 0-80-16-16-40-16t-40 16l-226 236q-16 16-16 38 0 24 16 40 206 214 226 236z" horiz-adv-x="341" />
+
+<glyph glyph-name="right-open" unicode="&#xe875;" d="M98 626l226-236q16-16 16-40 0-22-16-38l-226-236q-16-16-40-16t-40 16q-36 36 0 80l186 194-186 196q-36 44 0 80 16 16 41 16t39-16z" horiz-adv-x="340" />
+
+<glyph glyph-name="up-open" unicode="&#xe876;" d="M564 280q16-16 16-41t-16-41q-38-38-78 0l-196 188-196-188q-40-38-78 0-16 16-16 41t16 41l234 224q16 16 40 16t40-16z" horiz-adv-x="580" />
+
+<glyph glyph-name="down-open-mini" unicode="&#xe877;" d="M405 470q22 26 48 0 26-22 0-48l-196-192q-22-22-48 0l-196 192q-26 26 0 48 24 24 50 0l170-156z" horiz-adv-x="466" />
+
+<glyph glyph-name="left-open-mini" unicode="&#xe878;" d="M252 180q26-26 0-48-26-26-48 0l-192 194q-24 24 0 50l192 194q22 26 48 0 26-22 0-48l-156-172z" horiz-adv-x="265" />
+
+<glyph glyph-name="right-open-mini" unicode="&#xe879;" d="M13 180l158 170-158 172q-26 26 0 48 26 26 48 0l192-194q24-26 0-50l-192-194q-22-26-48 0-26 22 0 48z" horiz-adv-x="265" />
+
+<glyph glyph-name="up-open-mini" unicode="&#xe87a;" d="M62 230q-26-22-50 0-24 24 0 50l196 190q26 26 48 0l196-190q24-26 0-50-24-22-50 0l-170 158z" horiz-adv-x="464" />
+
+<glyph glyph-name="down-open-big" unicode="&#xe87b;" d="M63 570l370-356 372 356q22 26 48 0 26-22 0-48l-396-392q-22-22-48 0l-396 392q-26 26 0 48 24 24 50 0z" horiz-adv-x="866" />
+
+<glyph glyph-name="left-open-big" unicode="&#xe87c;" d="M452-20q26-26 0-48-26-26-48 0l-392 394q-24 24 0 50l392 394q22 26 48 0 26-22 0-48l-358-372z" horiz-adv-x="465" />
+
+<glyph glyph-name="right-open-big" unicode="&#xe87d;" d="M13-20l358 370-358 372q-26 26 0 48 26 26 48 0l392-394q24-26 0-50l-392-394q-22-26-48 0-26 22 0 48z" horiz-adv-x="465" />
+
+<glyph glyph-name="up-open-big" unicode="&#xe87e;" d="M804 130l-372 358-370-358q-26-22-50 0-24 24 0 50l396 390q26 26 48 0l396-390q24-26 0-50-26-22-48 0z" horiz-adv-x="864" />
+
+<glyph glyph-name="down" unicode="&#xe87f;" d="M660 366l-330-380-330 380 192 0 0 350 276 0 0-350 192 0z" horiz-adv-x="660" />
+
+<glyph glyph-name="left" unicode="&#xe880;" d="M378 20l-378 330 378 330 0-190 352 0 0-278-352 0 0-192z" horiz-adv-x="730" />
+
+<glyph glyph-name="right" unicode="&#xe881;" d="M350 680l380-330-380-330 0 192-350 0 0 278 350 0 0 190z" horiz-adv-x="730" />
+
+<glyph glyph-name="up" unicode="&#xe882;" d="M660 336l-192 0 0-350-276 0 0 350-192 0 330 380z" horiz-adv-x="660" />
+
+<glyph glyph-name="down-dir" unicode="&#xe883;" d="M460 550l-230-400-230 400 460 0z" horiz-adv-x="460" />
+
+<glyph glyph-name="left-dir" unicode="&#xe884;" d="M400 580l0-460-400 230z" horiz-adv-x="400" />
+
+<glyph glyph-name="right-dir" unicode="&#xe885;" d="M0 580l400-230-400-230 0 460z" horiz-adv-x="400" />
+
+<glyph glyph-name="up-dir" unicode="&#xe886;" d="M0 150l230 400 230-400-460 0z" horiz-adv-x="460" />
+
+<glyph glyph-name="down-bold" unicode="&#xe887;" d="M760 366l-380-380-380 380 192 0 0 350 376 0 0-350 192 0z" horiz-adv-x="760" />
+
+<glyph glyph-name="left-bold" unicode="&#xe888;" d="M378 730l0-190 352 0 0-378-352 0 0-192-378 380z" horiz-adv-x="730" />
+
+<glyph glyph-name="right-bold" unicode="&#xe889;" d="M350 730l380-380-380-380 0 192-350 0 0 378 350 0 0 190z" horiz-adv-x="730" />
+
+<glyph glyph-name="up-bold" unicode="&#xe88a;" d="M760 336l-192 0 0-350-376 0 0 350-192 0 380 380z" horiz-adv-x="760" />
+
+<glyph glyph-name="down-thin" unicode="&#xe88b;" d="M500 100l-250-240-250 240 162 0 0 740 176 0 0-740 162 0z" horiz-adv-x="500" />
+
+<glyph glyph-name="left-thin" unicode="&#xe88c;" d="M240 100l-240 250 240 250 0-160 740 0 0-178-740 0 0-162z" horiz-adv-x="980" />
+
+<glyph glyph-name="right-thin" unicode="&#xe88d;" d="M742 100l0 162-742 0 0 178 742 0 0 160 238-250z" horiz-adv-x="980" />
+
+<glyph glyph-name="up-thin" unicode="&#xe88e;" d="M500 602l-162 0 0-742-176 0 0 742-162 0 250 238z" horiz-adv-x="500" />
+
+<glyph glyph-name="ccw" unicode="&#xe88f;" d="M532 736q170 0 289-120t119-290-119-290-289-120q-142 0-252 88l70 74q84-60 182-60 126 0 216 90t90 218-90 218-216 90q-124 0-214-87t-92-211l142 0-184-204-184 204 124 0q2 166 122 283t286 117z" horiz-adv-x="940" />
+
+<glyph glyph-name="cw" unicode="&#xe890;" d="M408 760q168 0 287-116t123-282l122 0-184-206-184 206 144 0q-4 124-94 210t-214 86q-126 0-216-90t-90-218q0-126 90-216t216-90q104 0 182 60l70-76q-110-88-252-88-168 0-288 120t-120 290 120 290 288 120z" horiz-adv-x="940" />
+
+<glyph glyph-name="arrows-ccw" unicode="&#xe891;" d="M186 140l116 116 0-292-276 16 88 86q-116 122-114 290t120 288q100 100 240 116l4-102q-100-16-172-88-88-88-90-213t84-217z m332 598l276-16-88-86q116-122 114-290t-120-288q-96-98-240-118l-2 104q98 16 170 88 88 88 90 213t-84 217l-114-116z" horiz-adv-x="820" />
+
+<glyph glyph-name="level-down" unicode="&#xe892;" d="M100 200q-42 0-71 30t-29 70l0 350 140 0 0-310 364 0 0 150 240-220-240-220 0 150-404 0z" horiz-adv-x="744" />
+
+<glyph glyph-name="level-up" unicode="&#xe893;" d="M200 350l0-90-200 160 200 170 0-100 550 0q40 0 70-29t30-71l0-280-140 0 0 240-510 0z" horiz-adv-x="850" />
+
+<glyph glyph-name="shuffle" unicode="&#xe894;" d="M754 516q-54 0-105-32t-80-66-83-104q-48-62-75-94t-78-77-107-66-122-21l-104 0 0 140 104 0q54 0 106 32t81 66 83 104q62 82 101 126t116 88 163 44l36 0 0 120 210-180-210-180 0 100-36 0z m-484-88q-74 78-166 78l-104 0 0 140 104 0q140 0 254-108-14-16-37-45t-27-33q-8-12-24-32z m520-242l0 100 210-180-210-180 0 120-36 0q-140 0-260 116 46 58 72 92 0 2 6 9t8 11q84-88 174-88l36 0z" horiz-adv-x="1000" />
+
+<glyph glyph-name="loop" unicode="&#xe895;" d="M800 540q42 0 71-29t29-71l0-290q0-40-29-70t-71-30l-700 0q-40 0-70 30t-30 70l0 290q0 42 30 71t70 29l250 0 0 110 200-180-200-180 0 110-210 0 0-210 620 0 0 210-150 0 0 140 190 0z" horiz-adv-x="900" />
+
+<glyph glyph-name="switch" unicode="&#xe896;" d="M700 592l0-140-500 0 0-90-200 160 200 170 0-100 500 0z m300-420l-200-160 0 90-500 0 0 140 500 0 0 100z" horiz-adv-x="1000" />
+
+<glyph glyph-name="play" unicode="&#xe897;" d="M486 376q14-10 14-26 0-14-14-24l-428-266q-24-16-41-6t-17 40l0 514q0 30 17 40t41-6z" horiz-adv-x="500" />
+
+<glyph glyph-name="stop" unicode="&#xe898;" d="M526 650q74 0 74-64l0-470q0-66-74-66l-450 0q-76 0-76 66l0 470q0 36 18 50t58 14l450 0z" horiz-adv-x="600" />
+
+<glyph glyph-name="pause" unicode="&#xe899;" d="M440 700q90 0 90-64l0-570q0-66-90-66t-90 66l0 570q0 64 90 64z m-350 0q90 0 90-64l0-570q0-66-90-66t-90 66l0 570q0 64 90 64z" horiz-adv-x="530" />
+
+<glyph glyph-name="record" unicode="&#xe89a;" d="M350 700q146 0 248-102t102-248q0-144-102-247t-248-103-248 103-102 247q0 146 102 248t248 102z" horiz-adv-x="700" />
+
+<glyph glyph-name="to-end" unicode="&#xe89b;" d="M412 374q14-10 14-24 0-12-14-22l-362-228q-22-14-36-5t-14 35l0 442q0 26 14 35t36-5z m114 268q74 0 74-58l0-466q0-58-74-58-76 0-76 58l0 466q0 58 76 58z" horiz-adv-x="600" />
+
+<glyph glyph-name="to-start" unicode="&#xe89c;" d="M174 350q0 14 14 24l364 228q20 14 34 5t14-35l0-442q0-26-14-35t-34 5l-364 228q-14 10-14 22z m-174 234q0 58 76 58 74 0 74-58l0-466q0-58-74-58-76 0-76 58l0 466z" horiz-adv-x="600" />
+
+<glyph glyph-name="fast-forward" unicode="&#xe89d;" d="M866 374q14-10 14-24t-14-22l-372-248q-22-14-37-6t-15 36l0 482q0 28 15 36t37-6z m-454 0q14-10 14-24t-14-22l-360-248q-20-14-36-6t-16 36l0 482q0 28 16 36t36-6z" horiz-adv-x="880" />
+
+<glyph glyph-name="fast-backward" unicode="&#xe89e;" d="M0 350q0 14 14 24l374 248q20 14 36 6t16-36l0-482q0-28-16-36t-36 6l-374 248q-14 8-14 22z m454 0q0 14 14 24l360 248q20 14 36 6t16-36l0-482q0-28-16-36t-36 6l-360 248q-14 8-14 22z" horiz-adv-x="880" />
+
+<glyph glyph-name="progress-0" unicode="&#xe89f;" d="M1000 450l0-250q0-42-29-71t-71-29l-800 0q-40 0-70 29t-30 71l0 300q0 40 30 70t70 30l800 0q42 0 71-30t29-70l0-50z m-100-250l0 300-800 0 0-300 800 0z" horiz-adv-x="1000" />
+
+<glyph glyph-name="progress-1" unicode="&#xe8a0;" d="M1000 450l0-250q0-42-29-71t-71-29l-800 0q-40 0-70 29t-30 71l0 300q0 40 30 70t70 30l800 0q42 0 71-30t29-70l0-50z m-100-250l0 300-800 0 0-300 800 0z m-750 50l0 198 200 0 0-198-200 0z" horiz-adv-x="1000" />
+
+<glyph glyph-name="progress-2" unicode="&#xe8a1;" d="M1000 450l0-250q0-42-29-71t-71-29l-800 0q-40 0-70 29t-30 71l0 300q0 40 30 70t70 30l800 0q42 0 71-30t29-70l0-50z m-100-250l0 300-800 0 0-300 800 0z m-750 50l0 198 200 0 0-198-200 0z m250 0l0 198 200 0 0-198-200 0z" horiz-adv-x="1000" />
+
+<glyph glyph-name="progress-3" unicode="&#xe8a2;" d="M1000 450l0-250q0-42-29-71t-71-29l-800 0q-40 0-70 29t-30 71l0 300q0 40 30 70t70 30l800 0q42 0 71-30t29-70l0-50z m-100-250l0 300-800 0 0-300 800 0z m-750 50l0 198 200 0 0-198-200 0z m250 0l0 198 200 0 0-198-200 0z m250 198l200 0 0-198-200 0 0 198z" horiz-adv-x="1000" />
+
+<glyph glyph-name="target" unicode="&#xe8a3;" d="M430 780q178 0 304-126t126-304-126-304-304-126-304 126-126 304 126 304 304 126z m36-778q124 14 212 102t100 212l-192 0 0 70 192 0q-12 124-100 212t-212 102l0-194-70 0 0 194q-124-14-213-102t-101-212l194 0 0-70-194 0q12-124 101-212t213-102l0 194 70 0 0-194z" horiz-adv-x="860" />
+
+<glyph glyph-name="list" unicode="&#xe8a5;" d="M100 200q20 0 35-15t15-35-15-35-35-15l-50 0q-20 0-35 15t-15 35 14 35 36 15l50 0z m0 200q20 0 35-15t15-35-15-35-35-15l-50 0q-20 0-35 15t-15 35 14 35 36 15l50 0z m0 200q20 0 35-15t15-35-15-35-35-15l-50 0q-20 0-35 15t-15 35 14 35 36 15l50 0z m200-100q-20 0-35 15t-15 35 15 35 35 15l350 0q22 0 36-15t14-35-15-35-35-15l-350 0z m350-100q22 0 36-15t14-35-15-35-35-15l-350 0q-20 0-35 15t-15 35 15 35 35 15l350 0z m0-200q22 0 36-15t14-35-15-35-35-15l-350 0q-20 0-35 15t-15 35 15 35 35 15l350 0z" horiz-adv-x="700" />
+
+<glyph glyph-name="list-add" unicode="&#xe8a6;" d="M350 400q22 0 36-15t14-35-15-35-35-15l-300 0q-20 0-35 15t-15 35 14 35 36 15l300 0z m0-200q22 0 36-15t14-35-15-35-35-15l-300 0q-20 0-35 15t-15 35 14 35 36 15l300 0z m620 200q30 0 30-50t-30-50l-170 0 0-170q0-30-50-30t-50 30l0 170-164 0q-30 0-30 50t30 50l164 0 0 170q0 30 50 30t50-30l0-170 170 0z m-620 200q22 0 36-15t14-35-15-35-35-15l-300 0q-20 0-35 15t-15 35 14 35 36 15l300 0z" horiz-adv-x="1000" />
+
+<glyph glyph-name="battery" unicode="&#xe8a9;" d="M770 350q0-98 36-157t78-59l66 0q-30-46-64-65t-118-19l-500 0q-130 0-199 94t-69 206q0 110 69 205t199 95l500 0q84 0 118-19t64-65l-66 0q-42 0-78-60t-36-156z m-136-90q10 12-8 26-136 134-178 164-16 10-26 13t-18-5-10-12-8-18l-22-56-148 66q-26 12-34 0-8-14 8-28 136-132 180-162 34-16 42-11t18 31l24 58 146-68q26-12 34 2z m310 192q22 0 39-27t17-71-17-72-39-28l-38 0q-22 0-38 28t-16 72 16 71 38 27l38 0z" horiz-adv-x="1000" />
+
+<glyph glyph-name="back-in-time" unicode="&#xe8aa;" d="M532 760q170 0 289-120t119-290-119-290-289-120q-138 0-252 88l70 76q82-60 182-60 126 0 216 90t90 216q0 128-90 218t-216 90q-124 0-213-86t-93-210l142 0-184-206-184 206 124 0q4 166 123 282t285 116z m-36-190l70 0 0-204 130-130-50-50-150 150 0 234z" horiz-adv-x="940" />
+
+<glyph glyph-name="monitor" unicode="&#xe8ab;" d="M900 790q42 0 71-30t29-70l0-550q0-42-29-77t-69-43l-218-44 86-38q50-28-20-28l-500 0q-98 0 32 52l36 14-220 44q-40 8-69 43t-29 77l0 550q0 40 30 70t70 30l800 0z m0-646l0 556-800 0 0-556 800 0z" horiz-adv-x="1000" />
+
+<glyph glyph-name="mobile" unicode="&#xe8ac;" d="M480 840q42 0 71-29t29-71l0-780q0-40-29-70t-71-30l-380 0q-40 0-70 30t-30 70l0 780q0 42 30 71t70 29l380 0z m-190-940q30 0 50 15t20 35q0 22-20 36t-50 14q-28 0-49-15t-21-35 21-35 49-15z m210 150l0 660-420 0 0-660 420 0z" horiz-adv-x="580" />
+
+<glyph glyph-name="cd" unicode="&#xe8ae;" d="M460 810q190 0 325-135t135-325-135-325-325-135-325 135-135 325 135 325 325 135z m0-610q62 0 106 44t44 106q0 64-43 107t-107 43q-62 0-106-44t-44-106 44-106 106-44z" horiz-adv-x="920" />
+
+<glyph glyph-name="inbox" unicode="&#xe8af;" d="M967 398q40-42 30-72l-28-154q-4-20-22-33t-40-13l-816 0q-22 0-40 13t-22 33l-28 154q-8 32 32 72 8 10 36 38t68 67 52 51q22 22 52 22l516 0q30 0 52-22 16-16 53-52t67-65 38-39z m-266-32l178 0-102 114-556 0-102-114 178 0q8 0 12-8l40-100 300 0 40 100q4 8 12 8z" horiz-adv-x="999" />
+
+<glyph glyph-name="install" unicode="&#xe8b0;" d="M884 306q24-52 14-96l-34-184q-2-20-19-35t-39-15l-712 0q-22 0-39 15t-19 35l-34 184q-8 50 14 96l158 374q22 46 72 46l104 0-20-204-134 0 254-210 256 210-136 0-18 204 102 0q50 0 74-46z m-68-132q2 22-10 38t-34 16l-644 0q-22 0-34-16t-10-38l14-74q2-22 19-37t37-15l592 0q22 0 39 15t19 37z" horiz-adv-x="901" />
+
+<glyph glyph-name="globe" unicode="&#xe8b1;" d="M480 830q200 0 340-141t140-339q0-200-140-340t-340-140q-198 0-339 140t-141 340q0 198 141 339t339 141z m410-480q0 132-78 239t-202 149q-18-24-16-32 4-38 18-51t30-7l32 12t20 2q22-24 0-47t-45-56-1-77q34-64 96-64 28-2 43-36t17-66q10-80-14-140-22-44 14-76 86 112 86 250z m-466 404q-112-14-199-84t-127-174q6 0 22-2t28-3 26-4 24-8 12-13q4-12-14-45t-18-61q0-30 38-56t38-46q0-28 8-68t8-44q0-12 36-54t52-42q10 0 11 22t-2 54-3 40q0 32 14 74 12 42 59 70t55 46q16 34 9 61t-17 43-34 28-41 17-37 9-22 4q-16 6-42 7t-36-3-27 11-17 29q0 10 15 27t35 37 28 30q8 14 17 21t22 16 27 21q4 4 25 17t27 23z m-72-794q66-20 128-20 128 0 226 68-26 44-118 34-24-2-65-17t-47-17q-74-16-76-16-12-2-26-14t-22-18z" horiz-adv-x="960" />
+
+<glyph glyph-name="cloud" unicode="&#xe8b2;" d="M760 494q100 0 170-68t70-166-70-166-170-68l-578 0q-74 0-128 52t-54 124q0 74 53 126t129 52q2 0 10-1t10-1q-2 12-2 38 0 108 78 184t188 76q90 0 160-52t94-134q28 4 40 4z" horiz-adv-x="1000" />
+
+<glyph glyph-name="cloud-thunder" unicode="&#xe8b3;" d="M760 494q100 0 170-68t70-166-70-166-170-68l-578 0q-74 0-128 52t-54 124q0 74 53 126t129 52q2 0 10-1t10-1q-2 12-2 38 0 108 78 184t188 76q90 0 160-52t94-134q28 4 40 4z m-192-216q14 16 14 30 0 20-30 32l-4 0q-26 14-38 16l50 116q6 0 6 20 0 14-8 18-16 10-34-8-2-2-30-32t-61-66-45-52q-12-18-12-30 0-22 30-30l4-2q8-4 38-16l-52-114-2-8q-2-8-2-14 0-10 8-18 18-10 34 10 100 100 134 148z" horiz-adv-x="1000" />
+
+<glyph glyph-name="flash" unicode="&#xe8b4;" d="M40-100q-4 4 35 94t79 182 38 98-94 45-98 55q-4 12 84 120t180 209 96 97q6-4-74-186t-78-186 95-43 97-57q4-20-174-227t-186-201z" horiz-adv-x="400" />
+
+<glyph glyph-name="moon" unicode="&#xe8b5;" d="M524 238q106 106 125 252t-53 270q52-26 96-72 128-128 128-309t-128-309-310-128-310 128q-40 40-72 94 124-70 271-51t253 125z" horiz-adv-x="820" />
+
+<glyph glyph-name="flight" unicode="&#xe8b6;" d="M268-120l124 400-180 0-112-100-100 0 80 170-80 170 100 0 112-100 180 0-124 400 100 0 224-400 274 0t36-4 46-11 36-21 16-34q0-32-38-49t-74-19l-38-2-258 0-224-400-100 0z" horiz-adv-x="1000" />
+
+<glyph glyph-name="paper-plane" unicode="&#xe8b7;" d="M894 720q14 4 22-3t4-19q-2-6-72-310t-74-316q-2-14-14-19t-24 1l-248 134-30 16 22 26q388 420 394 426 4 4-1 9t-9 1l-550-402-112 44-190 76q-12 4-12 12t12 12q8 4 441 157t441 155z m-582-728l0 204 160-82q-130-116-142-128-18-14-18 6z" horiz-adv-x="921" />
+
+<glyph glyph-name="leaf" unicode="&#xe8b8;" d="M236 646q182 106 506 66 168-22 196-50 4-6-2-10-76-40-130-109t-78-132-65-132-93-105q-138-96-382-4-66-76-114-176-12-24-47-7t-25 39q44 100 129 193t176 153 176 106 141 68l54 20q-14 0-41-1t-104-14-148-38-162-84-161-141q-22 242 174 358z" horiz-adv-x="940" />
+
+<glyph glyph-name="lifebuoy" unicode="&#xe8b9;" d="M454 810q190 2 326-130t140-322q2-190-131-327t-323-141q-190-2-327 131t-139 323q-4 190 130 327t324 139z m0-60q-94 0-178-44l62-104q56 28 122 28t122-28l62 104q-88 46-190 44z m-246-522q-28 60-28 122 0 64 28 124l-102 62q-46-88-46-190 2-96 46-180z m258-278q98 4 178 46l-62 104q-60-30-122-30t-122 30l-62-104q86-46 190-46z m-6 180q92 0 156 65t64 155q0 92-64 156t-156 64-156-64-64-156q0-90 64-155t156-65z m252 98l104-62q46 96 44 190 0 96-44 180l-104-62q28-60 28-124 0-62-28-122z" horiz-adv-x="920" />
+
+<glyph glyph-name="mouse" unicode="&#xe8ba;" d="M551 130q28-80-17-157t-139-111q-94-28-175 9t-103 117l-106 384q-20 68 6 134t84 106l-96 186q-14 34 14 48 30 18 48-14l98-192q80 22 154-16t102-116z m-324 274q28 10 40 36t4 54q-10 28-35 41t-53 5q-28-10-40-36t-4-54q10-28 35-41t53-5z" horiz-adv-x="561" />
+
+<glyph glyph-name="briefcase" unicode="&#xe8bb;" d="M456 326l0-100-456 0q8 226 10 292 4 108 100 108l160 0q16 26 37 67t23 45q14 26 23 32t37 6l222 0q26 0 36-7t22-31q18-32 60-112l160 0q96 0 100-108l10-292-454 0 0 100-90 0z m-74 354l-28-54 292 0-28 54q-14 26-42 26l-152 0q-28 0-42-26z m164-604l0 100 430 0q-6-88-10-166-6-84-90-84l-750 0q-90 0-90 84l-10 166 430 0 0-100 90 0z" horiz-adv-x="1000" />
+
+<glyph glyph-name="suitcase" unicode="&#xe8bc;" d="M900 650q42 0 71-30t29-70l0-550q0-42-29-71t-71-29l-50 0 0 750 50 0z m-900-100q0 40 30 70t70 30l50 0 0-750-50 0q-40 0-70 29t-30 71l0 550z m670 204l0-104 110 0 0-750-560 0 0 750 110 0 0 104q98 46 170 46t170-46z m-60-104l0 66q-52 24-110 24-54 0-110-24l0-66 220 0z" horiz-adv-x="1000" />
+
+<glyph glyph-name="dot" unicode="&#xe8bd;" d="M110 460q46 0 78-32t32-78q0-44-32-77t-78-33-78 33-32 77q0 46 32 78t78 32z" horiz-adv-x="220" />
+
+<glyph glyph-name="dot-2" unicode="&#xe8be;" d="M110 460q46 0 78-32t32-78q0-44-32-77t-78-33-78 32-32 78 32 78 78 32z m350 0q46 0 78-32t32-78q0-44-33-77t-77-33q-46 0-78 32t-32 78 32 78 78 32z" horiz-adv-x="570" />
+
+<glyph glyph-name="dot-3" unicode="&#xe8bf;" d="M110 460q46 0 78-32t32-78q0-44-32-77t-78-33-78 33-32 77q0 46 32 78t78 32z m350 0q46 0 78-32t32-78q0-44-33-77t-77-33-77 33-33 77q0 46 32 78t78 32z m350 0q46 0 78-32t32-78q0-44-32-77t-78-33-78 33-32 77q0 46 32 78t78 32z" horiz-adv-x="920" />
+
+<glyph glyph-name="brush" unicode="&#xe8c0;" d="M118 170q38 34 85 29t87-45q42-40 48-87t-30-83q-86-84-228-102-84-12-80 14 0 4 6 10 52 60 64 145t48 119z m840 646q26-26-148-248t-292-338q-38-38-124-104-8-6-16 8-18 34-48 64-32 32-66 48-16 6-8 16 64 84 104 122 118 116 344 287t254 145z" horiz-adv-x="962" />
+
+<glyph glyph-name="infinity" unicode="&#xe8c2;" d="M796 570q84 0 144-53t60-167q0-112-60-166t-144-54q-78 0-157 40t-139 106q-58-66-137-106t-157-40q-86 0-146 54t-60 166q0 114 60 167t146 53q78 0 157-39t137-105q58 66 138 105t158 39z m-590-352q60 0 127 37t113 95q-46 58-112 95t-128 37q-114 0-114-132t114-132z m590 0q114 0 114 132t-114 132q-62 0-129-37t-111-95q44-58 111-95t129-37z" horiz-adv-x="1000" />
+
+<glyph glyph-name="erase" unicode="&#xe8c3;" d="M902 700q42 0 71-29t29-71l0-500q0-40-29-70t-71-30l-478 0q-38 0-70 28l-340 296q-28 26 0 54l340 296q30 26 70 26l478 0z m-140-550l72 74-128 126 128 128-72 72-128-126-128 126-72-72 128-128-128-126 72-74 128 128z" horiz-adv-x="1002" />
+
+<glyph glyph-name="chart-pie" unicode="&#xe8c4;" d="M368 770l0-368-368 0q18 146 121 249t247 119z m106 0q156-20 261-139t105-279q0-174-123-298t-299-124q-160 0-278 105t-140 263l424 0q20 0 35 14t15 36l0 422z" horiz-adv-x="840" />
+
+<glyph glyph-name="chart-line" unicode="&#xe8c5;" d="M34 284q-42 10-32 56 10 42 54 32l98-24-52-80z m890-12q14 12 33 11t31-15q32-32-2-64l-252-226q-12-12-30-12-14 0-28 10l-286 220-54 14 50 80 36-8q12-4 16-8l264-204z m-490 220l-350-550q-12-22-38-22-12 0-24 8-16 10-20 29t6 33l374 588q8 16 28 20 18 6 36-6l246-156 226 326q10 16 28 19t34-9q38-24 12-62l-252-362q-24-36-62-12z" horiz-adv-x="1003" />
+
+<glyph glyph-name="chart-bar" unicode="&#xe8c6;" d="M750 800q22 0 36-15t14-35l0-850-200 0 0 850q0 50 40 50l110 0z m-300-300q22 0 36-15t14-35l0-550-200 0 0 550q0 50 40 50l110 0z m-300-300q22 0 36-15t14-35l0-250-200 0 0 250q0 50 40 50l110 0z" horiz-adv-x="800" />
+
+<glyph glyph-name="chart-area" unicode="&#xe8c7;" d="M964 732q16 22 16-4l0-768-964 0q-12 0-15 7t5 17l230 288q20 22 40 2l74-66q10-8 21-7t17 11l158 238q16 26 38 4l112-104q20-20 38 4z" horiz-adv-x="980" />
+
+<glyph glyph-name="tape" unicode="&#xe8c8;" d="M770 580q96 0 163-67t67-163q0-94-67-162t-163-68l-540 0q-94 0-162 68t-68 162q0 96 68 163t162 67q96 0 163-67t67-163q0-72-40-130l160 0q-40 64-40 130 0 96 68 163t162 67z m-670-230q0-52 38-91t92-39 92 39 38 91q0 54-38 92t-92 38-92-38-38-92z m670-130q54 0 92 39t38 91q0 54-38 92t-92 38-92-38-38-92q0-52 38-91t92-39z" horiz-adv-x="1000" />
+
+<glyph glyph-name="graduation-cap" unicode="&#xe8c9;" d="M166 238l334-168 276 136q-4-22-8-47t-6-35-11-23-24-23-45-22q-40-18-80-41t-63-34-39-11-40 13-64 37-80 40q-72 32-103 69t-47 109z m810 246q24-14 24-33t-24-33l-78-44-308 102q-22 36-90 36-40 0-67-16t-27-40 27-40 67-16q26 0 36 4l292-68-268-152q-60-32-120 0l-416 234q-24 14-24 33t24 33l416 234q60 32 120 0z m-128-442q18 116 13 182t-19 90l-14 22 70 38q6-8 12-28t17-101-7-197q-4-26-22-30t-35 5-15 19z" horiz-adv-x="1000" />
+
+<glyph glyph-name="language" unicode="&#xe8ca;" d="M988 306q30-82-10-176t-134-160q-10 0-12 2t-16 19-16 19q-2 6 2 10 86 60 117 152t-11 148q-16-38-39-76t-59-80-86-65-106-15q-52 6-84 41t-32 93q0 84 60 148 50 50 114 66l-2 100q-140-24-146-24-6-2-10 4 0 2-5 29t-5 31q-2 2 1 4t7 2l156 28q0 110-2 114 0 8 8 8 46 0 52 2 10 0 10-8l0-104q158 22 164 22 8 4 10-6 0-2 4-23t4-25q4-10-4-12l-176-30 0-102 12 0q86 0 148-36t86-100z m-370-160q28-6 62 6l-4 214q-34-12-60-40-44-44-44-108 0-66 46-72z m122 28q28 24 58 68t45 79 7 41q-36 18-96 18-2 0-6-1t-6-1z m-448 382q10-28 53-165t83-261 40-126q0-4-4-4l-86 0q-6 0-6 4l-50 166-176 0q-48-164-50-166 0-4-6-4l-86 0q-4 0-4 4 10 18 176 552 2 8 10 8l96 0q10 0 10-8z m-130-316l144 0-72 264z" horiz-adv-x="1001" />
+
+<glyph glyph-name="ticket" unicode="&#xe8cb;" d="M216 272l326 326 178-178-326-326z m710 244q14-14 14-36t-14-36l-550-550q-16-16-36-16t-36 16l-76 76q12 20 12 48 0 42-29 72t-71 30q-22 0-50-14l-74 76q-16 16-16 36t16 36l550 550q14 14 36 14t36-14l74-76q-12-22-12-48 0-42 30-71t72-29q26 0 48 12z m-532-502l406 406-258 258-408-406z" horiz-adv-x="940" />
+
+<glyph glyph-name="water" unicode="&#xe8cc;" d="M168 844q10-86 50-155t73-123 33-112q0-66-48-113t-114-47-114 47-48 113q0 58 33 112t73 123 50 155q2 4 7 4t5-4z m616 0q10-86 50-155t73-123 33-112q0-66-48-113t-114-47-114 47-48 113q0 48 21 93t48 78 53 92 34 127q2 4 7 4t5-4z m-320-444q2 4 7 4t5-4q10-86 50-155t73-123 33-112q0-66-48-113t-114-47-114 47-48 113q0 58 33 112t73 123 50 155z" horiz-adv-x="940" />
+
+<glyph glyph-name="droplet" unicode="&#xe8cd;" d="M290 822q14-118 60-219t92-159 82-136 36-160q0-114-83-196t-197-82-197 82-83 196q0 82 36 160t82 136 92 159 60 219q2 8 11 8t9-8z m-42-392q2 4-2 14-6 6-14 6t-12-6l-40-58q-32-46-48-70t-34-75-18-101q0-24 17-41t41-17q58 0 58 68 0 94 42 246 2 6 5 17t5 17z" horiz-adv-x="560" />
+
+<glyph glyph-name="air" unicode="&#xe8ce;" d="M85 534q-16-14-36-12t-34 18q-14 14-12 36t18 36q48 40 79 60t89 40 129 4 159-66 155-53 100 16 89 67q38 30 70-6 32-40-6-72-122-110-234-110-100 0-222 70-68 38-119 52t-93 0-65-29-67-51z m736-110q38 32 70-6 32-40-6-72-40-34-65-53t-72-38-97-19q-96 0-222 70-68 38-119 52t-93 0-65-29-67-51q-14-14-35-12t-35 18q-32 40 6 72 38 34 60 50t69 38 88 23 105-15 134-56q68-38 119-52t93 0 65 29 67 51z m0-256q38 32 70-6 14-14 12-36t-18-36q-40-34-65-53t-72-38-97-19q-96 0-222 70-68 38-119 52t-93 1-66-29-66-52q-14-14-35-12t-35 18q-32 40 6 72 38 34 60 50t69 38 88 23 105-15 134-56q68-38 119-52t93 0 65 29 67 51z" horiz-adv-x="905" />
+
+<glyph glyph-name="credit-card" unicode="&#xe8cf;" d="M900 700q42 0 71-30t29-70l0-500q0-42-29-71t-71-29l-800 0q-40 0-70 29t-30 71l0 500q0 40 30 70t70 30l800 0z m0-600l0 300-800 0 0-300 800 0z m0 450l0 50-800 0 0-50 800 0z m-700-256l30 0 0-30-30 0 0 30z m180-60l30 0 0 30 30 0 0 30 60 0 0-30-30 0 0-30-30 0 0-30-60 0 0 30z m120-30l-30 0 0 30 30 0 0-30z m-150 0l-60 0 0 30 60 0 0-30z m30 60l0-30-30 0 0 60 60 0 0-30-30 0z m-120-30l0-30-60 0 0 30 30 0 0 30 30 0 0 30 60 0 0-30-30 0 0-30-30 0z" horiz-adv-x="1000" />
+
+<glyph glyph-name="floppy" unicode="&#xe8d0;" d="M658 750l142-156 0-544q0-40-29-70t-71-30l-600 0q-40 0-70 30t-30 70l0 600q0 42 30 71t70 29l558 0z m-58-300l0 250-400 0 0-250q0-20 15-35t35-15l300 0q20 0 35 15t15 35z m-50 200l0-200-100 0 0 200 100 0z" horiz-adv-x="800" />
+
+<glyph glyph-name="clipboard" unicode="&#xe8d1;" d="M630 750q28 0 49-21t21-49l0-760q0-30-21-50t-49-20l-560 0q-28 0-49 20t-21 50l0 760q0 28 21 49t49 21l60-150 440 0z m-100-100l-360 0-44 100 108 0 36 100 160 0 36-100 110 0z" horiz-adv-x="700" />
+
+<glyph glyph-name="megaphone" unicode="&#xe8d2;" d="M792 500q58-138 67-258t-39-140q-28-12-61 3t-65 40-99 41-149 8q-28-4-42-19t-6-37q22-56 46-108 4-10 24-22t24-20q14-34-22-46-50-22-102-40-30-10-54 42-32 76-58 132-6 12-34 17t-46 31q-30-10-38-14-34-12-74 12t-54 60q-17 32-5 79t43 61q126 52 213 108t124 103 59 92 25 78 15 59 36 36q48 20 130-70t142-228z m-28-300q8 4 10 38t-11 98-41 128q-28 66-67 123t-67 84-36 23-10-42 10-105 40-133 68-119 68-76 36-19z" horiz-adv-x="860" />
+
+<glyph glyph-name="database" unicode="&#xe8d3;" d="M686 208q14 20 14-2l0-100q0-74-104-135t-246-61q-140 0-245 61t-105 135l0 100q0 8 4 10t10-8q32-52 125-86t211-34 211 34 125 86z m2 254q8 16 12 0l0-116q0-68-102-114t-248-46q-144 0-247 46t-103 114l0 116q0 20 14 0 30-46 124-75t212-29 212 29 126 75z m-338 328q144 0 247-39t103-93l0-64q0-58-103-99t-247-41-247 41-103 99l0 64q0 54 103 93t247 39z" horiz-adv-x="700" />
+
+<glyph glyph-name="drive" unicode="&#xe8d4;" d="M884 304q26-44 14-96l-34-184q-2-20-19-35t-39-15l-712 0q-20 0-38 15t-20 35l-34 184q-8 52 14 96l158 374q22 46 72 46l408 0q50 0 74-46z m-68-132q2 22-10 38t-34 16l-644 0q-22 0-34-16t-10-38l14-74q2-22 19-37t39-15l590 0q22 0 39 15t19 37z" horiz-adv-x="902" />
+
+<glyph glyph-name="bucket" unicode="&#xe8d5;" d="M522 780q174 0 286-49t104-105q-6-38-48-307t-44-281q-2-18-37-44t-107-50-154-24-153 24-106 50-37 44q0 2-4 30 82-6 163 35t139 117q28 0 48 20t20 50q0 28-20 49t-50 21q-28 0-49-21t-21-49q0-20 10-36-48-58-115-89t-131-27q-102 10-157 57t-59 109q-8 122 156 184-18 94-22 138-8 56 104 105t284 49z m-452-470q4-32 37-59t91-39l-32 204q-100-44-96-106z m452 212q82 0 157 18t113 39 38 35-38 35-112 39-158 18q-82 0-156-18t-112-39-38-35 38-35 112-39 156-18z" horiz-adv-x="913" />
+
+<glyph glyph-name="thermometer" unicode="&#xe8d6;" d="M400 356q64-36 102-98t38-138q0-112-79-191t-191-79-191 79-79 191q0 76 38 138t102 98l0 444q0 50 40 50l170 0q20 0 35-15t15-35l0-444z m-130-406q70 0 120 50t50 120q0 56-32 100t-84 60l0 370-100 0 0-368q-54-16-89-61t-35-101q0-70 50-120t120-50z" horiz-adv-x="540" />
+
+<glyph glyph-name="key" unicode="&#xe8d7;" d="M774 612q20-116-28-215t-150-117q-66-12-130-2l-118-194-70-12-104-166q-14-28-46-32l-76-14q-12-4-22 4t-12 22l-16 98q-8 30 12 56l258 386q-24 50-38 120-18 106 53 187t185 101q106 20 195-45t107-177z m-126-76q30 44 21 97t-51 83q-42 32-92 22t-80-54q-8-12-12-23t-1-20 5-16 13-17 18-15 22-16 23-17q6-4 22-16t23-16 19-12 19-8 17 1 18 8 16 19z" horiz-adv-x="780" />
+
+<glyph glyph-name="flow-cascade" unicode="&#xe8d8;" d="M520 120q50 0 85-35t35-85-35-85-85-35q-80 0-110 74l-164 0q-88 0-131 54t-43 118l0 464q-72 34-72 110 0 50 35 85t85 35 85-35 35-85q0-76-72-110l0-114q0-78 78-78l164 0q30 72 110 72 50 0 85-35t35-85-35-85-85-35q-80 0-110 74l-164 0q-42 0-78 16l0-194q0-78 78-78l164 0q30 72 110 72z m0 300q-28 0-49-20t-21-50q0-28 21-48t49-20 49 20 21 48q0 30-21 50t-49 20z m-470 280q0-28 21-48t49-20 49 20 21 48q0 30-21 50t-49 20-49-20-21-50z m470-768q28 0 49 20t21 48q0 30-21 50t-49 20-49-20-21-50q0-28 21-48t49-20z" horiz-adv-x="640" />
+
+<glyph glyph-name="flow-branch" unicode="&#xe8d9;" d="M640 650q0-80-74-110-6-58-28-101t-61-69-68-38-75-26q-42-14-63-22t-47-24-38-40-16-60q70-30 70-110 0-50-35-85t-85-35-85 35-35 85q0 78 72 112l0 378q-72 34-72 110 0 50 35 85t85 35 85-35 35-85q0-76-72-110l0-204q40 30 138 60 58 18 84 29t51 41 29 76q-70 32-70 108 0 50 35 85t85 35 85-35 35-85z m-588 0q0-28 20-48t48-20 49 20 21 48q0 30-21 50t-49 20-48-20-20-50z m68-668q28 0 49 20t21 48q0 30-21 50t-49 20-48-20-20-50q0-28 20-48t48-20z m400 600q28 0 49 20t21 48q0 30-21 50t-49 20-48-20-20-50q0-28 20-48t48-20z" horiz-adv-x="640" />
+
+<glyph glyph-name="flow-tree" unicode="&#xe8da;" d="M868 112q72-34 72-112 0-50-35-85t-85-35-85 35-35 85q0 78 72 112l0 114q0 78-76 78l-100 0q-44 0-78 12l0-204q72-34 72-112 0-50-35-85t-85-35-85 35-35 85q0 78 72 112l0 204q-30-12-76-12l-100 0q-34 0-53-19t-22-33-3-26l0-114q72-34 72-112 0-50-35-85t-85-35-85 35-35 85q0 78 72 112l0 114q0 64 43 118t131 54l100 0q76 0 76 52l0 140q-72 34-72 110 0 50 35 85t85 35 85-35 35-85q0-76-72-110l0-140q0-52 78-52l100 0q86 0 129-54t43-118l0-114z m-678-112q0 30-21 50t-49 20-48-20-20-50q0-28 20-48t48-20 49 20 21 48z m212 700q0-28 20-48t48-20 49 20 21 48q0 30-21 50t-49 20-48-20-20-50z m138-700q0 30-21 50t-49 20-48-20-20-50q0-28 20-48t48-20 49 20 21 48z m280-68q28 0 49 20t21 48q0 30-21 50t-49 20-48-20-20-50q0-28 20-48t48-20z" horiz-adv-x="940" />
+
+<glyph glyph-name="flow-line" unicode="&#xe8db;" d="M168 162q72-34 72-112 0-50-35-85t-85-35-85 35-35 85q0 78 72 112l0 378q-72 34-72 110 0 50 35 85t85 35 85-35 35-85q0-76-72-110l0-378z m-116 488q0-28 20-48t48-20 49 20 21 48q0 30-21 50t-49 20-48-20-20-50z m68-668q28 0 49 20t21 48q0 30-21 50t-49 20-48-20-20-50q0-28 20-48t48-20z" horiz-adv-x="240" />
+
+<glyph glyph-name="flow-parallel" unicode="&#xe8dc;" d="M240 650q0-76-72-110l0-378q72-34 72-112 0-50-35-85t-85-35-85 35-35 85q0 78 72 112l0 378q-72 34-72 110 0 50 35 85t85 35 85-35 35-85z m-50-600q0 30-21 50t-49 20-48-20-20-50q0-28 20-48t48-20 49 20 21 48z m-70 532q28 0 49 20t21 48q0 30-21 50t-49 20-48-20-20-50q0-28 20-48t48-20z m448-420q72-34 72-112 0-50-35-85t-85-35-85 35-35 85q0 78 72 112l0 378q-72 34-72 110 0 50 35 85t85 35 85-35 35-85q0-76-72-110l0-378z m-116 488q0-28 20-48t48-20 49 20 21 48q0 30-21 50t-49 20-48-20-20-50z m68-668q28 0 49 20t21 48q0 30-21 50t-49 20-48-20-20-50q0-28 20-48t48-20z" horiz-adv-x="640" />
+
+<glyph glyph-name="rocket" unicode="&#xe8dd;" d="M543 236q6-50 8-81t-8-59-13-40-35-32-45-26-70-31-85-37q-32-12-45 4t-3 44l40 110-130 132-106-40q-28-12-43 2t-3 46q12 30 31 79t27 65 22 45 25 36 29 20 41 13l52 0t71-6q10 14 29 39t77 85 118 104 145 75 165 19q8 0 14-6 4-4 6-14 10-82-18-168t-76-151-98-118-86-81z m50 296q22-22 54-22t54 22q22 24 22 56t-22 56q-22 22-54 22t-54-22q-22-24-22-56t22-56z" horiz-adv-x="860" />
+
+<glyph glyph-name="gauge" unicode="&#xe8de;" d="M406 178q34 56 214 284t194 220q12-6-96-278t-138-326q-50-86-136-36t-38 136z m94 380q-168 0-284-127t-116-311q0-30 2-46 2-22-12-37t-34-17-36 12-18 34q0 8-1 26t-1 28q0 226 145 382t355 156q72 0 134-18l-70-86q-40 4-64 4z m362-62q138-154 138-376 0-38-2-56-2-20-16-33t-34-13l-4 0q-22 4-35 20t-11 36q2 14 2 46 0 150-80 268 6 14 20 51t22 57z" horiz-adv-x="1000" />
+</font>
+</defs>
+</svg>

BIN
src/mol-app/skin/fonts/fontello.ttf


BIN
src/mol-app/skin/fonts/fontello.woff


BIN
src/mol-app/skin/fonts/fontello.woff2


+ 135 - 0
src/mol-app/skin/icons.scss

@@ -0,0 +1,135 @@
+
+[class^="molstar-icon-"]:before, [class*=" molstar-icon-"]:before {
+    font-family: "fontello";
+    font-style: normal;
+    font-weight: normal;
+    speak: none;
+
+    display: inline-block;
+    text-decoration: inherit;
+    width: 1em;
+    margin-right: .2em;
+    text-align: center;
+    /* opacity: .8; */
+
+    /* For safety - reset parent styles, that can break glyph codes*/
+    font-variant: normal;
+    text-transform: none;
+
+    /* fix buttons height, for twitter bootstrap */
+    line-height: 1em;
+
+    /* Animation center compensation - margins should be symmetric */
+    /* remove if not needed */
+    margin-left: .2em;
+
+    /* you can be more comfortable with increased icons size */
+    /* font-size: 120%; */
+
+    /* Font smoothing. That was taken from TWBS */
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+
+    /* Uncomment for 3D effect */
+    /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
+  }
+
+  .molstar-icon-expand-layout:before {
+      content: "\e84a";
+  }
+
+  .molstar-icon-plus:before {
+      content: "\e816";
+  }
+
+  .molstar-icon-minus:before {
+      content: "\e819";
+  }
+
+  .molstar-icon-reset-scene:before {
+      content: "\e891";
+  }
+
+  .molstar-icon-ok:before {
+      content: "\e812";
+  }
+
+  .molstar-icon-cross:before {
+      content: "\e868";
+  }
+
+  .molstar-icon-off:before {
+      content: "\e813";
+  }
+
+  .molstar-icon-expand:before {
+      content: "\e885";
+  }
+
+  .molstar-icon-collapse:before {
+      content: "\e883";
+  }
+
+  .molstar-icon-visual-visibility:before {
+      content: "\e826";
+  }
+
+  .molstar-icon-abort:before {
+      content: "\e814";
+  }
+
+  .molstar-icon-focus-on-visual:before {
+      content: "\e8a3";
+  }
+
+  .molstar-icon-settings:before  {
+      content: "\e855";
+  }
+
+  .molstar-icon-tools:before {
+      content: "\e856";
+  }
+
+  .molstar-icon-log:before {
+      content: "\e8a5";
+  }
+
+  .molstar-icon-remove:before {
+      content: "\e847";
+  }
+
+  .molstar-icon-help:before {
+      content: '\e81c'
+  }
+
+  .molstar-icon-info:before {
+      content: '\e81e'
+  }
+
+  .molstar-icon-left-open-big:before {
+      content: '\e87c'
+  }
+
+  .molstar-icon-right-open-big:before {
+      content: '\e87d'
+  }
+
+  .molstar-icon-left-open:before {
+      content: '\e874'
+  }
+
+  .molstar-icon-right-open:before {
+      content: '\e875'
+  }
+
+  .molstar-icon-screenshot:before {
+      content: "\e80f";
+  }
+
+  .molstar-icon-help:before {
+      content: "\e81c";
+  }
+
+  .molstar-icon-help-circle:before {
+      content: "\e81d";
+  }

+ 29 - 0
src/mol-app/skin/layout.scss

@@ -0,0 +1,29 @@
+
+@import 'layout/common';
+
+.molstar-layout-standard-outside {
+    position: absolute;
+    @import 'layout/outside';
+}
+
+.molstar-layout-standard-landscape {
+    position: absolute;
+    @import 'layout/landscape';
+}
+
+.molstar-layout-standard-portrait {
+    position: absolute;
+    @import 'layout/portrait';
+}
+
+.molstar-layout-expanded {
+    position: fixed;
+
+    @media (orientation:landscape) {
+        @import 'layout/landscape';
+    };
+
+    @media (orientation:portrait) {
+        @import 'layout/portrait';
+    };
+}

+ 60 - 0
src/mol-app/skin/layout/common.scss

@@ -0,0 +1,60 @@
+
+.molstar-layout-expanded, .molstar-layout-standard {
+    left: 0;
+    right: 0;
+    top: 0;
+    bottom: 0;
+}
+
+.molstar-layout-region {
+    overflow: hidden;
+    background: $default-background;
+}
+
+.molstar-layout-static, .molstar-layout-scrollable {
+    position: absolute;
+}
+
+.molstar-layout-scrollable {
+    overflow-y: auto;
+}
+
+.molstar-layout-static {
+    overflow: hidden;
+}
+
+.molstar-layout-main, .molstar-layout-bottom {
+    .molstar-layout-static {
+        left: 0;
+        right: 0;
+        top: 0;
+        bottom: 0;
+    }
+}
+
+.molstar-layout-right {
+
+    .molstar-layout-static {
+        left: 0;
+        right: 0;
+        top: 0;
+        height: $row-height + $control-spacing;
+    }
+
+    .molstar-layout-scrollable {
+        left: 0;
+        right: 0;
+        top: $row-height + $control-spacing + 1;
+        bottom: 0;
+     }
+
+}
+
+.molstar-layout-left {
+    .molstar-layout-static {
+        left: 0;
+        right: 0;
+        bottom: 0;
+        top: 0;
+    }
+}

+ 81 - 0
src/mol-app/skin/layout/landscape.scss

@@ -0,0 +1,81 @@
+
+.molstar-layout-main {
+    position: absolute;
+    left: $expanded-left-width;
+    right: $expanded-right-width;
+    bottom: $expanded-bottom-height;
+    top: $expanded-top-height;
+}
+
+.molstar-layout-top {
+    position: absolute;
+    left: $expanded-left-width;
+    right: $expanded-right-width;
+    height: $expanded-top-height;
+    top: 0;
+    border-bottom: 1px solid $border-color;
+}
+
+.molstar-layout-bottom {
+    position: absolute;
+    left: $expanded-left-width;
+    right: $expanded-right-width;
+    height: $expanded-bottom-height;
+    bottom: 0;
+    border-top: 1px solid $border-color;
+}
+
+.molstar-layout-right {
+    position: absolute;
+    width: $expanded-right-width;
+    right: 0;
+    bottom: 0;
+    top: 0;
+    border-left: 1px solid $border-color;
+}
+
+.molstar-layout-left {
+    position: absolute;
+    width: $expanded-left-width;
+    left: 0;
+    bottom: 0;
+    top: 0;
+    border-right: 1px solid $border-color;
+}
+
+.molstar-layout-hide-right {
+    .molstar-layout-right {
+        display: none;
+    }
+    .molstar-layout-main, .molstar-layout-top, .molstar-layout-bottom {
+        right: 0;
+    }
+}
+
+
+.molstar-layout-hide-left {
+    .molstar-layout-left {
+        display: none;
+    }
+    .molstar-layout-main, .molstar-layout-top, .molstar-layout-bottom {
+        left: 0;
+    }
+}
+
+.molstar-layout-hide-bottom {
+    .molstar-layout-bottom {
+        display: none;
+    }
+    .molstar-layout-main {
+        bottom: 0;
+    }
+}
+
+.molstar-layout-hide-top {
+    .molstar-layout-top {
+        display: none;
+    }
+    .molstar-layout-main {
+        top: 0;
+    }
+}

+ 89 - 0
src/mol-app/skin/layout/outside.scss

@@ -0,0 +1,89 @@
+
+.molstar-layout-main {
+    position: absolute;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    top: 0;
+}
+
+.molstar-layout-top {
+    position: absolute;
+    right: 0;
+    height: $standard-top-height;
+    top: -$standard-top-height;
+    width: 50%;
+    border-left: 1px solid $border-color;
+    border-bottom: 1px solid $border-color;
+}
+
+.molstar-layout-bottom {
+    position: absolute;
+    left: 0;
+    right: 0;
+    height: $standard-top-height;
+    top: -$standard-top-height;
+    width: 50%;
+    border-bottom: 1px solid $border-color;
+}
+
+.molstar-layout-right {
+    position: absolute;
+    width: 50%;
+    right: 0;
+    bottom: -$standard-bottom-height;
+    height: $standard-bottom-height;
+    border-left: 1px solid $border-color;
+    border-top: 1px solid $border-color;
+}
+
+.molstar-layout-left {
+    position: absolute;
+    width: 50%;
+    left: 0;
+    bottom: 0;
+    bottom: -$standard-bottom-height;
+    height: $standard-bottom-height;
+    border-top: 1px solid $border-color;
+}
+
+/////////////////////////////////////////
+.molstar-layout-hide-right {
+    .molstar-layout-right {
+        display: none;
+    }
+    .molstar-layout-left {
+        width: 100%;
+    }
+}
+
+.molstar-layout-hide-left {
+    .molstar-layout-left {
+        display: none;
+    }
+    .molstar-layout-right {
+        width: 100%;
+        border-left: none;
+    }
+}
+
+///////////////////////////////////
+.molstar-layout-hide-top {
+    .molstar-layout-top {
+        display: none;
+    }
+    .molstar-layout-bottom {
+        width: 100%;
+        border-left: none;
+    }
+}
+
+.molstar-layout-hide-bottom {
+    .molstar-layout-bottom {
+        display: none;
+    }
+    .molstar-layout-top {
+        width: 100%;
+        border-left: none;
+    }
+}

+ 99 - 0
src/mol-app/skin/layout/portrait.scss

@@ -0,0 +1,99 @@
+
+.molstar-layout-main {
+    position: absolute;
+    left: 0;
+    right: 0;
+    bottom: $expanded-portrait-bottom-height;
+    top: $expanded-portrait-top-height;
+}
+
+.molstar-layout-top {
+    position: absolute;
+    right: 0;
+    height: $expanded-portrait-top-height;
+    top: 0;
+    width: 50%;
+    border-left: 1px solid $border-color;
+    border-bottom: 1px solid $border-color;
+}
+
+.molstar-layout-bottom {
+    position: absolute;
+    left: 0;
+    right: 0;
+    height: $expanded-portrait-top-height;
+    width: 50%;
+    border-bottom: 1px solid $border-color;
+}
+
+.molstar-layout-right {
+    position: absolute;
+    width: 50%;
+    right: 0;
+    bottom: 0;
+    height: $expanded-portrait-bottom-height;
+    border-left: 1px solid $border-color;
+    border-top: 1px solid $border-color;
+}
+
+.molstar-layout-left {
+    position: absolute;
+    width: 50%;
+    left: 0;
+    bottom: 0;
+    height: $expanded-portrait-bottom-height;
+    border-top: 1px solid $border-color;
+}
+
+/////////////////////////////////////////
+.molstar-layout-hide-right {
+    .molstar-layout-right {
+        display: none;
+    }
+    .molstar-layout-left {
+        width: 100%;
+    }
+}
+
+.molstar-layout-hide-left {
+    .molstar-layout-left {
+        display: none;
+    }
+    .molstar-layout-right {
+        width: 100%;
+        border-left: none;
+    }
+}
+
+.molstar-layout-hide-right.molstar-layout-hide-left {
+    .molstar-layout-main {
+        bottom: 0;
+    }
+}
+
+///////////////////////////////////
+.molstar-layout-hide-top {
+    .molstar-layout-top {
+        display: none;
+    }
+    .molstar-layout-bottom {
+        width: 100%;
+        border-left: none;
+    }
+}
+
+.molstar-layout-hide-bottom {
+    .molstar-layout-bottom {
+        display: none;
+    }
+    .molstar-layout-top {
+        width: 100%;
+        border-left: none;
+    }
+}
+
+.molstar-layout-hide-top.molstar-layout-hide-bottom {
+    .molstar-layout-main {
+        top: 0;
+    }
+}

Різницю між файлами не показано, бо вона завелика
+ 45 - 0
src/mol-app/skin/logo.scss


+ 2 - 0
src/mol-app/skin/molstar-blue.scss

@@ -0,0 +1,2 @@
+@import 'colors/blue';
+@import 'base';

+ 2 - 0
src/mol-app/skin/molstar-dark.scss

@@ -0,0 +1,2 @@
+@import 'colors/dark';
+@import 'base';

+ 2 - 0
src/mol-app/skin/molstar-light.scss

@@ -0,0 +1,2 @@
+@import 'colors/light';
+@import 'base';

+ 38 - 0
src/mol-app/skin/ui.scss

@@ -0,0 +1,38 @@
+@mixin non-selectable {
+    -webkit-user-select: none; /* Chrome/Safari */
+    -moz-user-select: none; /* Firefox */
+    -ms-user-select: none; /* IE10+ */
+    /* Rules below not implemented in browsers yet */
+    -o-user-select: none;
+    user-select: none;
+
+    cursor: default;
+}
+
+::-webkit-scrollbar {
+    width: 10px;
+    height:10px;
+}
+
+::-webkit-scrollbar-track {
+    //-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.8);
+    border-radius: 0;
+    background-color: color-lower-contrast($control-background, 4%);
+}
+
+::-webkit-scrollbar-thumb {
+    border-radius: 0;
+    //-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.9);
+    background-color: color-lower-contrast($control-background, 8%);
+}
+
+@import 'components/controls-base';
+@import 'components/controls';
+@import 'components/entity';
+@import 'components/help';
+@import 'components/jobs';
+@import 'components/log';
+@import 'components/misc';
+@import 'components/panel';
+@import 'components/slider';
+@import 'components/viewport';

+ 78 - 0
src/mol-app/skin/variables.scss

@@ -0,0 +1,78 @@
+
+// measures
+
+$control-label-width:   110px;
+$row-height:            32px;
+$control-spacing:       10px;
+$entity-subtree-offset: 8px;
+$info-vertical-padding: 6px;
+$slider-border-radius-base: 6px;
+
+// layout
+$expanded-top-height:    100px;
+$expanded-bottom-height: 3 * $row-height + 2;
+$expanded-right-width:   290px;
+$expanded-left-width:    290px;
+
+$expanded-portrait-bottom-height: 10 * ($row-height + 1) + 3 * $control-spacing + 1;
+$expanded-portrait-top-height:    2 * $row-height + 1;
+
+$standard-bottom-height: 8 * ($row-height + 1) + 3 * $control-spacing + 1;
+$standard-top-height:    2 * $row-height + 1;
+
+//////////////////////////////////////////////////
+// ENTITY COLORS
+
+
+// entity colors are "somewhat orthogonal" on the RGB cube
+// TypeClass = 'Root' | 'Group' | 'Data' | 'Object' | 'Visual' | 'Selection' | 'Action' | 'Behaviour'
+
+// DO NOT CHANGE THESE!!
+$entity-color-Root:      $default-background;
+$entity-color-Data:      color-lower-contrast(#95a5a6, 15%);
+$entity-color-Selection: color-lower-contrast(#e74c3c, 15%);
+$entity-color-Action:    color-lower-contrast(#34495e, 10%);
+$entity-color-Object:    color-lower-contrast(#2ecc71, 10%);
+$entity-color-Behaviour: color-lower-contrast(#9b59b6, 10%);
+$entity-color-Visual:    color-lower-contrast(#3498db, 5%);
+$entity-color-Group:     color-lower-contrast(#e67e22, 5%);
+
+//////////////////////////////////////////////////
+// COLORS and COMPUTED COLORS
+
+$slider-disabledColor: #ccc;
+
+$control-background: color-increase-contrast($default-background, 6.5%);
+$border-color: color-increase-contrast($default-background, 15%);
+$molstar-form-control-background: color-lower-contrast($default-background, 2.5%);
+
+// buttons
+$molstar-btn-link-font-color: $font-color;
+$molstar-btn-link-toggle-on-font-color: $font-color;
+$molstar-btn-link-toggle-off-font-color: color-lower-contrast($font-color, 33%);
+
+// used for "actions" -- i.e. + in selection
+$molstar-btn-remove-font-color: $font-color;
+
+$molstar-btn-action-background: $molstar-form-control-background;
+
+// update selection etc
+//!! $molstar-btn-commit-on-font-color: $entity-current-font-color;
+$molstar-btn-commit-on-hover-font-color: color-lower-contrast($molstar-btn-commit-on-font-color, 20%); //!!Change
+$molstar-btn-commit-on-background: color-lower-contrast($default-background, 2%);
+$molstar-btn-commit-off-background:  color-lower-contrast($default-background, 4%); //$control-background;
+$molstar-btn-commit-off-font-color: $font-color;
+
+// log
+$log-font-color: color-lower-contrast($font-color, 5%);
+$log-timestamp-font-color: color-lower-contrast($font-color, 20%);
+
+// highlight
+$highlight-info-font-color: $hover-font-color;
+$highlight-info-additional-font-color: color-lower-contrast($hover-font-color, 20%);
+
+// entity state
+$entity-color-fully-visible: $font-color;
+$entity-color-not-visible: color-lower-contrast($font-color, 66%);
+$entity-color-partialy-visible: color-lower-contrast($font-color, 33%);
+$entity-tag-color: color-lower-contrast($font-color, 20%);

+ 167 - 0
src/mol-app/ui/controls/common.tsx

@@ -0,0 +1,167 @@
+/*
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ */
+
+import * as React from 'react'
+import { shallowEqual } from 'mol-util'
+
+export type ButtonSize = 'xs' | 'sm' | 'normal' | 'lg'
+
+export type ButtonStyle = 'link' | 'remove' | 'default'
+
+export abstract class Pure<Props> extends React.Component<Props, {}> {
+    shouldComponentUpdate(nextProps: any, nextState: any) {
+        return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState);
+    }
+}
+
+export class Button extends Pure<{
+    onClick: (e: React.MouseEvent<HTMLButtonElement> | React.TouchEvent<HTMLButtonElement>) => void,
+    size?: ButtonSize,
+    style?: ButtonStyle,
+    active?: boolean,
+    activeStyle?: ButtonStyle,
+    icon?: string,
+    activeIcon?: string,
+    disabled?: boolean,
+    disabledStyle?: ButtonStyle,
+    asBlock?: boolean,
+    title?: string,
+    customClass?: string,
+    customStyle?: any
+}> {
+    render() {
+
+        let props = this.props;
+
+        let className = 'molstar-btn';
+        if (props.size && props.size !== 'normal') className += ' molstar-btn-' + props.size;
+        if (props.asBlock) className += ' molstar-btn-block';
+
+        if (props.disabled) className += ' molstar-btn-' + (props.disabledStyle || props.style || 'default');
+        else if (props.active) className += ' molstar-btn-' + (props.activeStyle || props.style || 'default');
+        else className += ' molstar-btn-' + (props.style || 'default');
+
+        if (props.customClass) className += ' ' + props.customClass;
+
+        let icon: any = void 0;
+
+        if (props.icon) {
+            if (props.active && props.activeIcon) icon = <span className={ `molstar-icon molstar-icon-${props.activeIcon}` }></span>
+            else icon = <span className={ `molstar-icon molstar-icon-${props.icon}` }></span>
+        }
+        //onTouchEnd={(e) => { (e.target as HTMLElement).blur() } }
+
+        return <button
+            title={props.title}
+            className={className}
+            style={props.customStyle}
+            disabled={props.disabled}
+            onClick={(e) => { props.onClick.call(null, e); (e.target as HTMLElement).blur() } }
+                >
+            {icon}{props.children}
+        </button>
+    }
+}
+
+export const TextBox = (props: {
+    onChange: (v: string) => void,
+    value?: string,
+    defaultValue?: string,
+    onKeyPress?: (e: React.KeyboardEvent<HTMLInputElement>) => void,
+    onBlur?: (e: React.FormEvent<HTMLInputElement>) => void,
+    placeholder?: string
+}) => <input type='text' className='molstar-form-control' placeholder={props.placeholder} value={props.value} defaultValue={props.defaultValue}
+        onBlur={e => { if (props.onBlur) props.onBlur.call(null, e) } }
+        onChange={e => props.onChange.call(null, (e.target as HTMLInputElement).value)} onKeyPress={props.onKeyPress} />;
+
+export function isEnter(e: React.KeyboardEvent<HTMLInputElement>) {
+    if ((e.keyCode === 13 || e.charCode === 13)) {
+        return true;
+    }
+    return false;
+}
+
+export function TextBoxGroup(props: {
+    value: string,
+    onChange: (v: string) => void,
+    placeholder?:string,
+    label: string,
+    onEnter?: (e: React.KeyboardEvent<HTMLInputElement>) => void
+    title?: string
+}) {
+    return <div className='molstar-control-row molstar-options-group' title={props.title}>
+        <span>{props.label}</span>
+        <div>
+            <TextBox placeholder={props.placeholder} onChange={props.onChange} value={props.value} onKeyPress={(e) => {
+                if (isEnter(e) && props.onEnter) props.onEnter.call(null, e)
+            } }  />
+        </div>
+    </div>;
+}
+
+export const CommitButton = (props: {
+    action: () => void,
+    isOn: boolean,
+    on: string,
+    off?: string,
+    title?: string
+}) => <div style={{ marginTop: '1px' }}><button onClick={e => { props.action(); (e.target as HTMLElement).blur(); }}
+        className={'molstar-btn molstar-btn-block molstar-btn-commit molstar-btn-commit-' + (props.isOn ? 'on' : 'off')}
+        disabled={!props.isOn} title={props.title}>
+        <span className={ `molstar-icon molstar-icon-${props.isOn ? 'ok' : 'cross'}` }></span>
+        {props.isOn ? <b>{props.on}</b> : (props.off ? props.off : props.on) }
+    </button></div> ;
+
+export const Toggle = (props: {
+    onChange: (v: boolean) => void,
+    value: boolean,
+    label: string,
+    title?: string
+}) => <div className='molstar-control-row molstar-toggle-button' title={props.title}>
+        <span>{props.label}</span>
+        <div>
+            <button onClick={e => { props.onChange.call(null, !props.value); (e.target as HTMLElement).blur(); }}>
+                    <span className={ `molstar-icon molstar-icon-${props.value ? 'ok' : 'off'}` }></span> {props.value ? 'On' : 'Off'}
+            </button>
+        </div>
+    </div>
+
+export const ControlGroupExpander = (props: { onChange: (e: boolean) => void, isExpanded: boolean }) =>
+        <Button style='link' title={`${props.isExpanded ? 'Less' : 'More'} options`} onClick={() => props.onChange.call(null, !props.isExpanded) }
+                            icon={props.isExpanded ? 'minus' : 'plus'} customClass='molstar-conrol-group-expander' />
+
+
+export const RowText = (props: {
+    value: any,
+    label: string,
+    title?: string
+}) => <div className='molstar-control-row molstar-row-text' title={props.title}>
+        <span>{props.label}</span>
+        <div>
+            {props.value}
+        </div>
+    </div>
+
+export const HelpBox = (props: {
+    title: string,
+    content: JSX.Element | string
+}) => <div className='molstar-help-row'>
+        <span>{props.title}</span>
+        <div>{props.content}</div>
+    </div>
+
+export function FileInput (props: {
+    accept: string
+    onChange: (v: FileList | null) => void,
+}) {
+    return <input
+        accept={props.accept || '*.*'}
+        type='file'
+        className='molstar-form-control'
+        onChange={e => props.onChange.call(null, e.target.files)}
+    />
+}

+ 814 - 0
src/mol-app/ui/controls/slider.tsx

@@ -0,0 +1,814 @@
+/*
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ */
+
+import * as React from 'react'
+import { TextBox, isEnter } from './common'
+
+export class Slider extends React.Component<{
+    label: any,
+    min: number,
+    max: number,
+    value: number,
+    step?: number,
+    title?: string,
+    onChange: (v: number) => void
+}, { value: string }> {
+
+    state = { value: '0' }
+
+    private firedValue = NaN;
+
+    componentWillMount() {
+        this.setState({ value: '' + this.props.value });
+    }
+
+    componentWillReceiveProps(nextProps: any) {
+        this.setState({ value: '' + nextProps.value });
+    }
+
+    private updateValue(s: string) {
+        let v = +s;
+        if (v < this.props.min) { v = this.props.min; s = '' + v; }
+        else if (v > this.props.max) { v = this.props.max; s = '' + v; }
+        this.setState({ value: s })
+    }
+
+    private fire() {
+        let v = +this.state.value;
+        if (isNaN(v)) { v = this.props.value; }
+        if (v !== this.props.value) {
+            if (this.firedValue !== v) {
+                this.firedValue = v;
+                this.props.onChange.call(null, v);
+            }
+        }
+    }
+
+    render() {
+        let step = this.props.step;
+        if (step === void 0) step = 1;
+        return <div className='molstar-control-row molstar-slider' title={this.props.title}>
+            <span>{this.props.label}</span>
+            <div>
+                <div>
+                    <div>
+                        <SliderBase min={this.props.min} max={this.props.max} step={step} value={+this.state.value}
+                            onChange={v => this.setState({ value: '' + v })}
+                            onAfterChange={v => this.fire()} />
+                    </div>
+                </div>
+                <div>
+                    <TextBox value={this.state.value}  onChange={v => this.updateValue(v)} onBlur={() => this.fire()} onKeyPress={e => {
+                        if (isEnter(e)) this.fire();
+                    } } />
+                </div>
+            </div>
+        </div>;
+    }
+}
+
+/**
+ * The following code was adapted from react-components/slider library.
+ *
+ * The MIT License (MIT)
+ * Copyright (c) 2015-present Alipay.com, https://www.alipay.com/
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+
+    * The above copyright notice and this permission notice shall be included in
+    * all copies or substantial portions of the Software.
+
+    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+    * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+    * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+    * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+    * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+    * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+    * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+    */
+
+function classNames(_classes: { [name: string]: boolean | number }) {
+    let classes = [];
+    let hasOwn = {}.hasOwnProperty;
+
+    for (let i = 0; i < arguments.length; i++) {
+        let arg = arguments[i];
+        if (!arg) continue;
+
+        let argType = typeof arg;
+
+        if (argType === 'string' || argType === 'number') {
+            classes.push(arg);
+        } else if (Array.isArray(arg)) {
+            classes.push(classNames.apply(null, arg));
+        } else if (argType === 'object') {
+            for (let key in arg) {
+                if (hasOwn.call(arg, key) && arg[key]) {
+                    classes.push(key);
+                }
+            }
+        }
+    }
+
+    return classes.join(' ');
+}
+
+function noop() {
+}
+
+function isNotTouchEvent(e: TouchEvent) {
+    return e.touches.length > 1 || (e.type.toLowerCase() === 'touchend' && e.touches.length > 0);
+}
+
+function getTouchPosition(vertical: boolean, e: TouchEvent) {
+    return vertical ? e.touches[0].clientY : e.touches[0].pageX;
+}
+
+function getMousePosition(vertical: boolean, e: MouseEvent) {
+    return vertical ? e.clientY : e.pageX;
+}
+
+function getHandleCenterPosition(vertical: boolean, handle: HTMLElement) {
+    const coords = handle.getBoundingClientRect();
+    return vertical ?
+        coords.top + (coords.height * 0.5) :
+        coords.left + (coords.width * 0.5);
+}
+
+function pauseEvent(e: Event) {
+    e.stopPropagation();
+    e.preventDefault();
+}
+
+export class Handle extends React.Component<Partial<HandleProps>, {}> {
+    render() {
+        const {
+            className,
+            tipFormatter,
+            vertical,
+            offset,
+            value,
+            index,
+        } = this.props as HandleProps;
+
+        const style = vertical ? { bottom: `${offset}%` } : { left: `${offset}%` };
+        return (
+            <div className={className} style={style} title={tipFormatter(value, index)}
+                />
+        );
+    }
+}
+
+export interface SliderBaseProps {
+    min: number,
+    max: number,
+    step?: number,
+    defaultValue?: number | number[],
+    value?: number | number[],
+    marks?: any,
+    included?: boolean,
+    className?: string,
+    prefixCls?: string,
+    disabled?: boolean,
+    children?: any,
+    onBeforeChange?: (value: number | number[]) => void,
+    onChange?: (value: number | number[]) => void,
+    onAfterChange?: (value: number | number[]) => void,
+    handle?: JSX.Element,
+    tipFormatter?: (value: number, index: number) => any,
+    dots?: boolean,
+    range?: boolean | number,
+    vertical?: boolean,
+    allowCross?: boolean,
+    pushable?: boolean | number,
+}
+
+export interface SliderBaseState {
+    handle: number | null,
+    recent: number,
+    bounds: number[]
+}
+
+export class SliderBase extends React.Component<SliderBaseProps, SliderBaseState> {
+    private sliderElement: HTMLElement | undefined = void 0;
+    private handleElements: (HTMLElement | undefined)[] = [];
+
+    constructor(props: SliderBaseProps) {
+        super(props);
+
+        const { range, min, max } = props;
+        const initialValue = range ? Array.apply(null, Array(+range + 1)).map(() => min) : min;
+        const defaultValue = ('defaultValue' in props ? props.defaultValue : initialValue);
+        const value = (props.value !== undefined ? props.value : defaultValue);
+
+        const bounds = (range ? value : [min, value]).map((v: number) => this.trimAlignValue(v));
+
+        let recent;
+        if (range && bounds[0] === bounds[bounds.length - 1] && bounds[0] === max) {
+            recent = 0;
+        } else {
+            recent = bounds.length - 1;
+        }
+
+        this.state = {
+            handle: null,
+            recent,
+            bounds,
+        };
+    }
+
+    public static defaultProps: SliderBaseProps = {
+        prefixCls: 'molstar-slider-base',
+        className: '',
+        min: 0,
+        max: 100,
+        step: 1,
+        marks: {},
+        handle: <Handle className='' vertical={false} offset={0} tipFormatter={v => v} value={0} index={0} />,
+        onBeforeChange: noop,
+        onChange: noop,
+        onAfterChange: noop,
+        tipFormatter: (value, index) => value,
+        included: true,
+        disabled: false,
+        dots: false,
+        range: false,
+        vertical: false,
+        allowCross: true,
+        pushable: false,
+    };
+
+    private dragOffset = 0;
+    private startPosition = 0;
+    private startValue = 0;
+    private _getPointsCache: any = void 0;
+
+    componentWillReceiveProps(nextProps: SliderBaseProps) {
+        if (!('value' in nextProps || 'min' in nextProps || 'max' in nextProps)) return;
+
+        const { bounds } = this.state;
+        if (nextProps.range) {
+            const value = nextProps.value || bounds;
+            const nextBounds = (value as number[]).map((v: number) => this.trimAlignValue(v, nextProps));
+            if (nextBounds.every((v: number, i: number) => v === bounds[i])) return;
+
+            this.setState({ bounds: nextBounds } as SliderBaseState);
+            if (bounds.some(v => this.isValueOutOfBounds(v, nextProps))) {
+                this.props.onChange!(nextBounds);
+            }
+        } else {
+            const value = nextProps.value !== undefined ? nextProps.value : bounds[1];
+            const nextValue = this.trimAlignValue(value as number, nextProps);
+            if (nextValue === bounds[1] && bounds[0] === nextProps.min) return;
+
+            this.setState({ bounds: [nextProps.min, nextValue] } as SliderBaseState);
+            if (this.isValueOutOfBounds(bounds[1], nextProps)) {
+                this.props.onChange!(nextValue);
+            }
+        }
+    }
+
+    onChange(state: this['state']) {
+        const props = this.props;
+        const isNotControlled = !('value' in props);
+        if (isNotControlled) {
+            this.setState(state);
+        } else if (state.handle !== undefined) {
+            this.setState({ handle: state.handle } as SliderBaseState);
+        }
+
+        const data = { ...this.state, ...(state as any) };
+        const changedValue = props.range ? data.bounds : data.bounds[1];
+        props.onChange!(changedValue);
+    }
+
+    onMouseDown(e: MouseEvent) {
+        if (e.button !== 0) { return; }
+
+        let position = getMousePosition(this.props.vertical!, e);
+        if (!this.isEventFromHandle(e)) {
+            this.dragOffset = 0;
+        } else {
+            const handlePosition = getHandleCenterPosition(this.props.vertical!, e.target as HTMLElement);
+            this.dragOffset = position - handlePosition;
+            position = handlePosition;
+        }
+        this.onStart(position);
+        this.addDocumentEvents('mouse');
+        pauseEvent(e);
+    }
+
+    onMouseMove(e: MouseEvent) {
+        const position = getMousePosition(this.props.vertical!, e);
+        this.onMove(e, position - this.dragOffset);
+    }
+
+    onMove(e: MouseEvent | TouchEvent, position: number) {
+        pauseEvent(e);
+        const props = this.props;
+        const state = this.state;
+
+        let diffPosition = position - this.startPosition;
+        diffPosition = this.props.vertical ? -diffPosition : diffPosition;
+        const diffValue = diffPosition / this.getSliderLength() * (props.max - props.min);
+
+        const value = this.trimAlignValue(this.startValue + diffValue);
+        const oldValue = state.bounds[state.handle!];
+        if (value === oldValue) return;
+
+        const nextBounds = [...state.bounds];
+        nextBounds[state.handle!] = value;
+        let nextHandle = state.handle!;
+        if (props.pushable !== false) {
+            const originalValue = state.bounds[nextHandle];
+            this.pushSurroundingHandles(nextBounds, nextHandle, originalValue);
+        } else if (props.allowCross) {
+            nextBounds.sort((a, b) => a - b);
+            nextHandle = nextBounds.indexOf(value);
+        }
+        this.onChange({
+            handle: nextHandle,
+            bounds: nextBounds,
+        } as SliderBaseState);
+    }
+
+    onStart(position: number) {
+        const props = this.props;
+        props.onBeforeChange!(this.getValue());
+
+        const value = this.calcValueByPos(position);
+        this.startValue = value;
+        this.startPosition = position;
+
+        const state = this.state;
+        const { bounds } = state;
+
+        let valueNeedChanging = 1;
+        if (this.props.range) {
+            let closestBound = 0;
+            for (let i = 1; i < bounds.length - 1; ++i) {
+                if (value > bounds[i]) { closestBound = i; }
+            }
+            if (Math.abs(bounds[closestBound + 1] - value) < Math.abs(bounds[closestBound] - value)) {
+                closestBound = closestBound + 1;
+            }
+            valueNeedChanging = closestBound;
+
+            const isAtTheSamePoint = (bounds[closestBound + 1] === bounds[closestBound]);
+            if (isAtTheSamePoint) {
+                valueNeedChanging = state.recent;
+            }
+
+            if (isAtTheSamePoint && (value !== bounds[closestBound + 1])) {
+                valueNeedChanging = value < bounds[closestBound + 1] ? closestBound : closestBound + 1;
+            }
+        }
+
+        this.setState({
+            handle: valueNeedChanging,
+            recent: valueNeedChanging,
+        } as SliderBaseState);
+
+        const oldValue = state.bounds[valueNeedChanging];
+        if (value === oldValue) return;
+
+        const nextBounds = [...state.bounds];
+        nextBounds[valueNeedChanging] = value;
+        this.onChange({ bounds: nextBounds } as SliderBaseState);
+    }
+
+    onTouchMove(e: TouchEvent) {
+        if (isNotTouchEvent(e)) {
+            this.end('touch');
+            return;
+        }
+
+        const position = getTouchPosition(this.props.vertical!, e);
+        this.onMove(e, position - this.dragOffset);
+    }
+
+    onTouchStart(e: TouchEvent) {
+        if (isNotTouchEvent(e)) return;
+
+        let position = getTouchPosition(this.props.vertical!, e);
+        if (!this.isEventFromHandle(e)) {
+            this.dragOffset = 0;
+        } else {
+            const handlePosition = getHandleCenterPosition(this.props.vertical!, e.target as HTMLElement);
+            this.dragOffset = position - handlePosition;
+            position = handlePosition;
+        }
+        this.onStart(position);
+        this.addDocumentEvents('touch');
+        pauseEvent(e);
+    }
+
+    /**
+     * Returns an array of possible slider points, taking into account both
+     * `marks` and `step`. The result is cached.
+     */
+    getPoints() {
+        const { marks, step, min, max } = this.props;
+        const cache = this._getPointsCache;
+        if (!cache || cache.marks !== marks || cache.step !== step) {
+            const pointsObject = { ...marks };
+            if (step !== null) {
+                for (let point = min; point <= max; point += step!) {
+                    pointsObject[point] = point;
+                }
+            }
+            const points = Object.keys(pointsObject).map(parseFloat);
+            points.sort((a, b) => a - b);
+            this._getPointsCache = { marks, step, points };
+        }
+        return this._getPointsCache.points;
+    }
+
+    getPrecision(step: number) {
+        const stepString = step.toString();
+        let precision = 0;
+        if (stepString.indexOf('.') >= 0) {
+            precision = stepString.length - stepString.indexOf('.') - 1;
+        }
+        return precision;
+    }
+
+    getSliderLength() {
+        const slider = this.sliderElement;
+        if (!slider) {
+            return 0;
+        }
+
+        return this.props.vertical ? slider.clientHeight : slider.clientWidth;
+    }
+
+    getSliderStart() {
+        const slider = this.sliderElement as HTMLElement;
+        const rect = slider.getBoundingClientRect();
+
+        return this.props.vertical ? rect.top : rect.left;
+    }
+
+    getValue(): number {
+        const { bounds } = this.state;
+        return (this.props.range ? bounds : bounds[1]) as number;
+    }
+
+    private eventHandlers = {
+        'touchmove': (e: TouchEvent) => this.onTouchMove(e),
+        'touchend': (e: TouchEvent) => this.end('touch'),
+        'mousemove': (e: MouseEvent) => this.onMouseMove(e),
+        'mouseup': (e: MouseEvent) => this.end('mouse'),
+    }
+
+    addDocumentEvents(type: 'touch' | 'mouse') {
+        if (type === 'touch') {
+            document.addEventListener('touchmove', this.eventHandlers.touchmove);
+            document.addEventListener('touchend', this.eventHandlers.touchend);
+        } else if (type === 'mouse') {
+            document.addEventListener('mousemove', this.eventHandlers.mousemove);
+            document.addEventListener('mouseup', this.eventHandlers.mouseup);
+        }
+    }
+
+    calcOffset(value: number) {
+        const { min, max } = this.props;
+        const ratio = (value - min) / (max - min);
+        return ratio * 100;
+    }
+
+    calcValue(offset: number) {
+        const { vertical, min, max } = this.props;
+        const ratio = Math.abs(offset / this.getSliderLength());
+        const value = vertical ? (1 - ratio) * (max - min) + min : ratio * (max - min) + min;
+        return value;
+    }
+
+    calcValueByPos(position: number) {
+        const pixelOffset = position - this.getSliderStart();
+        const nextValue = this.trimAlignValue(this.calcValue(pixelOffset));
+        return nextValue;
+    }
+
+    end(type: 'mouse' | 'touch') {
+        this.removeEvents(type);
+        this.props.onAfterChange!(this.getValue());
+        this.setState({ handle: null } as SliderBaseState);
+    }
+
+    isEventFromHandle(e: Event) {
+        for (const h of this.handleElements) {
+            if (h === e.target) return true;
+        }
+        return false;
+
+        // return this.state.bounds.some((x, i) => e.target
+
+        // (
+        //     //this.handleElements[i] && e.target === ReactDOM.findDOMNode(this.handleElements[i])
+        // ));
+    }
+
+    isValueOutOfBounds(value: number, props: SliderBaseProps) {
+        return value < props.min || value > props.max;
+    }
+
+    pushHandle(bounds: number[], handle: number, direction: number, amount: number) {
+        const originalValue = bounds[handle];
+        let currentValue = bounds[handle];
+        while (direction * (currentValue - originalValue) < amount) {
+            if (!this.pushHandleOnePoint(bounds, handle, direction)) {
+                // can't push handle enough to create the needed `amount` gap, so we
+                // revert its position to the original value
+                bounds[handle] = originalValue;
+                return false;
+            }
+            currentValue = bounds[handle];
+        }
+        // the handle was pushed enough to create the needed `amount` gap
+        return true;
+    }
+
+    pushHandleOnePoint(bounds: number[], handle: number, direction: number) {
+        const points = this.getPoints();
+        const pointIndex = points.indexOf(bounds[handle]);
+        const nextPointIndex = pointIndex + direction;
+        if (nextPointIndex >= points.length || nextPointIndex < 0) {
+            // reached the minimum or maximum available point, can't push anymore
+            return false;
+        }
+        const nextHandle = handle + direction;
+        const nextValue = points[nextPointIndex];
+        const { pushable: threshold } = this.props;
+        const diffToNext = direction * (bounds[nextHandle] - nextValue);
+        if (!this.pushHandle(bounds, nextHandle, direction, +threshold! - diffToNext)) {
+            // couldn't push next handle, so we won't push this one either
+            return false;
+        }
+        // push the handle
+        bounds[handle] = nextValue;
+        return true;
+    }
+
+    pushSurroundingHandles(bounds: number[], handle: number, originalValue: number) {
+        const { pushable: threshold } = this.props;
+        const value = bounds[handle];
+
+        let direction = 0;
+        if (bounds[handle + 1] - value < threshold!) {
+            direction = +1;
+        } else if (value - bounds[handle - 1] < threshold!) {
+            direction = -1;
+        }
+
+        if (direction === 0) { return; }
+
+        const nextHandle = handle + direction;
+        const diffToNext = direction * (bounds[nextHandle] - value);
+        if (!this.pushHandle(bounds, nextHandle, direction, +threshold! - diffToNext)) {
+            // revert to original value if pushing is impossible
+            bounds[handle] = originalValue;
+        }
+    }
+
+    removeEvents(type: 'touch' | 'mouse') {
+        if (type === 'touch') {
+            document.removeEventListener('touchmove', this.eventHandlers.touchmove);
+            document.removeEventListener('touchend', this.eventHandlers.touchend);
+        } else if (type === 'mouse') {
+            document.removeEventListener('mousemove', this.eventHandlers.mousemove);
+            document.removeEventListener('mouseup', this.eventHandlers.mouseup);
+        }
+    }
+
+    trimAlignValue(v: number, nextProps?: SliderBaseProps) {
+        const { handle, bounds } = (this.state || {}) as this['state'];
+        const { marks, step, min, max, allowCross } = { ...this.props, ...(nextProps || {}) } as SliderBaseProps;
+
+        let val = v;
+        if (val <= min) {
+            val = min;
+        }
+        if (val >= max) {
+            val = max;
+        }
+        /* eslint-disable eqeqeq */
+        if (!allowCross && handle != null && handle > 0 && val <= bounds[handle - 1]) {
+            val = bounds[handle - 1];
+        }
+        if (!allowCross && handle != null && handle < bounds.length - 1 && val >= bounds[handle + 1]) {
+            val = bounds[handle + 1];
+        }
+        /* eslint-enable eqeqeq */
+
+        const points = Object.keys(marks).map(parseFloat);
+        if (step !== null) {
+            const closestStep = (Math.round((val - min) / step!) * step!) + min;
+            points.push(closestStep);
+        }
+
+        const diffs = points.map((point) => Math.abs(val - point));
+        const closestPoint = points[diffs.indexOf(Math.min.apply(Math, diffs))];
+
+        return step !== null ? parseFloat(closestPoint.toFixed(this.getPrecision(step!))) : closestPoint;
+    }
+
+    render() {
+        const {
+            handle,
+            bounds,
+        } = this.state;
+        const {
+            className,
+            prefixCls,
+            disabled,
+            vertical,
+            dots,
+            included,
+            range,
+            step,
+            marks,
+            max, min,
+            tipFormatter,
+            children,
+        } = this.props;
+
+        const customHandle = this.props.handle;
+
+        const offsets = bounds.map(v => this.calcOffset(v));
+
+        const handleClassName = `${prefixCls}-handle`;
+
+        const handlesClassNames = bounds.map((v, i) => classNames({
+            [handleClassName]: true,
+            [`${handleClassName}-${i + 1}`]: true,
+            [`${handleClassName}-lower`]: i === 0,
+            [`${handleClassName}-upper`]: i === bounds.length - 1,
+        }));
+
+        const isNoTip = (step === null) || (tipFormatter === null);
+
+        const commonHandleProps = {
+            prefixCls,
+            noTip: isNoTip,
+            tipFormatter,
+            vertical,
+        };
+
+        this.handleElements = [];
+        const handles = bounds.map((v, i) => React.cloneElement(customHandle!, {
+            ...commonHandleProps,
+            className: handlesClassNames[i],
+            value: v,
+            offset: offsets[i],
+            dragging: handle === i,
+            index: i,
+            key: i,
+            ref: (h: any) => this.handleElements.push(h)  //`handle-${i}`,
+        }));
+        if (!range) { handles.shift(); }
+
+        const isIncluded = included || range;
+
+        const tracks: JSX.Element[] = [];
+        // for (let i = 1; i < bounds.length; ++i) {
+        //     const trackClassName = classNames({
+        //         [`${prefixCls}-track`]: true,
+        //         [`${prefixCls}-track-${i}`]: true,
+        //     });
+        //     tracks.push(
+        //         <Track className={trackClassName} vertical={vertical} included={isIncluded}
+        //             offset={offsets[i - 1]} length={offsets[i] - offsets[i - 1]} key={i}
+        //             />
+        //     );
+        // }
+
+        const sliderClassName = classNames({
+            [prefixCls!]: true,
+            [`${prefixCls}-with-marks`]: Object.keys(marks).length,
+            [`${prefixCls}-disabled`]: disabled!,
+            [`${prefixCls}-vertical`]: this.props.vertical!,
+            [className!]: !!className,
+        });
+
+        return (
+            <div ref={e => this.sliderElement = e!} className={sliderClassName}
+                onTouchStart={disabled ? noop : this.onTouchStart.bind(this)}
+                onMouseDown={disabled ? noop : this.onMouseDown.bind(this)}
+                >
+                <div className={`${prefixCls}-rail`} />
+                {tracks}
+                <Steps prefixCls={prefixCls} vertical={vertical} marks={marks} dots={dots} step={step}
+                    included={isIncluded} lowerBound={bounds[0]}
+                    upperBound={bounds[bounds.length - 1]} max={max} min={min}
+                    />
+                {handles}
+                <Marks className={`${prefixCls}-mark`} vertical={vertical!} marks={marks}
+                    included={isIncluded!} lowerBound={bounds[0]}
+                    upperBound={bounds[bounds.length - 1]} max={max} min={min}
+                    />
+                {children}
+            </div>
+        );
+    }
+}
+
+export interface HandleProps {
+    className: string,
+    vertical: boolean,
+    offset: number,
+    tipFormatter: (v: number, index: number) => any,
+    value: number,
+    index: number,
+}
+
+interface MarksProps {
+    className: string,
+    vertical: boolean,
+    marks: any,
+    included: boolean | number,
+    upperBound: number,
+    lowerBound: number,
+    max: number,
+    min: number
+}
+const Marks = ({ className, vertical, marks, included, upperBound, lowerBound, max, min }: MarksProps) => {
+    const marksKeys = Object.keys(marks);
+    const marksCount = marksKeys.length;
+    const unit = 100 / (marksCount - 1);
+    const markWidth = unit * 0.9;
+
+    const range = max - min;
+    const elements = marksKeys.map(parseFloat).sort((a, b) => a - b).map((point) => {
+        const isActived = (!included && point === upperBound) ||
+            (included && point <= upperBound && point >= lowerBound);
+        const markClassName = classNames({
+            [`${className}-text`]: true,
+            [`${className}-text-active`]: isActived,
+        });
+
+        const bottomStyle = {
+            // height: markWidth + '%',
+            marginBottom: '-50%',
+            bottom: `${(point - min) / range * 100}%`,
+        };
+
+        const leftStyle = {
+            width: `${markWidth}%`,
+            marginLeft: `${-markWidth / 2}%`,
+            left: `${(point - min) / range * 100}%`,
+        };
+
+        const style = vertical ? bottomStyle : leftStyle;
+
+        const markPoint = marks[point];
+        const markPointIsObject = typeof markPoint === 'object' && !React.isValidElement(markPoint);
+        const markLabel = markPointIsObject ? markPoint.label : markPoint;
+        const markStyle = markPointIsObject ? { ...style, ...markPoint.style } : style;
+        return (<span className={markClassName} style={markStyle} key={point}>
+            {markLabel}
+        </span>);
+    });
+
+    return <div className={className}>{elements}</div>;
+};
+
+function calcPoints(vertical: boolean, marks: any, dots: boolean, step: number, min: number, max: number) {
+    const points = Object.keys(marks).map(parseFloat);
+    if (dots) {
+        for (let i = min; i <= max; i = i + step) {
+            if (points.indexOf(i) >= 0) continue;
+            points.push(i);
+        }
+    }
+    return points;
+}
+
+const Steps = ({ prefixCls, vertical, marks, dots, step, included,
+    lowerBound, upperBound, max, min }: any) => {
+    const range = max - min;
+    const elements = calcPoints(vertical, marks, dots, step, min, max).map((point) => {
+        const offset = `${Math.abs(point - min) / range * 100}%`;
+        const style = vertical ? { bottom: offset } : { left: offset };
+
+        const isActived = (!included && point === upperBound) ||
+            (included && point <= upperBound && point >= lowerBound);
+        const pointClassName = classNames({
+            [`${prefixCls}-dot`]: true,
+            [`${prefixCls}-dot-active`]: isActived,
+        });
+
+        return <span className={pointClassName} style={style} key={point} />;
+    });
+
+    return <div className={`${prefixCls}-step`}>{elements}</div>;
+};

+ 98 - 0
src/mol-app/ui/entity/tree.tsx

@@ -0,0 +1,98 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react'
+
+import { View } from '../view';
+import { EntityTreeController } from '../../controller/entity/tree';
+import { Controller } from '../../controller/controller';
+import { AnyEntity, RootEntity } from 'mol-view/state/entity';
+import { AnyTransform, SpacefillUpdate, UrlToData, DataToCif, FileToData, CifToMmcif, MmcifToModel, ModelToStructure, StructureToSpacefill, MmcifFileToSpacefill } from 'mol-view/state/transform';
+
+function getTransforms(entity: AnyEntity): AnyTransform[] {
+    const transforms: AnyTransform[] = []
+    switch (entity.kind) {
+        case 'root':
+            transforms.push(MmcifFileToSpacefill)
+            break;
+        case 'url':
+            transforms.push(UrlToData)
+            break;
+        case 'file':
+            transforms.push(FileToData)
+            break;
+        case 'data':
+            transforms.push(DataToCif)
+            break;
+        case 'cif':
+            transforms.push(CifToMmcif)
+            break;
+        case 'mmcif':
+            transforms.push(MmcifToModel)
+            break;
+        case 'model':
+            transforms.push(ModelToStructure)
+            break;
+        case 'structure':
+            transforms.push(StructureToSpacefill)
+            break;
+        case 'spacefill':
+            transforms.push(SpacefillUpdate)
+            break;
+    }
+    return transforms
+}
+
+export class Entity extends View<Controller<any>, {}, { entity: AnyEntity}> {
+    render() {
+        const entity = this.props.entity
+
+        return <div className='molstar-entity-tree-entry'>
+            <div className='molstar-entity-tree-entry-body'>
+                <div className='molstar-entity-tree-entry-label-wrap'>
+                    <button
+                        className='molstar-entity-tree-entry-label'
+                        onClick={() => {
+                            console.log(entity)
+                            this.controller.context.currentEntity.next(entity)
+                            this.controller.context.currentTransforms.next(getTransforms(entity))
+                        }}
+                    >
+                        <span>{entity.id} - {entity.kind}</span>
+                    </button>
+                </div>
+            </div>
+        </div>;
+    }
+}
+
+export class EntityTree extends View<EntityTreeController, {}, {}> {
+    render() {
+        const entities: JSX.Element[] = []
+        const state = this.controller.state.getValue()
+        if (state) {
+            state.entities.forEach(e => {
+                entities.push(
+                    <div key={e.id}>
+                        <Entity controller={this.controller} entity={e}></Entity>
+                    </div>
+                )
+            })
+        }
+
+        return <div className='molstar-entity-tree'>
+            <div className='molstar-entity-tree-root'>
+                <Entity controller={this.controller} entity={RootEntity}></Entity>
+            </div>
+            <div className='molstar-entity-tree-children'>
+                <div>{entities}</div>
+            </div>
+        </div>;
+    }
+}

+ 89 - 0
src/mol-app/ui/layout.tsx

@@ -0,0 +1,89 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ */
+
+import * as React from 'react'
+import { LayoutController, LayoutTarget, LayoutRegion, CollapsedControlsLayout } from '../controller/layout';
+import { View } from './view';
+
+export class Layout extends View<LayoutController, { }, { }> {
+
+    private renderTarget(target: LayoutTarget) {
+        const statics: any[] = [];
+        const scrollable: any[] = [];
+
+        for (let c of target.components) {
+            if (c.isStatic) statics.push(<c.view key={c.key} controller={c.controller} />);
+            else scrollable.push(<c.view key={c.key} controller={c.controller} />);
+        }
+
+        return <div key={target.cssClass} className={'molstar-layout-region molstar-layout-' + target.cssClass}>
+            { statics.length ? <div className='molstar-layout-static'>{statics}</div> : void 0 }
+            { scrollable.length ? <div className='molstar-layout-scrollable'>{scrollable}</div> : void 0 }
+        </div>;
+    }
+
+    private updateTarget(name: string, regionType: LayoutRegion, layout: { regions: any[], layoutClass: string }) {
+        const state = this.controller.latestState;
+        const regionStates = state.regionStates;
+        const region = this.controller.targets[regionType];
+        let show: boolean;
+
+        if (state.hideControls) {
+            show = regionStates !== void 0 && regionStates[regionType] === 'Sticky' && region.components.length > 0;
+        } else if (regionStates && regionStates[regionType] === 'Hidden') {
+            show = false;
+        } else {
+            show = region.components.length > 0;
+        }
+
+        if (show) {
+            layout.regions.push(this.renderTarget(region));
+        } else {
+            layout.layoutClass += ' molstar-layout-hide-' + name;
+        }
+    }
+
+    render() {
+        let layoutClass = '';
+
+        const state = this.controller.latestState;
+        let layoutType: string;
+
+        if (state.isExpanded) {
+            layoutType = 'molstar-layout-expanded';
+        } else {
+            layoutType = 'molstar-layout-standard ';
+            switch (state.collapsedControlsLayout) {
+                case CollapsedControlsLayout.Outside: layoutType += 'molstar-layout-standard-outside'; break;
+                case CollapsedControlsLayout.Landscape: layoutType += 'molstar-layout-standard-landscape'; break;
+                case CollapsedControlsLayout.Portrait: layoutType += 'molstar-layout-standard-portrait'; break;
+                default: layoutType += 'molstar-layout-standard-outside'; break;
+            }
+        }
+
+        const targets = this.controller.targets;
+        const regions = [this.renderTarget(targets[LayoutRegion.Main])];
+
+        const layout = { regions, layoutClass };
+        this.updateTarget('top', LayoutRegion.Top, layout);
+        this.updateTarget('right', LayoutRegion.Right, layout);
+        this.updateTarget('bottom', LayoutRegion.Bottom, layout);
+        this.updateTarget('left', LayoutRegion.Left, layout);
+        layoutClass = layout.layoutClass;
+
+        let root = targets[LayoutRegion.Root].components.map(c => <c.view key={c.key} controller={c.controller} />);
+
+        return <div className='molstar-plugin'>
+            <div className={'molstar-plugin-content ' + layoutType}>
+                <div className={layoutClass}>
+                    {regions}
+                    {root}
+                </div>
+            </div>
+        </div>;
+    }
+}

+ 64 - 0
src/mol-app/ui/misc/jobs.tsx

@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ */
+
+import * as React from 'react'
+import { JobInfo, JobsController } from '../../controller/misc/jobs';
+import { Button } from '../controls/common';
+import { View } from '../view';
+
+class JobState extends React.Component<{ info: JobInfo, isSmall?: boolean }, {}> {
+    render() {
+        const info = this.props.info;
+        return <div className='molstar-task-state'>
+            <div>
+                { info.abort ? <Button onClick={() => info.abort!.call(null) } style='remove'
+                    icon='abort' title='Abort' customClass='molstar-btn-icon'
+                /> : void 0 }
+                <div>
+                    {info.name}: {info.message}
+                </div>
+            </div>
+        </div>;
+    }
+}
+
+export class Overlay extends View<JobsController, {}, {}> {
+    render() {
+        const state = this.controller.latestState;
+
+        if (!state.jobs!.count()) return <div className='molstar-empty-control' />
+
+        const jobs: any[] = [];
+        state.jobs!.forEach((t, k) => jobs.push(<JobState key={k} info={t!} />));
+
+        return <div className='molstar-overlay'>
+            <div className='molstar-overlay-background' />
+            <div className='molstar-overlay-content-wrap'>
+                <div className='molstar-overlay-content'>
+                    <div>
+                        {jobs}
+                    </div>
+                </div>
+            </div>
+        </div>;
+    }
+}
+
+export class BackgroundJobs extends View<JobsController, {}, {}> {
+    render() {
+        const state = this.controller.latestState;
+
+        if (!state.jobs!.count()) return <div className='molstar-empty-control' />
+
+        const jobs: any[] = [];
+        state.jobs!.forEach((t, k) => jobs.push(<JobState key={k} info={t!} isSmall={true} />));
+
+        return <div className='molstar-background-jobs'>
+            {jobs}
+        </div>;
+    }
+}

+ 69 - 0
src/mol-app/ui/misc/log.tsx

@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ */
+
+import * as React from 'react'
+import { View } from '../view';
+import { LogController } from '../../controller/misc/log';
+import { CommonEvents } from '../../event/basic';
+import { formatTime } from 'mol-util';
+import { Logger } from '../../service/logger';
+
+export class Log extends View<LogController, {}, {}> {
+
+    private wrapper: HTMLDivElement | undefined = void 0;
+
+    componentWillMount() {
+        super.componentWillMount();
+        this.subscribe(CommonEvents.LayoutChanged.getStream(this.controller.context), () => this.scrollToBottom());
+    }
+
+    componentDidUpdate() {
+        this.scrollToBottom();
+    }
+
+    private scrollToBottom() {
+        const log = this.wrapper;
+        if (log) log.scrollTop = log.scrollHeight - log.clientHeight - 1;
+    }
+
+    render() {
+        const entries = this.controller.latestState.entries;
+
+        return <div className='molstar-log-wrap'>
+            <div className='molstar-log' ref={log => this.wrapper = log!}>
+                <ul className='molstar-list-unstyled'>
+                    {entries.map((entry, i, arr) => {
+
+                        let label: JSX.Element;
+                        let e = entry!;
+                        switch (e.type) {
+                            case Logger.EntryType.Error:
+                                label = <span className='label label-danger'>Error</span>;
+                                break;
+                            case Logger.EntryType.Warning:
+                                label = <span className='label label-warning'>Warning</span>;
+                                break;
+                            case Logger.EntryType.Info:
+                                label = <span className='label label-info'>Info</span>;
+                                break;
+                            default:
+                                label = <span></span>
+                        }
+
+                        let t = formatTime(e.timestamp);
+                        return <li key={i}>
+                            <div className={'molstar-log-entry-badge molstar-log-entry-' + Logger.EntryType[e.type].toLowerCase()} />
+                            {label}
+                            <div className='molstar-log-timestamp'>{t}</div>
+                            <div className='molstar-log-entry'>{e.message}</div>
+                        </li>;
+                    }) }
+                </ul>
+            </div>
+        </div>;
+    }
+}

+ 29 - 0
src/mol-app/ui/transform/file-loader.tsx

@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react'
+import { View } from '../view';
+import { FileInput } from '../controls/common';
+import { TransformListController } from '../../controller/transform/list';
+import { FileEntity } from 'mol-view/state/entity';
+import { MmcifFileToSpacefill } from 'mol-view/state/transform';
+import { StateContext } from 'mol-view/state/context';
+
+export class FileLoader extends View<TransformListController, {}, { ctx: StateContext }> {
+    render() {
+        return <div className='molstar-file-loader'>
+            <FileInput
+                accept='*.cif'
+                onChange={files => {
+                    if (files) {
+                        const fileEntity = FileEntity.ofFile(this.props.ctx, files[0])
+                        MmcifFileToSpacefill.apply(this.props.ctx, fileEntity)
+                    }
+                }}
+            />
+        </div>;
+    }
+}

+ 74 - 0
src/mol-app/ui/transform/list.tsx

@@ -0,0 +1,74 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react'
+
+import { View } from '../view';
+import { Controller } from '../../controller/controller';
+import { TransformListController } from '../../controller/transform/list';
+import { AnyTransform } from 'mol-view/state/transform';
+import { Spacefill } from './spacefill';
+import { AnyEntity } from 'mol-view/state/entity';
+import { FileLoader } from './file-loader';
+import { ModelToStructure } from './model';
+
+function getTransformComponent(controller: TransformListController, entity: AnyEntity, transform: AnyTransform) {
+    switch (transform.kind) {
+        case 'file-to-spacefill':
+            return <FileLoader controller={controller} ctx={controller.context.stage.ctx}></FileLoader>
+        case 'model-to-structure':
+            return <ModelToStructure controller={controller} entity={entity} transform={transform} ctx={controller.context.stage.ctx}></ModelToStructure>
+        case 'spacefill-update':
+            return <Spacefill controller={controller} entity={entity} transform={transform} ctx={controller.context.stage.ctx}></Spacefill>
+    }
+    return <Transform controller={controller} entity={entity} transform={transform}></Transform>
+}
+
+export class Transform extends View<Controller<any>, {}, { transform: AnyTransform, entity: AnyEntity }> {
+    render() {
+        const { transform, entity } = this.props
+
+        return <div className='molstar-transformer-wrapper'>
+            <div className='molstar-panel molstar-control molstar-transformer'>
+                <div className='molstar-panel-header'>
+                    <button
+                        className='molstar-btn molstar-btn-link molstar-panel-expander'
+                        onClick={(e)=> {
+                            console.log(transform, entity)
+                        }}
+                    >
+                        <span>[{transform.kind}] {transform.inputKind} -> {transform.outputKind}</span>
+                    </button>
+                </div>
+            </div>
+        </div>;
+    }
+}
+
+export class TransformList extends View<TransformListController, {}, {}> {
+    render() {
+        const transforms: JSX.Element[] = []
+        const state = this.controller.state.getValue()
+        if (state && state.entity) {
+            const entity = state.entity
+            if (entity) {
+                state.transforms.forEach(t => {
+                    transforms.push(
+                        <div
+                            key={`${t.inputKind}|${t.outputKind}`}
+                            children={getTransformComponent(this.controller, entity, t)}
+                        />
+                    )
+                })
+            }
+        }
+
+        return <div className='molstar-transform-view' children={transforms} />;
+    }
+}

+ 67 - 0
src/mol-app/ui/transform/model.tsx

@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react'
+
+import { View } from '../view';
+import { Controller } from '../../controller/controller';
+import { ModelEntity } from 'mol-view/state/entity';
+import { StructureProps, ModelToStructure as ModelToStructureTransform } from 'mol-view/state/transform'
+import { StateContext } from 'mol-view/state/context';
+
+export class ModelToStructure extends View<Controller<any>, StructureProps, { transform: ModelToStructureTransform, entity: ModelEntity, ctx: StateContext }> {
+    state = {
+        assembly: ''
+    }
+
+    create(state?: Partial<StructureProps>) {
+        const { transform, entity, ctx } = this.props
+        console.log('create structure', transform, entity)
+        const newState = { ...this.state, ...state }
+        this.setState(newState)
+        transform.apply(ctx, entity, newState)
+    }
+
+    render() {
+        const { transform, entity } = this.props
+
+        const assemblyOptions = entity.value[0].symmetry.assemblies.map((value, idx) => {
+            return <option key={value.id} value={value.id}>{value.details}</option>
+        })
+
+        return <div className='molstar-transformer-wrapper'>
+            <div className='molstar-panel molstar-control molstar-transformer molstar-panel-expanded'>
+                <div className='molstar-panel-header'>
+                    <button
+                        className='molstar-btn molstar-btn-link molstar-panel-expander'
+                        onClick={() => this.create()}
+                    >
+                        <span>[{transform.kind}] {transform.inputKind} -> {transform.outputKind}</span>
+                    </button>
+                </div>
+                <div className='molstar-panel-body'>
+                    <div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <span>Details</span>
+                            <div>
+                                <select
+                                    className='molstar-form-control'
+                                    value={this.state.assembly}
+                                    onChange={(e) => this.create({ assembly: e.target.value })}
+                                >
+                                    {assemblyOptions}
+                                </select>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>;
+    }
+}

+ 143 - 0
src/mol-app/ui/transform/spacefill.tsx

@@ -0,0 +1,143 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react'
+
+import { View } from '../view';
+import { Controller } from '../../controller/controller';
+import { SpacefillEntity } from 'mol-view/state/entity';
+import { SpacefillUpdate } from 'mol-view/state/transform'
+import { StateContext } from 'mol-view/state/context';
+import { ColorTheme } from 'mol-geo/theme';
+import { Color, ColorNames } from 'mol-util/color';
+
+export const ColorThemeInfo = {
+    'atom-index': {},
+    'chain-id': {},
+    'element-symbol': {},
+    'instance-index': {},
+    'uniform': {}
+}
+export type ColorThemeInfo = keyof typeof ColorThemeInfo
+
+interface SpacefillState {
+    doubleSided: boolean
+    detail: number
+    colorTheme: ColorTheme
+    colorValue: Color
+}
+
+export class Spacefill extends View<Controller<any>, SpacefillState, { transform: SpacefillUpdate, entity: SpacefillEntity, ctx: StateContext }> {
+    state = {
+        doubleSided: true,
+        detail: 2,
+        colorTheme: { name: 'element-symbol' } as ColorTheme,
+        colorValue: 0x000000
+    }
+
+    update(state?: Partial<SpacefillState>) {
+        const { transform, entity, ctx } = this.props
+        console.log('update spacefill', transform, entity)
+        const newState = { ...this.state, ...state }
+        this.setState(newState)
+        transform.apply(ctx, entity, newState)
+    }
+
+    render() {
+        const { transform } = this.props
+
+        const sphereDetailOptions = [0, 1, 2, 3].map((value, idx) => {
+            return <option key={value} value={value}>{value.toString()}</option>
+        })
+
+        const colorThemeOptions = Object.keys(ColorThemeInfo).map((name, idx) => {
+            return <option key={name} value={name}>{name}</option>
+        })
+
+        const colorValueOptions = Object.keys(ColorNames).map((name, idx) => {
+            return <option key={name} value={(ColorNames as any)[name]}>{name}</option>
+        })
+
+        return <div className='molstar-transformer-wrapper'>
+            <div className='molstar-panel molstar-control molstar-transformer molstar-panel-expanded'>
+                <div className='molstar-panel-header'>
+                    <button
+                        className='molstar-btn molstar-btn-link molstar-panel-expander'
+                        onClick={() => this.update()}
+                    >
+                        <span>[{transform.kind}] {transform.inputKind} -> {transform.outputKind}</span>
+                    </button>
+                </div>
+                <div className='molstar-panel-body'>
+                    <div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <span>Sphere detail</span>
+                            <div>
+                                <select
+                                    className='molstar-form-control'
+                                    value={this.state.detail}
+                                    onChange={(e) => this.update({ detail: parseInt(e.target.value) })}
+                                >
+                                    {sphereDetailOptions}
+                                </select>
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <span>Color theme</span>
+                            <div>
+                                <select
+                                    className='molstar-form-control'
+                                    value={this.state.colorTheme.name}
+                                    onChange={(e) => {
+                                        const colorThemeName = e.target.value as ColorThemeInfo
+                                        if (colorThemeName === 'uniform') {
+                                            this.update({
+                                                colorTheme: {
+                                                    name: colorThemeName,
+                                                    value: this.state.colorValue
+                                                }
+                                            })
+                                        } else {
+                                            this.update({
+                                                colorTheme: { name: colorThemeName }
+                                            })
+                                        }
+                                    }}
+                                >
+                                    {colorThemeOptions}
+                                </select>
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <span>Color value</span>
+                            <div>
+                                <select
+                                    className='molstar-form-control'
+                                    value={this.state.colorValue}
+                                    onChange={(e) => {
+                                        const colorValue = parseInt(e.target.value)
+                                        this.update({
+                                            colorTheme: {
+                                                name: 'uniform',
+                                                value: colorValue
+                                            },
+                                            colorValue
+                                        })
+                                    }}
+                                >
+                                    {colorValueOptions}
+                                </select>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>;
+    }
+}

+ 93 - 0
src/mol-app/ui/view.tsx

@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ */
+
+import * as React from 'react'
+import { Observable, Subscription } from 'rxjs';
+import { merge, shallowEqual } from 'mol-util'
+import { Context } from '../context/context';
+import { Controller } from '../controller/controller';
+
+export abstract class PureView<State, Props, ViewState> extends React.Component<{
+    state: State
+    onChange: (s: State) => void
+} & Props, ViewState> {
+
+    protected update(s: State) {
+        let ns = merge<State>(this.props.state, s);
+        if (ns !== this.props.state as any) this.props.onChange(ns);
+    }
+
+    shouldComponentUpdate(nextProps: any, nextState: any) {
+        return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState);
+    }
+}
+
+export abstract class ComponentView<Props> extends React.Component<{ context: Context } & Props, {}> {
+
+    // shouldComponentUpdate(nextProps: any, nextState: any) {
+    //     return !shallowEqual(this.props, nextProps);
+    // }
+
+    private subs: Subscription[] = [];
+    protected subscribe<T>(stream: Observable<T>, obs: (n: T) => void) {
+        let sub = stream.subscribe(obs);
+        this.subs.push(sub);
+        return sub;
+    }
+
+    protected unsubscribe(sub: Subscription) {
+        let idx = this.subs.indexOf(sub);
+        for (let i = idx; i < this.subs.length - 1; i++) {
+            this.subs[i] = this.subs[i + 1];
+        }
+        sub.unsubscribe();
+        this.subs.pop();
+    }
+
+    componentWillUnmount() {
+        for (let s of this.subs) s.unsubscribe();
+        this.subs = [];
+    }
+}
+
+export abstract class ObserverView<P, S> extends React.Component<P, S> {
+    private subs: Subscription[] = [];
+
+    protected subscribe<T>(stream: Observable<T>, obs: (n: T) => void) {
+        let sub = stream.subscribe(obs);
+        this.subs.push(sub);
+        return sub;
+    }
+
+    protected unsubscribe(sub: Subscription) {
+        let idx = this.subs.indexOf(sub);
+        for (let i = idx; i < this.subs.length - 1; i++) {
+            this.subs[i] = this.subs[i + 1];
+        }
+        sub.unsubscribe();
+        this.subs.pop();
+    }
+
+    componentWillUnmount() {
+        for (let s of this.subs) s.unsubscribe();
+        this.subs = [];
+    }
+}
+
+export abstract class View<T extends Controller<any>, State, CustomProps>
+    extends ObserverView<{ controller: T } & CustomProps, State> {
+
+    public get controller(): T {
+        return this.props.controller as any;
+    }
+
+    componentWillMount() {
+        this.subscribe(this.controller.state as any, (s) => {
+            this.forceUpdate()
+        });
+    }
+}

Деякі файли не було показано, через те що забагато файлів було змінено