changeset 2083:ad7ab7027907

clean up non-elements; get the lit elements at least to work with autoformat
author drewp@bigasterisk.com
date Wed, 25 May 2022 01:11:41 -0700
parents b62c78f35380
children c4eab47d3c83
files light9/web/live/ActiveSettings.ts light9/web/live/GraphToControls.ts light9/web/live/Light9Listbox.ts light9/web/live/Light9LiveControl.ts light9/web/live/Light9LiveControls.ts light9/web/live/Light9LiveDeviceControl.ts
diffstat 6 files changed, 741 insertions(+), 680 deletions(-) [+]
line wrap: on
line diff
--- a/light9/web/live/ActiveSettings.ts	Wed May 25 01:10:57 2022 -0700
+++ b/light9/web/live/ActiveSettings.ts	Wed May 25 01:11:41 2022 -0700
@@ -1,7 +1,18 @@
-class ActiveSettings {
-  graph: any;
-  settings: any;
-  keyForSetting: any;
+import debug from "debug";
+import { NamedNode } from "n3";
+import { SyncedGraph } from "../SyncedGraph";
+const log = debug("active");
+
+interface SettingRow {
+  setting: NamedNode;
+  onChangeFunc: (x: null | undefined | string) => void;
+  jsValue?: string;
+}
+
+export class ActiveSettings {
+  graph: SyncedGraph;
+  settings: Map<string, SettingRow>;
+  keyForSetting: Map<string, string>;
   onChanged: any;
   constructor(graph: any) {
     // The settings we're showing (or would like to but the widget
@@ -11,14 +22,12 @@
     this.settings = new Map();
     this.keyForSetting = new Map(); // setting uri str -> dev+attr
 
-
-
     // Registered graphValueChanged funcs, by dev+attr. Kept even when
     // settings are deleted.
     this.onChanged = new Map();
   }
 
-  addSettingsRow(device: { value: string; }, deviceAttr: { value: string; }, setting: { value: string; }, value: any) {
+  addSettingsRow(device: NamedNode, deviceAttr: NamedNode, setting: NamedNode, value: any) {
     const key = device.value + " " + deviceAttr.value;
     if (this.settings.has(key)) {
       throw new Error("repeated setting on " + key);
@@ -29,7 +38,7 @@
     this.settings.set(key, {
       setting,
       onChangeFunc: this.onChanged[key],
-      jsValue: value
+      jsValue: value,
     });
     this.keyForSetting.set(setting.value, key);
     if (this.onChanged[key] != null) {
@@ -37,54 +46,65 @@
     }
   }
 
-  has(setting: { value: any; }) {
+  has(setting: { value: any }) {
     return this.keyForSetting.has(setting.value);
   }
 
-  setValue(setting: { value: any; }, value: any) {
-    const row = this.settings.get(this.keyForSetting.get(setting.value));
+  setValue(setting: { value: any }, value: any) {
+    const k = this.keyForSetting.get(setting.value);
+    if (!k) throw new Error("not found");
+    const row = this.settings.get(k);
+    if (!row) throw new Error(`${setting.value} not found`);
     row.jsValue = value;
-    if (row.onChangeFunc != null) { return row.onChangeFunc(value); }
+    if (row.onChangeFunc != null) {
+      return row.onChangeFunc(value);
+    }
   }
 
-  registerWidget(device: { value: string; }, deviceAttr: { value: string; }, graphValueChanged: any) {
+  registerWidget(device: NamedNode, deviceAttr: NamedNode, graphValueChanged: any) {
     const key = device.value + " " + deviceAttr.value;
     this.onChanged[key] = graphValueChanged;
 
-    if (this.settings.has(key)) {
-      const row = this.settings.get(key);
-      row.onChangeFunc = graphValueChanged;
-      return row.onChangeFunc(row.jsValue);
-    }
+    const row = this.settings.get(key);
+    if (!row) throw new Error(`${key} not found`);
+
+    row.onChangeFunc = graphValueChanged;
+    row.onChangeFunc(row.jsValue);
   }
 
-  effectSettingLookup(device: { value: string; }, attr: { value: string; }) {
+  effectSettingLookup(device: NamedNode, attr: NamedNode): NamedNode | null {
     const key = device.value + " " + attr.value;
-    if (this.settings.has(key)) {
-      return this.settings.get(key).setting;
+    const row = this.settings.get(key);
+    if (row) {
+      return row.setting;
     }
 
     return null;
   }
 
-  deleteSetting(setting: { value: string; }) {
-    log('deleteSetting ' + setting.value);
+  deleteSetting(setting: NamedNode) {
+    log("deleteSetting " + setting.value);
     const key = this.keyForSetting.get(setting.value);
+    if (!key) throw new Error("not found");
     const row = this.settings.get(key);
-    if ((row != null) && !row.setting.equals(setting)) {
-      throw new Error('corrupt row for ' + setting.value);
+    if (row && !row.setting.equals(setting)) {
+      throw new Error("corrupt row for " + setting.value);
     }
-    if ((row != null ? row.onChangeFunc : undefined) != null) { row.onChangeFunc(null); }
+    if (row) {
+      row.onChangeFunc(null);
+    }
     this.settings.delete(key);
-    return this.keyForSetting.delete(setting);
+    return this.keyForSetting.delete(setting.value);
   }
 
   clear() {
-    new Map(this.settings).forEach(function (row: { onChangeFunc: (arg0: any) => any; }, key: any) {
-      if (row.onChangeFunc != null) { return row.onChangeFunc(null); }
+    this.settings.forEach((row: { onChangeFunc: (arg0: any) => any }, key: any) => {
+      if (row.onChangeFunc != null) {
+        return row.onChangeFunc(null);
+      }
     });
     this.settings.clear();
-    return this.keyForSetting.clear();
+    this.keyForSetting.clear();
   }
 
   forAll(cb: (arg0: any) => any) {
--- a/light9/web/live/GraphToControls.ts	Wed May 25 01:10:57 2022 -0700
+++ b/light9/web/live/GraphToControls.ts	Wed May 25 01:11:41 2022 -0700
@@ -1,16 +1,34 @@
+import debug from "debug";
+import { BlankNode, Literal, NamedNode, Quad_Object, Quad_Predicate, Quad_Subject, Term } from "n3";
+import { some } from "underscore";
+import { Patch } from "../patch";
+import { SyncedGraph } from "../SyncedGraph";
+import { ActiveSettings } from "./ActiveSettings";
+const log = debug("g2c");
 
-const valuePred = function(graph: { Uri: (arg0: any) => any; }, attr: { equals: (arg0: any) => any; }) {
-  const U = (x: string) => graph.Uri(x);
-  const scaledAttributeTypes = [U(':color'), U(':brightness'), U(':uv')];
-  if (_.some(scaledAttributeTypes,
-      (            x: any) => attr.equals(x))) { return U(':scaledValue'); } else { return U(':value'); }
+const valuePred = function (graph: SyncedGraph, attr: NamedNode) {
+  const U = graph.U();
+  const scaledAttributeTypes = [U(":color"), U(":brightness"), U(":uv")];
+  if (some(scaledAttributeTypes, (x: NamedNode) => attr.equals(x))) {
+    return U(":scaledValue");
+  } else {
+    return U(":value");
+  }
 };
 
-const log = debug('live');
+function isUri(x: Term | number | string): x is NamedNode {
+  return typeof x == "object" && x.termType == "NamedNode";
+}
 
+type ControlValue = number | string | NamedNode | null;
 // Like element.set(path, newArray), but minimizes splices.
 // Dotted paths don't work yet.
-const syncArray = function(element: this, path: string, newArray: { length?: any; }, isElementEqual: { (a: any, b: any): boolean; (arg0: any, arg1: any): any; }) {
+const syncArray = function (
+  element: Element,
+  path: string,
+  newArray: { length?: any },
+  isElementEqual: { (a: any, b: any): boolean; (arg0: any, arg1: any): any }
+) {
   let pos = 0;
   let newPos = 0;
 
@@ -20,79 +38,76 @@
         pos += 1;
         newPos += 1;
       } else {
-        element.splice('devices', pos, 1);
+        element.splice("devices", pos, 1);
       }
     } else {
-      element.push('devices', newArray[newPos]);
+      element.push("devices", newArray[newPos]);
       pos += 1;
       newPos += 1;
     }
   }
 
   if (pos < element[path].length) {
-    return element.splice('devices', pos, element[path].length - pos);
+    return element.splice("devices", pos, element[path].length - pos);
   }
 };
 
-
-class GraphToControls {
-  graph: any;
+export class GraphToControls {
   activeSettings: ActiveSettings;
-  effect: any;
-  ctx: any;
+  effect: NamedNode | null = null;
+  ctx: NamedNode | null = null;
   // More efficient bridge between liveControl widgets and graph edits,
   // as opposed to letting each widget scan the graph and push lots of
   // tiny patches to it.
-  constructor(graph: any) {
-    this.graph = graph;
+  constructor(public graph: SyncedGraph) {
     this.activeSettings = new ActiveSettings(this.graph);
-    this.effect = null;
   }
 
-  ctxForEffect(effect: { value: { replace: (arg0: string, arg1: string) => any; }; }) {
-    return this.graph.Uri(effect.value.replace(
-      "light9.bigasterisk.com/effect",
-      "light9.bigasterisk.com/show/dance2019/effect"));
+  ctxForEffect(effect: NamedNode): NamedNode {
+    return this.graph.Uri(effect.value.replace("light9.bigasterisk.com/effect", "light9.bigasterisk.com/show/dance2019/effect"));
   }
 
-  setEffect(effect: any) {
+  setEffect(effect: NamedNode) {
     this.clearSettings();
     this.effect = effect;
-    this.ctx = this.ctxForEffect(this.effect);
+    this.ctx = this.ctxForEffect(effect);
     // are these going to pile up? consider @graph.triggerHandler('GTC sync')
-    return this.graph.runHandler(this.syncFromGraph.bind(this), 'GraphToControls sync');
+    return this.graph.runHandler(this.syncFromGraph.bind(this), "GraphToControls sync");
   }
 
   newEffect() {
     // wrong- this should be our editor's scratch effect, promoted to a
     // real one when you name it.
-    const U = (x: string) => this.graph.Uri(x);
-    const effect = this.graph.nextNumberedResource(U('http://light9.bigasterisk.com/effect/effect'));
+    const U = this.graph.U();
+    const effect = this.graph.nextNumberedResource(U("http://light9.bigasterisk.com/effect/effect"));
     const ctx = this.ctxForEffect(effect);
-    const quad = (s: any, p: any, o: any) => this.graph.Quad(s, p, o, ctx);
+    const quad = (s: Quad_Subject, p: Quad_Predicate, o: Quad_Object) => this.graph.Quad(s, p, o, ctx);
 
     const addQuads = [
-      quad(effect, U('rdf:type'), U(':Effect')),
-      quad(effect, U('rdfs:label'), this.graph.Literal(effect.value.replace(/.*\//, ""))),
-      quad(effect, U(':publishAttr'), U(':strength'))
+      quad(effect, U("rdf:type"), U(":Effect")),
+      quad(effect, U("rdfs:label"), this.graph.Literal(effect.value.replace(/.*\//, ""))),
+      quad(effect, U(":publishAttr"), U(":strength")),
     ];
-    const patch = { addQuads, delQuads: [] };
-    log('init new effect', patch);
+    const patch = { adds: addQuads, dels: [] } as Patch;
+    log("init new effect", patch);
     this.graph.applyAndSendPatch(patch);
     return effect;
   }
 
   syncFromGraph() {
-    const U = (x: string) => this.graph.Uri(x);
-    if (!this.effect) { return; }
-    log('syncFromGraph', this.effect);
+    const U = this.graph.U();
+    if (!this.effect) {
+      return;
+    }
+    log("syncFromGraph", this.effect);
 
     const toClear = new Set(this.activeSettings.allSettingsStr());
 
-    for (let setting of Array.from(this.graph.objects(this.effect, U(':setting')))) {
-      var value: { id: { match: (arg0: {}) => any; }; };
-      const dev = this.graph.uriValue(setting, U(':device'));
-      const devAttr = this.graph.uriValue(setting, U(':deviceAttr'));
+    for (let setting of Array.from(this.graph.objects(this.effect, U(":setting")))) {
+      if (!isUri(setting)) throw new Error();
+      var value: { id: { match: (arg0: {}) => any } };
+      const dev = this.graph.uriValue(setting, U(":device"));
+      const devAttr = this.graph.uriValue(setting, U(":deviceAttr"));
 
       const pred = valuePred(this.graph, devAttr);
       try {
@@ -127,80 +142,91 @@
     return this.activeSettings.registerWidget(device, deviceAttr, graphValueChanged);
   }
 
-  shouldBeStored(deviceAttr: any, value: string | number) {
+  shouldBeStored(deviceAttr: any, value: ControlValue) {
     // this is a bug for zoom=0, since collector will default it to
     // stick at the last setting if we don't explicitly send the
     // 0. rx/ry similar though not the exact same deal because of
     // their remap.
-    return (value != null) && (value !== 0) && (value !== '#000000');
+    return value != null && value !== 0 && value !== "#000000";
   }
 
   emptyEffect() {
     return this.activeSettings.forAll(this._removeEffectSetting.bind(this));
   }
 
-  controlChanged(device: any, deviceAttr: any, value: string) {
+  controlChanged(device: NamedNode, deviceAttr: NamedNode, value: ControlValue) {
     // todo: controls should be disabled if there's no effect and they won't do anything.
-    if (!this.effect) { return; }
+    if (!this.effect) {
+      return;
+    }
 
     // value is float or #color or (Uri or null)
-    if ((value === undefined) || ((typeof value === "number") && isNaN(value)) || ((typeof value === "object") && (value !== null) && !value.id)) {
-      throw new Error("controlChanged sent bad value " + value);
-    }
+
     const effectSetting = this.activeSettings.effectSettingLookup(device, deviceAttr);
 
     // sometimes this misses an existing setting, which leads to a mess
     if (this.shouldBeStored(deviceAttr, value)) {
-      if ((effectSetting == null)) {
+      if (effectSetting == null) {
         return this._addEffectSetting(device, deviceAttr, value);
       } else {
         return this._patchExistingEffectSetting(effectSetting, deviceAttr, value);
       }
     } else {
-      return this._removeEffectSetting(effectSetting);
+      if (effectSetting !== null) {
+        return this._removeEffectSetting(effectSetting);
+      }
     }
   }
 
-  _nodeForValue(value: { id: any; }) {
-    if (value.id != null) {
+  _nodeForValue(value: ControlValue) {
+    if (value === null) {
+      throw new Error("no value");
+    }
+    if (isUri(value)) {
       return value;
     }
     return this.graph.prettyLiteral(value);
   }
 
-  _addEffectSetting(device: any, deviceAttr: { value: any; }, value: any) {
-    log('change: _addEffectSetting', deviceAttr.value, value);
+  _addEffectSetting(device: NamedNode, deviceAttr: NamedNode, value: ControlValue) {
+    log("change: _addEffectSetting", deviceAttr.value, value);
     const U = (x: string) => this.graph.Uri(x);
-    const quad = (s: any, p: any, o: any) => this.graph.Quad(s, p, o, this.ctx);
-    const effectSetting = this.graph.nextNumberedResource(this.effect.value + '_set');
+    const quad = (s: Quad_Subject, p: Quad_Predicate, o: Quad_Object) => this.graph.Quad(s, p, o, this.ctx);
+    if (!this.effect) throw new Error("effect unset");
+    const effectSetting = this.graph.nextNumberedResource(this.effect.value + "_set");
     this.activeSettings.addSettingsRow(device, deviceAttr, effectSetting, value);
     const addQuads = [
-      quad(this.effect, U(':setting'), effectSetting),
-      quad(effectSetting, U(':device'), device),
-      quad(effectSetting, U(':deviceAttr'), deviceAttr),
-      quad(effectSetting, valuePred(this.graph, deviceAttr), this._nodeForValue(value))
+      quad(this.effect, U(":setting"), effectSetting),
+      quad(effectSetting, U(":device"), device),
+      quad(effectSetting, U(":deviceAttr"), deviceAttr),
+      quad(effectSetting, valuePred(this.graph, deviceAttr), this._nodeForValue(value)),
     ];
-    const patch = { addQuads, delQuads: [] };
-    log('save', patch);
+    const patch = { adds: addQuads, dels: [] } as Patch;
+    log("save", patch);
     return this.graph.applyAndSendPatch(patch);
   }
 
-  _patchExistingEffectSetting(effectSetting: { value: any; }, deviceAttr: any, value: any) {
-    log('change: patch existing', effectSetting.value);
+  _patchExistingEffectSetting(effectSetting: NamedNode, deviceAttr: NamedNode, value: ControlValue) {
+    if (!this.ctx) throw new Error("no ctx");
+    log("change: patch existing", effectSetting.value);
     this.activeSettings.setValue(effectSetting, value);
-    return this.graph.patchObject(effectSetting, valuePred(this.graph, deviceAttr), this._nodeForValue(value), this.ctx);
+    return this.graph.patchObject(
+      effectSetting, //
+      valuePred(this.graph, deviceAttr),
+      this._nodeForValue(value),
+      this.ctx
+    );
   }
 
-  _removeEffectSetting(effectSetting: { value: any; }) {
+  _removeEffectSetting(effectSetting: NamedNode) {
     const U = (x: string) => this.graph.Uri(x);
-    const quad = (s: any, p: any, o: any) => this.graph.Quad(s, p, o, this.ctx);
     if (effectSetting != null) {
-      log('change: _removeEffectSetting', effectSetting.value);
-      const toDel = [quad(this.effect, U(':setting'), effectSetting, this.ctx)];
+      log("change: _removeEffectSetting", effectSetting.value);
+      const toDel = [this.graph.Quad(this.effect, U(":setting"), effectSetting, this.ctx)];
       for (let q of Array.from(this.graph.graph.getQuads(effectSetting))) {
         toDel.push(q);
       }
-      this.graph.applyAndSendPatch({ delQuads: toDel, addQuads: [] });
+      this.graph.applyAndSendPatch({ dels: toDel, adds: [] } as Patch);
       return this.activeSettings.deleteSetting(effectSetting);
     }
   }
--- a/light9/web/live/Light9Listbox.ts	Wed May 25 01:10:57 2022 -0700
+++ b/light9/web/live/Light9Listbox.ts	Wed May 25 01:11:41 2022 -0700
@@ -1,72 +1,71 @@
-<dom-module id="light9-listbox">
-  <template>
-    <style>
-     paper-listbox {
-         --paper-listbox-background-color: none;
-         --paper-listbox-color: white;
-         --paper-listbox: {
-             /* measure biggest item? use flex for columns? */
-             column-width: 9em;
-         }
-     }
-     paper-item {
-         --paper-item-min-height: 0;
-         --paper-item: {
-             display: block;
-             border: 1px outset #0f440f;
-             margin: 0 1px 5px 0;
-             background: #0b1d0b;
-         }
-     }
-     paper-item.iron-selected {
-         background: #7b7b4a;
-     }
-    </style>
-    <paper-listbox id="list"
-                   selected="{{value}}"
-                   attr-for-selected="uri"
-                   on-focus-changed="selectOnFocus"
-    >
-      <paper-item on-focus="selectOnFocus">None</paper-item>
-      <template is="dom-repeat" items="{{choices}}">
-        <paper-item on-focus="selectOnFocus" uri="{{item.uri}}">{{item.label}}</paper-item>
-      </template>
-    </paper-listbox>
+import debug from "debug";
+const log = debug("listbox");
+import { css, html, LitElement } from "lit";
+import { customElement, property } from "lit/decorators.js";
+
+@customElement("light9-listbox")
+export class Light9Listbox extends LitElement {
+  static styles = [
+    css`
+      paper-listbox {
+        --paper-listbox-background-color: none;
+        --paper-listbox-color: white;
+        --paper-listbox: {
+          /* measure biggest item? use flex for columns? */
+          column-width: 9em;
+        }
+      }
+      paper-item {
+        --paper-item-min-height: 0;
+        --paper-item: {
+          display: block;
+          border: 1px outset #0f440f;
+          margin: 0 1px 5px 0;
+          background: #0b1d0b;
+        }
+      }
+      paper-item.iron-selected {
+        background: #7b7b4a;
+      }
+    `,
+  ];
 
-  </template>
-  <script>
-   HTMLImports.whenReady(function () {
-     Polymer({
-       is: "light9-listbox",
-       properties: {
-         choices: { type: Array },
-         value: { type: String, notify: true },
-       },
-       observers: ['onValue(value)'],
-       selectOnFocus: function(ev) {
-         if (ev.target.uri === undefined) {
-           // *don't* clear for this, or we can't cycle through all choices (including none) with up/down keys
-           //this.clear();
-           //return;
-         }
-         this.value = ev.target.uri;
-         
-       },
-       onValue: function(value) {
-         if (value === null) {
-           this.clear();
-         }
-       },
-       clear: function() {
-         this.async(function() {
-           this.querySelectorAll('paper-item').forEach(
-             function(item) { item.blur(); });
-           this.value = undefined;
-         }.bind(this));
-
-       },
-       
-     });
-   });
-  </script>
-</dom-module>
\ No newline at end of file
+  render() {
+    return html`
+      <paper-listbox id="list" selected="{{value}}" attr-for-selected="uri" on-focus-changed="selectOnFocus">
+        <paper-item on-focus="selectOnFocus">None</paper-item>
+        <template is="dom-repeat" items="{{choices}}">
+          <paper-item on-focus="selectOnFocus" uri="{{item.uri}}">{{item.label}}</paper-item>
+        </template>
+      </paper-listbox>
+    `;
+  }
+  properties: {
+    choices: { type: Array };
+    value: { type: String; notify: true };
+  };
+  observers: ["onValue(value)"];
+  selectOnFocus(ev) {
+    if (ev.target.uri === undefined) {
+      // *don't* clear for this, or we can't cycle through all choices (including none) with up/down keys
+      //this.clear();
+      //return;
+    }
+    this.value = ev.target.uri;
+  }
+  onValue(value) {
+    if (value === null) {
+      this.clear();
+    }
+  }
+  clear() {
+    this.async(
+      function () {
+        this.querySelectorAll("paper-item").forEach(function (item) {
+          item.blur();
+        });
+        this.value = undefined;
+      }.bind(this)
+    );
+  }
+}
--- a/light9/web/live/Light9LiveControl.ts	Wed May 25 01:10:57 2022 -0700
+++ b/light9/web/live/Light9LiveControl.ts	Wed May 25 01:11:41 2022 -0700
@@ -1,70 +1,76 @@
-
-<dom-module id="light9-live-control">
-<template>
-  <style>
-   #colorControls {
-       display: flex;
-       align-items: center;
-   }
-   #colorControls > * {
-       margin: 0 3px;
-   }
-   #colorControls paper-slider {
+import debug from "debug";
+const log = debug("control");
+import { css, html, LitElement } from "lit";
+import { customElement, property } from "lit/decorators.js";
 
-   }
-   paper-slider { width: 100%; height: 25px; }
-  </style>
-
-  <style is="custom-style">
-   paper-slider {
-       --paper-slider-knob-color: var(--paper-red-500);
-       --paper-slider-active-color: var(--paper-red-500);
+@customElement("light9-live-control")
+export class Light9LiveControl extends LitElement {
+  static styles = [
+    css`
+      #colorControls {
+        display: flex;
+        align-items: center;
+      }
+      #colorControls > * {
+        margin: 0 3px;
+      }
+      #colorControls paper-slider {
+      }
+      paper-slider {
+        width: 100%;
+        height: 25px;
+      }
 
-       --paper-slider-font-color: white;
-       --paper-slider-input: {
-           width: 75px;
+      paper-slider {
+        --paper-slider-knob-color: var(--paper-red-500);
+        --paper-slider-active-color: var(--paper-red-500);
 
-           background: black;
-           display: inline-block;
-       }
-   }
-   
-  </style>
+        --paper-slider-font-color: white;
+        --paper-slider-input: {
+          width: 75px;
 
-  <template is="dom-if" if="{{deviceAttrRow.useSlider}}">
-    <paper-slider min="0"
-                  max="{{deviceAttrRow.max}}"
-                  step=".001"
-                  editable
-                  content-type="application/json"
-                  value="{{sliderWriteValue}}"
-                  immediate-value="{{immediateSlider}}"></paper-slider>
-  </template>
-  <template is="dom-if" if="{{deviceAttrRow.useColor}}">
-    <div id="colorControls">
-      <button on-click="goBlack">0.0</button>
-      <light9-color-picker color="{{value}}"></light9-color-picker>
-      
-    </div>
-  </template>
-  <template is="dom-if" if="{{deviceAttrRow.useChoice}}">
-    <light9-listbox choices="{{deviceAttrRow.choices}}" value="{{choiceValue}}">
-    </light9-listbox>
-  </template>
+          background: black;
+          display: inline-block;
+        }
+      }
+    `,
+  ];
 
-</template>
-
-</dom-module>
+  render() {
+    return html`
+      <template is="dom-if" if="{{deviceAttrRow.useSlider}}">
+        <paper-slider
+          min="0"
+          max="{{deviceAttrRow.max}}"
+          step=".001"
+          editable
+          content-type="application/json"
+          value="{{sliderWriteValue}}"
+          immediate-value="{{immediateSlider}}"
+        ></paper-slider>
+      </template>
+      <template is="dom-if" if="{{deviceAttrRow.useColor}}">
+        <div id="colorControls">
+          <button on-click="goBlack">0.0</button>
+          <light9-color-picker color="{{value}}"></light9-color-picker>
+        </div>
+      </template>
+      <template is="dom-if" if="{{deviceAttrRow.useChoice}}">
+        <light9-listbox choices="{{deviceAttrRow.choices}}" value="{{choiceValue}}"> </light9-listbox>
+      </template>
+    `;
+  }
 
-const coffeeElementSetupLight9LiveControl = (function() {
-    class Light9LiveControl extends Polymer.Element {
-      static is: string;
-      static getter_properties: {
-          graph: { type: any; notify: boolean; }; device: { type: any; }; deviceAttrRow: { type: any; }; // object returned from attrRow, below
-          value: { type: any; notify: boolean; }; // null, Uri, float, str
-          choiceValue: { type: any; }; immediateSlider: { notify: boolean; observer: string; }; sliderWriteValue: { ...; }; pickedChoice: { ...; }; graphToControls: { ...; };
-      };
-      static getter_observers: {};
+  graph: { type: any; notify: boolean; }
+  device: { type: any; };
+  deviceAttrRow: { type: any; }; // object returned from attrRow, below
+  value: { type: any; notify: boolean; }; // null, Uri, float, str
+  choiceValue: { type: any; };
+  immediateSlider: { notify: boolean; observer: string; };
+  sliderWriteValue: { ...; };
+  pickedChoice: { ...; };
+  graphToControls: { ...; };
+
       enableChange: boolean;
       value: any;
       immediateSlider: any;
@@ -74,88 +80,92 @@
       graphToControls: any;
       graph: any;
       pickedChoice: any;
-      static initClass() {
-        this.is = 'light9-live-control';
-        this.getter_properties = {
-          graph: { type: Object, notify: true },
-          device: { type: Object },
-          deviceAttrRow: { type: Object }, // object returned from attrRow, below
-          value: { type: Object, notify: true }, // null, Uri, float, str
-          choiceValue: { type: Object },
-        
-          immediateSlider: { notify: true, observer: 'onSlider' },
-          sliderWriteValue: { type: Number },
-    
-          pickedChoice: { observer: 'onChange' },
-          graphToControls: { type: Object }
-        };
-        this.getter_observers = [
-          'onChange(value)',
-          'onGraphToControls(graphToControls)',
-          'onChoice(choiceValue)'
-          ];
-      }
-      constructor() {
-        super();
-        this.enableChange = false; // until 1st graph read
-      }
-      onSlider() { return this.value = this.immediateSlider; }
-      goBlack() { return this.value = "#000000"; }
-      onGraphToControls(gtc: { register: (arg0: any, arg1: any, arg2: any) => void; }) {
-        gtc.register(this.device, this.deviceAttrRow.uri, this.graphValueChanged.bind(this));
-        return this.enableChange = true;
-      }
-        device(device: any, uri: any, arg2: any) {
-            throw new Error("Method not implemented.");
-        }
-  
-      graphValueChanged(v: { value: any, }) {
-        log('change: control gets', v);
-        this.enableChange = false;
-        if (v === null) {
-          this.clear();
-        } else {
-          this.value = v;
-        }
-        if (this.deviceAttrRow.useSlider) { this.sliderWriteValue = v; }
-        if (this.deviceAttrRow.useChoice) { this.choiceValue = (v === null ? v : v.value); }
-        return this.enableChange = true;
-      }
-  
-      onChoice(value: any) {
-        if ((this.graphToControls == null) || !this.enableChange) { return; }
-        if (value != null) {
-          value = this.graph.Uri(value);
-        } else {
-          value = null;
-        }
-        return this.graphToControls.controlChanged(this.device, this.deviceAttrRow.uri, value);
-      }
-      
-      onChange(value: any) {
-        if ((this.graphToControls == null) || !this.enableChange) { return; }
-        if ((typeof value === "number") && isNaN(value)) { return; } // let onChoice do it
-        //log('change: control tells graph', @deviceAttrRow.uri.value, value)
-        if (value === undefined) {
-          value = null;
-        }
-        return this.graphToControls.controlChanged(this.device, this.deviceAttrRow.uri, value);
-      }
-  
-      clear() {
-        this.pickedChoice = null;
-        this.sliderWriteValue = 0;
-        if (this.deviceAttrRow.useColor) {
-          return this.value = '#000000';
-        } else if (this.deviceAttrRow.useChoice) {
-          return this.value = (this.pickedChoice = null);
-        } else {
-          return this.value = (this.immediateSlider = 0);
-        }
-      }
+  static initClass() {
+    this.getter_properties = {
+      graph: { type: Object, notify: true },
+      device: { type: Object },
+      deviceAttrRow: { type: Object }, // object returned from attrRow, below
+      value: { type: Object, notify: true }, // null, Uri, float, str
+      choiceValue: { type: Object },
+
+      immediateSlider: { notify: true, observer: "onSlider" },
+      sliderWriteValue: { type: Number },
+
+      pickedChoice: { observer: "onChange" },
+      graphToControls: { type: Object },
+    };
+    this.getter_observers = ["onChange(value)", "onGraphToControls(graphToControls)", "onChoice(choiceValue)"];
+  }
+  constructor() {
+    super();
+    this.enableChange = false; // until 1st graph read
+  }
+  onSlider() {
+    return (this.value = this.immediateSlider);
+  }
+  goBlack() {
+    return (this.value = "#000000");
+  }
+  onGraphToControls(gtc: { register: (arg0: any, arg1: any, arg2: any) => void }) {
+    gtc.register(this.device, this.deviceAttrRow.uri, this.graphValueChanged.bind(this));
+    return (this.enableChange = true);
+  }
+  device(device: any, uri: any, arg2: any) {
+    throw new Error("Method not implemented.");
+  }
+
+  graphValueChanged(v: { value: any }) {
+    log("change: control gets", v);
+    this.enableChange = false;
+    if (v === null) {
+      this.clear();
+    } else {
+      this.value = v;
+    }
+    if (this.deviceAttrRow.useSlider) {
+      this.sliderWriteValue = v;
     }
-    
-    Light9LiveControl.initClass();
-    return Light9LiveControl;
-  })();
-  
\ No newline at end of file
+    if (this.deviceAttrRow.useChoice) {
+      this.choiceValue = v === null ? v : v.value;
+    }
+    return (this.enableChange = true);
+  }
+
+  onChoice(value: any) {
+    if (this.graphToControls == null || !this.enableChange) {
+      return;
+    }
+    if (value != null) {
+      value = this.graph.Uri(value);
+    } else {
+      value = null;
+    }
+    return this.graphToControls.controlChanged(this.device, this.deviceAttrRow.uri, value);
+  }
+
+  onChange(value: any) {
+    if (this.graphToControls == null || !this.enableChange) {
+      return;
+    }
+    if (typeof value === "number" && isNaN(value)) {
+      return;
+    } // let onChoice do it
+    //log('change: control tells graph', @deviceAttrRow.uri.value, value)
+    if (value === undefined) {
+      value = null;
+    }
+    return this.graphToControls.controlChanged(this.device, this.deviceAttrRow.uri, value);
+  }
+
+  clear() {
+    this.pickedChoice = null;
+    this.sliderWriteValue = 0;
+    if (this.deviceAttrRow.useColor) {
+      return (this.value = "#000000");
+    } else if (this.deviceAttrRow.useChoice) {
+      return (this.value = this.pickedChoice = null);
+    } else {
+      return (this.value = this.immediateSlider = 0);
+    }
+  }
+}
--- a/light9/web/live/Light9LiveControls.ts	Wed May 25 01:10:57 2022 -0700
+++ b/light9/web/live/Light9LiveControls.ts	Wed May 25 01:11:41 2022 -0700
@@ -1,178 +1,187 @@
-
-<dom-module id="light9-live-controls">
-<template>
-  <style>
-   :host {
-       display: flex;
-       flex-direction: column;
-   }
-   #preview {
-       width: 100%;
-   }
-   #deviceControls {
-       flex-grow: 1;
-       position: relative;
-       width: 100%;
-       overflow-y: auto;
-   }
-   
-   light9-live-device-control > div {
-       break-inside: avoid-column;
-   }
-   light9-live-device-control {
-
-   }
-  </style>
-  <rdfdb-synced-graph graph="{{graph}}"></rdfdb-synced-graph>
-
-  <h1>device control</h1>
-
-  <div id="save">
-    <div>
-      <button on-click="newEffect">New effect</button>
-      <edit-choice graph="{{graph}}" uri="{{effectChoice}}"></edit-choice>
-      <button on-click="clearAll">clear settings in this effect</button>
-    </div>
-  </div>
+import debug from "debug";
+const log = debug("controls");
+import { css, html, LitElement } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import { GraphToControls } from "./GraphToControls";
 
-  <div id="deviceControls">
-    <template is="dom-repeat" items="{{devices}}" as="device">
-      <light9-live-device-control
-        graph="{{graph}}"
-        uri="{{device.uri}}"
-        effect="{{effect}}"
-        graph-to-controls="{{graphToControls}}"
-      ></light9-live-device-control>
-    </template>
-  </div>
-  
-</template>
-</dom-module>
+@customElement("light9-live-controls")
+export class Light9LiveControls extends LitElement {
+  static styles = [
+    css`
+      :host {
+        display: flex;
+        flex-direction: column;
+      }
+      #preview {
+        width: 100%;
+      }
+      #deviceControls {
+        flex-grow: 1;
+        position: relative;
+        width: 100%;
+        overflow-y: auto;
+      }
 
-const coffeeElementSetupLight9LiveControls = (function() {
-    class Light9LiveControls extends Polymer.Element {
-      static is: string;
-      static getter_properties: {
-          graph: { type: any; notify: boolean; }; devices: { type: any; notify: boolean; value: {}; };
-          // string uri of the effect being edited, or null. This is the
-          // master value; GraphToControls follows.
-          effectChoice: { type: any; notify: boolean; value: any; }; graphToControls: { type: any; };
-      };
-      static getter_observers: {};
-      graphToControls: any;
-      okToWriteUrl: boolean;
-      currentSettings: {};
-      graph: any;
-      effectChoice: any;
-      static initClass() {
-        this.is = "light9-live-controls";
-        this.getter_properties = {
-          graph: { type: Object, notify: true },
-          devices: { type: Array, notify: true, value: [] },
-          // string uri of the effect being edited, or null. This is the
-          // master value; GraphToControls follows.
-          effectChoice: { type: String, notify: true, value: null },
-          graphToControls: { type: Object }
-        };
-        this.getter_observers = [
-          'onGraph(graph)',
-          'onEffectChoice(effectChoice)'
-          ];
+      light9-live-device-control > div {
+        break-inside: avoid-column;
+      }
+      light9-live-device-control {
       }
-  
-      constructor() {
-        super();
-        this.graphToControls = null;
-        this.okToWriteUrl = false;
-      }
-  
-      ready() {
-        super.ready(...arguments).ready();
-        return this.currentSettings = {};
-      }
-  
-      onGraph() {
-        this.graphToControls = new GraphToControls(this.graph);
-        this.graph.runHandler(this.update.bind(this), 'Light9LiveControls update');
-  
-        // need graph to be loaded, so we don't make double settings? not sure.
-        return setTimeout(this.setFromUrl.bind(this), 1);
-      }
-  
-      setFromUrl() {
-        // not a continuous bidi link between url and effect; it only reads
-        // the url when the page loads.
-        const effect = new URL(window.location.href).searchParams.get('effect');
-        if (effect != null) {
-          log('found url', effect);
-          this.effectChoice = effect;
-        }
-        return this.okToWriteUrl = true;
-      }
-  
-      writeToUrl(effectStr: any) {
-        if (!this.okToWriteUrl) { return; }
-        const u = new URL(window.location.href);
-        if (u.searchParams.get('effect') === effectStr) {
-          return;        
-        }
-        u.searchParams.set('effect', effectStr);
-        window.history.replaceState({}, "", u.href);
-        return log('wrote new url', u.href);
-      }
-  
-      newEffect() {
-        return this.effectChoice = this.graphToControls.newEffect().value;
+    `,
+  ];
+
+  render() {
+    return html`
+      <rdfdb-synced-graph graph="{{graph}}"></rdfdb-synced-graph>
+
+      <h1>device control</h1>
+
+      <div id="save">
+        <div>
+          <button on-click="newEffect">New effect</button>
+          <edit-choice graph="{{graph}}" uri="{{effectChoice}}"></edit-choice>
+          <button on-click="clearAll">clear settings in this effect</button>
+        </div>
+      </div>
+
+      <div id="deviceControls">
+        <template is="dom-repeat" items="{{devices}}" as="device">
+          <light9-live-device-control
+            graph="{{graph}}"
+            uri="{{device.uri}}"
+            effect="{{effect}}"
+            graph-to-controls="{{graphToControls}}"
+          ></light9-live-device-control>
+        </template>
+      </div>
+    `;
+  }
+
+  static getter_properties: {
+    graph: { type: any; notify: boolean };
+    devices: { type: any; notify: boolean; value: {} };
+    // string uri of the effect being edited, or null. This is the
+    // master value; GraphToControls follows.
+    effectChoice: { type: any; notify: boolean; value: any };
+    graphToControls: { type: any };
+  };
+  static getter_observers: {};
+  graphToControls: any;
+  okToWriteUrl: boolean;
+  currentSettings: {};
+  graph: any;
+  effectChoice: any;
+  static initClass() {
+    this.getter_properties = {
+      graph: { type: Object, notify: true },
+      devices: { type: Array, notify: true, value: [] },
+      // string uri of the effect being edited, or null. This is the
+      // master value; GraphToControls follows.
+      effectChoice: { type: String, notify: true, value: null },
+      graphToControls: { type: Object },
+    };
+    this.getter_observers = ["onGraph(graph)", "onEffectChoice(effectChoice)"];
+  }
+
+  constructor() {
+    super();
+    this.graphToControls = null;
+    this.okToWriteUrl = false;
+  }
+
+  ready() {
+    super.ready(...arguments).ready();
+    return (this.currentSettings = {});
+  }
+
+  onGraph() {
+    this.graphToControls = new GraphToControls(this.graph);
+    this.graph.runHandler(this.update.bind(this), "Light9LiveControls update");
+
+    // need graph to be loaded, so we don't make double settings? not sure.
+    return setTimeout(this.setFromUrl.bind(this), 1);
+  }
+
+  setFromUrl() {
+    // not a continuous bidi link between url and effect; it only reads
+    // the url when the page loads.
+    const effect = new URL(window.location.href).searchParams.get("effect");
+    if (effect != null) {
+      log("found url", effect);
+      this.effectChoice = effect;
+    }
+    return (this.okToWriteUrl = true);
+  }
+
+  writeToUrl(effectStr: any) {
+    if (!this.okToWriteUrl) {
+      return;
+    }
+    const u = new URL(window.location.href);
+    if (u.searchParams.get("effect") === effectStr) {
+      return;
+    }
+    u.searchParams.set("effect", effectStr);
+    window.history.replaceState({}, "", u.href);
+    return log("wrote new url", u.href);
+  }
+
+  newEffect() {
+    return (this.effectChoice = this.graphToControls.newEffect().value);
+  }
+
+  onEffectChoice() {
+    const U = (x: any) => this.graph.Uri(x);
+    if (this.effectChoice == null) {
+      // unlink
+      if (this.graphToControls != null) {
+        this.graphToControls.setEffect(null);
       }
-        
-      onEffectChoice() {
-        const U = (x: any) => this.graph.Uri(x);
-        if ((this.effectChoice == null)) {
-          // unlink
-          if (this.graphToControls != null) { this.graphToControls.setEffect(null); }
-        } else {
-          log('load', this.effectChoice);
-          if (this.graphToControls != null) { this.graphToControls.setEffect(this.graph.Uri(this.effectChoice)); }
-        }
-        return this.writeToUrl(this.effectChoice);
-      }
-   
-      clearAll() {
-        // clears the effect!
-        return this.graphToControls.emptyEffect();
-      }
-      
-      update() {
-        const U = (x: string) => this.graph.Uri(x);
-  
-        const newDevs = [];
-        for (let dc of Array.from(this.graph.sortedUris(this.graph.subjects(U('rdf:type'), U(':DeviceClass'))))) {
-          for (let dev of Array.from(this.graph.sortedUris(this.graph.subjects(U('rdf:type'), dc)))) {
-            if (this.graph.contains(dev, U(':hideInLiveUi'), null)) {
-              continue;
-            }
-            newDevs.push({uri: dev});
-          }
-        }
-  
-        //log("controls update now has #{newDevs.length} devices")
-        syncArray(this, 'devices', newDevs, (a: { uri: { value: any; }; }, b: { uri: { value: any; }; }) => a.uri.value === b.uri.value);
-  
-        return;
-  
-        // Tried css columns- big slowdown from relayout as I'm scrolling.
-        // Tried isotope- seems to only scroll to the right.
-        // Tried columnize- fails in jquery maybe from weird elements.
-      
-        // not sure how to get this run after the children are created
-        return setTimeout((() => $('#deviceControls').isotope({
-          // fitColumns would be nice, but it doesn't scroll vertically
-          layoutMode: 'masonry',
-          containerStyle: null
-          })), 2000);
+    } else {
+      log("load", this.effectChoice);
+      if (this.graphToControls != null) {
+        this.graphToControls.setEffect(this.graph.Uri(this.effectChoice));
       }
     }
-    
-    Light9LiveControls.initClass();
-    return Light9LiveControls;
-  })();
\ No newline at end of file
+    return this.writeToUrl(this.effectChoice);
+  }
+
+  clearAll() {
+    // clears the effect!
+    return this.graphToControls.emptyEffect();
+  }
+
+  update() {
+    const U = (x: string) => this.graph.Uri(x);
+
+    const newDevs = [];
+    for (let dc of Array.from(this.graph.sortedUris(this.graph.subjects(U("rdf:type"), U(":DeviceClass"))))) {
+      for (let dev of Array.from(this.graph.sortedUris(this.graph.subjects(U("rdf:type"), dc)))) {
+        if (this.graph.contains(dev, U(":hideInLiveUi"), null)) {
+          continue;
+        }
+        newDevs.push({ uri: dev });
+      }
+    }
+
+    //log("controls update now has #{newDevs.length} devices")
+    syncArray(this, "devices", newDevs, (a: { uri: { value: any } }, b: { uri: { value: any } }) => a.uri.value === b.uri.value);
+
+    return;
+
+    // Tried css columns- big slowdown from relayout as I'm scrolling.
+    // Tried isotope- seems to only scroll to the right.
+    // Tried columnize- fails in jquery maybe from weird elements.
+
+    // not sure how to get this run after the children are created
+    return setTimeout(
+      () =>
+        $("#deviceControls").isotope({
+          // fitColumns would be nice, but it doesn't scroll vertically
+          layoutMode: "masonry",
+          containerStyle: null,
+        }),
+      2000
+    );
+  }
+}
--- a/light9/web/live/Light9LiveDeviceControl.ts	Wed May 25 01:10:57 2022 -0700
+++ b/light9/web/live/Light9LiveDeviceControl.ts	Wed May 25 01:11:41 2022 -0700
@@ -1,200 +1,197 @@
-
-<dom-module id="light9-live-device-control">
-<template>
-  <style>
-   :host {
-       display: inline-block;
-   }
-   .device {
-       border: 2px solid #151e2d;
-       margin: 4px;
-       padding: 1px;
-       background: #171717;  /* deviceClass gradient added later */
-       break-inside: avoid-column;
-       width: 335px;
-       
-   }
-   .deviceAttr {
-       border-top: 1px solid #272727;
-       padding-bottom: 2px;
-       display: flex;
-   }
-   .deviceAttr > span {
+import debug from "debug";
+const log = debug("devcontrol");
+import { css, html, LitElement } from "lit";
+import { customElement, property } from "lit/decorators.js";
 
-   }
-   .deviceAttr > light9-live-control {
-       flex-grow: 1;
-   }
-   h2 {
-       font-size: 110%;
-       padding: 4px;
-       margin-top: 0;
-       margin-bottom: 0;
-   }
-   .device, h2 {
-       border-top-right-radius: 15px;
-   }
+@customElement("light9-device-control")
+export class Light9DeviceControl extends LitElement {
+  static styles = [
+    css`
+      :host {
+        display: inline-block;
+      }
+      .device {
+        border: 2px solid #151e2d;
+        margin: 4px;
+        padding: 1px;
+        background: #171717; /* deviceClass gradient added later */
+        break-inside: avoid-column;
+        width: 335px;
+      }
+      .deviceAttr {
+        border-top: 1px solid #272727;
+        padding-bottom: 2px;
+        display: flex;
+      }
+      .deviceAttr > span {
+      }
+      .deviceAttr > light9-live-control {
+        flex-grow: 1;
+      }
+      h2 {
+        font-size: 110%;
+        padding: 4px;
+        margin-top: 0;
+        margin-bottom: 0;
+      }
+      .device,
+      h2 {
+        border-top-right-radius: 15px;
+      }
 
-   #mainLabel {
-       font-size: 120%; 
-       color: #9ab8fd;
-       text-decoration: initial;
-   }
-   .device.selected h2 {
-       outline: 3px solid #ffff0047;
-   }
-   .deviceAttr.selected {
-       background: #cada1829;
-   }
-  </style>
-  <div class$="device {{devClasses}}">
-    <h2 style$="[[bgStyle]]" xon-click="onClick">
-      <resource-display id="mainLabel" graph="{{graph}}" uri="{{uri}}"></resource-display>
-      a <resource-display minor graph="{{graph}}" uri="{{deviceClass}}"></resource-display>
-    </h2>
-    <template is="dom-repeat" items="{{deviceAttrs}}" as="dattr">
-      <div xon-click="onAttrClick" class$="deviceAttr {{dattr.attrClasses}}">
-        <span>attr <resource-display minor graph="{{graph}}" uri="{{dattr.uri}}"></resource-display></span>
-        <light9-live-control
-          graph="{{graph}}"
-          device="{{uri}}"
-          device-attr-row="{{dattr}}"
-          effect="{{effect}}"
-          graph-to-controls="{{graphToControls}}"
-        ></light9-live-control>
+      #mainLabel {
+        font-size: 120%;
+        color: #9ab8fd;
+        text-decoration: initial;
+      }
+      .device.selected h2 {
+        outline: 3px solid #ffff0047;
+      }
+      .deviceAttr.selected {
+        background: #cada1829;
+      }
+    `,
+  ];
+
+  render() {
+    return html`
+      <div class$="device {{devClasses}}">
+        <h2 style$="[[bgStyle]]" xon-click="onClick">
+          <resource-display id="mainLabel" graph="{{graph}}" uri="{{uri}}"></resource-display>
+          a <resource-display minor graph="{{graph}}" uri="{{deviceClass}}"></resource-display>
+        </h2>
+        <template is="dom-repeat" items="{{deviceAttrs}}" as="dattr">
+          <div xon-click="onAttrClick" class$="deviceAttr {{dattr.attrClasses}}">
+            <span>attr <resource-display minor graph="{{graph}}" uri="{{dattr.uri}}"></resource-display></span>
+            <light9-live-control
+              graph="{{graph}}"
+              device="{{uri}}"
+              device-attr-row="{{dattr}}"
+              effect="{{effect}}"
+              graph-to-controls="{{graphToControls}}"
+            ></light9-live-control>
+          </div>
+        </template>
       </div>
-    </template>
-  </div>
-</template>
-</dom-module>
+    `;
+  }
 
-const coffeeElementSetupLight9LiveDeviceControl = (function() {
-    class Light9LiveDeviceControl extends Polymer.Element {
-      static is: string;
-      static getter_properties: {
-          graph: { type: any; notify: boolean; }; uri: { type: any; notify: boolean; }; effect: { type: any; }; deviceClass: { type: any; notify: boolean; }; // the uri str
-          deviceAttrs: { type: any; notify: boolean; }; graphToControls: { ...; }; bgStyle: { ...; }; devClasses: { ...; }; // the css kind
-      };
-      static getter_observers: {};
-      selectedAttrs: any;
-      graph: any;
-      uri: any;
-      devClasses: string;
-      deviceClass: any;
-      deviceAttrs: {};
-      shadowRoot: any;
-      static initClass() {
-        this.is = "light9-live-device-control";
-        this.getter_properties = {
-          graph: { type: Object, notify: true },
-          uri: { type: String, notify: true },
-          effect: { type: String },
-          deviceClass: { type: String, notify: true }, // the uri str
-          deviceAttrs: { type: Array, notify: true },
-          graphToControls: { type: Object },
-          bgStyle: { type: String, computed: '_bgStyle(deviceClass)' },
-          devClasses: { type: String, value: '' } // the css kind
-        };
-        this.getter_observers = [
-          'onGraph(graph)'
-          ];
-      }
-      constructor() {
-        super();
-        this.selectedAttrs = new Set(); // uri strings
-      }
-      _bgStyle(deviceClass: { value: any, length: number, charCodeAt: (arg0: number) => number, }) {
-        let hash = 0;
-        deviceClass = deviceClass.value;
-        for (let start = deviceClass.length-10, i = start, end = deviceClass.length, asc = start <= end; asc ? i < end : i > end; asc ? i++ : i--) {
-          hash += deviceClass.charCodeAt(i);
-        }
-        const hue = (hash * 8) % 360;
-        const accent = `hsl(${hue}, 49%, 22%)`;
-        return `background: linear-gradient(to right, rgba(31,31,31,0) 50%, ${accent} 100%);`;
-      }
-      
-      onGraph() {
-        return this.graph.runHandler(this.update.bind(this), `${this.uri.value} update`);
-      }
-  
-      setDeviceSelected(isSel: any) {
-        return this.devClasses = isSel ? 'selected' : '';
-      }
-  
-      setAttrSelected(devAttr: { value: any, }, isSel: any) {
-        if (isSel) {
-          this.selectedAttrs.add(devAttr.value);
-        } else {
-          this.selectedAttrs.delete(devAttr.value);
+  static getter_properties: {
+      graph: { type: any; notify: boolean; }; uri: { type: any; notify: boolean; }; effect: { type: any; }; deviceClass: { type: any; notify: boolean; }; // the uri str
+      deviceAttrs: { type: any; notify: boolean; }; graphToControls: { ...; }; bgStyle: { ...; }; devClasses: { ...; }; // the css kind
+  };
+  selectedAttrs: any;
+  graph: any;
+  uri: any;
+  devClasses: string;
+  deviceClass: any;
+  deviceAttrs: {};
+  shadowRoot: any;
+  static initClass() {
+    this.getter_properties = {
+      graph: { type: Object, notify: true },
+      uri: { type: String, notify: true },
+      effect: { type: String },
+      deviceClass: { type: String, notify: true }, // the uri str
+      deviceAttrs: { type: Array, notify: true },
+      graphToControls: { type: Object },
+      bgStyle: { type: String, computed: "_bgStyle(deviceClass)" },
+      devClasses: { type: String, value: "" }, // the css kind
+    };
+    this.getter_observers = ["onGraph(graph)"];
+  }
+  constructor() {
+    super();
+    this.selectedAttrs = new Set(); // uri strings
+  }
+  _bgStyle(deviceClass: { value: any; length: number; charCodeAt: (arg0: number) => number }) {
+    let hash = 0;
+    deviceClass = deviceClass.value;
+    for (let start = deviceClass.length - 10, i = start, end = deviceClass.length, asc = start <= end; asc ? i < end : i > end; asc ? i++ : i--) {
+      hash += deviceClass.charCodeAt(i);
+    }
+    const hue = (hash * 8) % 360;
+    const accent = `hsl(${hue}, 49%, 22%)`;
+    return `background: linear-gradient(to right, rgba(31,31,31,0) 50%, ${accent} 100%);`;
+  }
+
+  onGraph() {
+    return this.graph.runHandler(this.update.bind(this), `${this.uri.value} update`);
+  }
+
+  setDeviceSelected(isSel: any) {
+    return (this.devClasses = isSel ? "selected" : "");
+  }
+
+  setAttrSelected(devAttr: { value: any }, isSel: any) {
+    if (isSel) {
+      this.selectedAttrs.add(devAttr.value);
+    } else {
+      this.selectedAttrs.delete(devAttr.value);
+    }
+    return this.update();
+  }
+
+  update(patch: null) {
+    const U = (x: string) => this.graph.Uri(x);
+    if (patch != null && !SyncedGraph.patchContainsPreds(patch, [U("rdf:type"), U(":deviceAttr"), U(":dataType"), U(":choice")])) {
+      return;
+    }
+    this.deviceClass = this.graph.uriValue(this.uri, U("rdf:type"));
+    this.deviceAttrs = [];
+    return Array.from(_.unique(this.graph.sortedUris(this.graph.objects(this.deviceClass, U(":deviceAttr"))))).map((da: any) =>
+      this.push("deviceAttrs", this.attrRow(da))
+    );
+  }
+  push(arg0: string, arg1: { uri: { value: any }; dataType: any; showColorPicker: any; attrClasses: string }) {
+    throw new Error("Method not implemented.");
+  }
+
+  attrRow(devAttr: { value: any }) {
+    let x: { value: any };
+    const U = (x: string) => this.graph.Uri(x);
+    const dataType = this.graph.uriValue(devAttr, U(":dataType"));
+    const daRow = {
+      uri: devAttr,
+      dataType,
+      showColorPicker: dataType.equals(U(":color")),
+      attrClasses: this.selectedAttrs.has(devAttr.value) ? "selected" : "",
+    };
+    if (dataType.equals(U(":color"))) {
+      daRow.useColor = true;
+    } else if (dataType.equals(U(":choice"))) {
+      daRow.useChoice = true;
+      const choiceUris = this.graph.sortedUris(this.graph.objects(devAttr, U(":choice")));
+      daRow.choices = (() => {
+        const result = [];
+        for (x of Array.from(choiceUris)) {
+          result.push({ uri: x.value, label: this.graph.labelOrTail(x) });
         }
-        return this.update();
-      }
-      
-      update(patch: null) {
-        const U = (x: string) => this.graph.Uri(x);
-        if ((patch != null) && !SyncedGraph.patchContainsPreds(
-          patch, [U('rdf:type'), U(':deviceAttr'), U(':dataType'), U(':choice')])) { return; }
-        this.deviceClass = this.graph.uriValue(this.uri, U('rdf:type'));
-        this.deviceAttrs = [];
-        return Array.from(_.unique(this.graph.sortedUris(this.graph.objects(this.deviceClass, U(':deviceAttr'))))).map((da: any) =>
-          this.push('deviceAttrs', this.attrRow(da)));
-      }
-        push(arg0: string, arg1: { uri: { value: any, }, dataType: any, showColorPicker: any, attrClasses: string, }) {
-            throw new Error("Method not implemented.");
-        }
-  
-      attrRow(devAttr: { value: any, }) {
-        let x: { value: any; };
-        const U = (x: string) => this.graph.Uri(x);
-        const dataType = this.graph.uriValue(devAttr, U(':dataType'));
-        const daRow = {
-          uri: devAttr,
-          dataType,
-          showColorPicker: dataType.equals(U(':color')),
-          attrClasses: this.selectedAttrs.has(devAttr.value) ? 'selected' : ''
-          };
-        if (dataType.equals(U(':color'))) {
-          daRow.useColor = true;
-        } else if (dataType.equals(U(':choice'))) {
-          daRow.useChoice = true;
-          const choiceUris = this.graph.sortedUris(this.graph.objects(devAttr, U(':choice')));
-          daRow.choices = ((() => {
-            const result = [];
-            for (x of Array.from(choiceUris)) {             result.push({uri: x.value, label: this.graph.labelOrTail(x)});
-            }
-            return result;
-          })());
-          daRow.choiceSize = Math.min(choiceUris.length + 1, 10);
-        } else {
-          daRow.useSlider = true;
-          daRow.max = 1;
-          if (dataType.equals(U(':angle'))) {
-            // varies
-            daRow.max = 1;
-          }
-        }
-        return daRow;
-      }
-        
-      clear() {
-        return Array.from(this.shadowRoot.querySelectorAll("light9-live-control")).map((lc: { clear: () => any; }) =>
-          lc.clear());
-      }
-  
-      onClick(ev: any) {
-        return log('click', this.uri);
-      }
-        // select, etc
-  
-      onAttrClick(ev: { model: { dattr: { uri: any, }, }, }) {
-        return log('attr click', this.uri, ev.model.dattr.uri);
+        return result;
+      })();
+      daRow.choiceSize = Math.min(choiceUris.length + 1, 10);
+    } else {
+      daRow.useSlider = true;
+      daRow.max = 1;
+      if (dataType.equals(U(":angle"))) {
+        // varies
+        daRow.max = 1;
       }
     }
-        // select
-    
-    Light9LiveDeviceControl.initClass();
-    return Light9LiveDeviceControl;
-  })();
-  
-  
\ No newline at end of file
+    return daRow;
+  }
+
+  clear() {
+    return Array.from(this.shadowRoot.querySelectorAll("light9-live-control")).map((lc: { clear: () => any }) => lc.clear());
+  }
+
+  onClick(ev: any) {
+    return log("click", this.uri);
+  }
+  // select, etc
+
+  onAttrClick(ev: { model: { dattr: { uri: any } } }) {
+    return log("attr click", this.uri, ev.model.dattr.uri);
+  }
+}
+// select