changeset 2084:c4eab47d3c83

WIP half-ported live/ page to working TS
author drewp@bigasterisk.com
date Wed, 25 May 2022 22:58:35 -0700
parents ad7ab7027907
children 0a4ae0083382
files light9/web/live/GraphToControls.ts light9/web/live/Light9DeviceControl.ts light9/web/live/Light9Listbox.ts light9/web/live/Light9LiveControl.ts light9/web/live/Light9LiveControls.ts light9/web/live/Light9LiveDeviceControl.ts light9/web/live/index.html
diffstat 7 files changed, 318 insertions(+), 306 deletions(-) [+]
line wrap: on
line diff
--- a/light9/web/live/GraphToControls.ts	Wed May 25 01:11:41 2022 -0700
+++ b/light9/web/live/GraphToControls.ts	Wed May 25 22:58:35 2022 -0700
@@ -67,12 +67,12 @@
     return this.graph.Uri(effect.value.replace("light9.bigasterisk.com/effect", "light9.bigasterisk.com/show/dance2019/effect"));
   }
 
-  setEffect(effect: NamedNode) {
+  setEffect(effect: NamedNode | null) {
     this.clearSettings();
     this.effect = effect;
-    this.ctx = this.ctxForEffect(effect);
+    this.ctx = !effect ? null : this.ctxForEffect(effect);
     // are these going to pile up? consider @graph.triggerHandler('GTC sync')
-    return this.graph.runHandler(this.syncFromGraph.bind(this), "GraphToControls sync");
+    this.graph.runHandler(this.syncFromGraph.bind(this), "GraphToControls sync");
   }
 
   newEffect() {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/light9/web/live/Light9DeviceControl.ts	Wed May 25 22:58:35 2022 -0700
@@ -0,0 +1,213 @@
+import debug from "debug";
+import { css, html, LitElement } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import { NamedNode } from "n3";
+import { unique } from "underscore";
+import { Patch, patchContainsPreds } from "../patch";
+import { getTopGraph } from "../RdfdbSyncedGraph";
+import { SyncedGraph } from "../SyncedGraph";
+import { GraphToControls } from "./GraphToControls";
+import { Choice } from "./Light9Listbox";
+import { Light9LiveControl } from "./Light9LiveControl";
+export { ResourceDisplay } from "../ResourceDisplay";
+export { Light9LiveControl };
+const log = debug("devcontrol");
+
+interface DeviceAttrRow {
+  uri: NamedNode; //devattr
+  attrClasses: string; // the css kind
+  dataType: NamedNode;
+  showColorPicker: boolean;
+  useColor: boolean;
+  useChoice: boolean;
+  choices: Choice[];
+  choiceSize: number;
+  useSlider: boolean;
+  max: number;
+}
+
+@customElement("light9-device-control")
+export class Light9DeviceControl extends LitElement {
+  graph!: SyncedGraph;
+  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;
+      }
+    `,
+  ];
+
+  render() {
+    return html`
+      <div class="device ${this.devClasses}">
+        <h2 style="${this._bgStyle(this.deviceClass)}" xon-click="onClick">
+          <resource-display id="mainLabel" .uri="${this.uri}"></resource-display>
+          a <resource-display minor .uri="${this.deviceClass}"></resource-display>
+        </h2>
+        ${this.deviceAttrs.map(
+          (dattr: DeviceAttrRow) => html`
+            <div xon-click="onAttrClick" class="deviceAttr ${dattr.attrClasses}">
+              <span>attr <resource-display minor .uri="${dattr.uri}"></resource-display></span>
+              <light9-live-control
+                .device="${this.uri}"
+                .deviceAttrRow="${dattr}"
+                .effect="${this.effect}"
+                .graphToControls="${this.graphToControls}"
+              ></light9-live-control>
+            </div>
+          `
+        )}
+      </div>
+    `;
+  }
+
+  @property() uri!: NamedNode;
+  @property() effect!: NamedNode;
+  @property() graphToControls!: GraphToControls;
+
+  @property() devClasses: string = ""; // the css kind
+  @property() deviceAttrs: DeviceAttrRow[] = [];
+  @property() deviceClass: NamedNode | null = null;
+  @property() selectedAttrs: Set<string> = new Set();
+
+  constructor() {
+    super();
+    getTopGraph().then((g) => {
+      this.graph = g;
+      this.graph.runHandler(this.configureFromGraphz.bind(this), `${this.uri.value} update`);
+    });
+    this.selectedAttrs = new Set(); // uri strings
+  }
+
+  _bgStyle(deviceClass: NamedNode | null): string {
+    if (!deviceClass) return "";
+    let hash = 0;
+    const u = deviceClass.value;
+    for (let i = u.length - 10; i < u.length; i++) {
+      hash += u.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%);`;
+  }
+
+  setDeviceSelected(isSel: any) {
+    this.devClasses = isSel ? "selected" : "";
+  }
+
+  setAttrSelected(devAttr: NamedNode, isSel: boolean) {
+    if (isSel) {
+      this.selectedAttrs.add(devAttr.value);
+    } else {
+      this.selectedAttrs.delete(devAttr.value);
+    }
+    return this.configureFromGraphz();
+  }
+
+  configureFromGraphz(patch?: Patch) {
+    const U = this.graph.U();
+    if (patch != null && !patchContainsPreds(patch, [U("rdf:type"), U(":deviceAttr"), U(":dataType"), U(":choice")])) {
+      return;
+    }
+    this.deviceClass = this.graph.uriValue(this.uri, U("rdf:type"));
+    this.deviceAttrs = [];
+    Array.from(unique(this.graph.sortedUris(this.graph.objects(this.deviceClass, U(":deviceAttr"))))).map((da: NamedNode) =>
+      this.deviceAttrs.push(this.attrRow(da))
+    );
+    this.requestUpdate();
+  }
+
+  attrRow(devAttr: NamedNode): DeviceAttrRow {
+    let x: NamedNode;
+    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" : "",
+      useColor: false,
+      useChoice: false,
+      choices: [] as Choice[],
+      choiceSize: 0,
+      useSlider: false,
+      max: 1,
+    };
+    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() {
+    Array.from(this.shadowRoot!.querySelectorAll("light9-live-control")).map((lc: Element) => (lc as Light9LiveControl).clear());
+  }
+
+  onClick(ev: any) {
+    log("click", this.uri);
+    // select, etc
+  }
+
+  onAttrClick(ev: { model: { dattr: { uri: any } } }) {
+    log("attr click", this.uri, ev.model.dattr.uri);
+    // select
+  }
+}
--- a/light9/web/live/Light9Listbox.ts	Wed May 25 01:11:41 2022 -0700
+++ b/light9/web/live/Light9Listbox.ts	Wed May 25 22:58:35 2022 -0700
@@ -1,8 +1,8 @@
 import debug from "debug";
 const log = debug("listbox");
-import { css, html, LitElement } from "lit";
+import { css, html, LitElement, PropertyValues } from "lit";
 import { customElement, property } from "lit/decorators.js";
-
+export type Choice = {uri:string,label:string}
 @customElement("light9-listbox")
 export class Light9Listbox extends LitElement {
   static styles = [
@@ -40,11 +40,12 @@
       </paper-listbox>
     `;
   }
-  properties: {
-    choices: { type: Array };
-    value: { type: String; notify: true };
-  };
-  observers: ["onValue(value)"];
+  @property() choices: Array<Choice> = [];
+  @property() value: String | null = null;
+
+  constructor() {
+    super();
+  }
   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
@@ -53,19 +54,22 @@
     }
     this.value = ev.target.uri;
   }
-  onValue(value) {
+  updated(changedProperties: PropertyValues) {
+    if (changedProperties.has("value")) {
+      if (this.value === null) {
+        this.clear();
+      }
+    }
+  }
+  onValue(value: String | null) {
     if (value === null) {
       this.clear();
     }
   }
   clear() {
-    this.async(
-      function () {
-        this.querySelectorAll("paper-item").forEach(function (item) {
-          item.blur();
-        });
-        this.value = undefined;
-      }.bind(this)
-    );
+    this.querySelectorAll("paper-item").forEach(function (item) {
+      item.blur();
+    });
+    this.value = null;
   }
 }
--- a/light9/web/live/Light9LiveControl.ts	Wed May 25 01:11:41 2022 -0700
+++ b/light9/web/live/Light9LiveControl.ts	Wed May 25 22:58:35 2022 -0700
@@ -2,9 +2,12 @@
 const log = debug("control");
 import { css, html, LitElement } from "lit";
 import { customElement, property } from "lit/decorators.js";
+import { SyncedGraph } from "../SyncedGraph";
 
 @customElement("light9-live-control")
 export class Light9LiveControl extends LitElement {
+  graph!:SyncedGraph
+
   static styles = [
     css`
       #colorControls {
@@ -61,41 +64,38 @@
     `;
   }
 
-  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;
-      deviceAttrRow: any;
-      sliderWriteValue: { value: any; };
-      choiceValue: any;
-      graphToControls: any;
-      graph: any;
-      pickedChoice: any;
-  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)"];
-  }
+  // "onChange(value)", 
+  // "onChoice(choiceValue)"];
+  // "onGraphToControls(graphToControls)", 
+  // choiceValue: { type: any; };
+  // choiceValue: { type: Object },
+  // choiceValue: any;
+  // device: { type: any; };
+  // device: { type: Object },
+  // deviceAttrRow: { type: any; }; // object returned from attrRow, below
+  // deviceAttrRow: { type: Object }, // object returned from attrRow, below
+  // deviceAttrRow: any;
+  // enableChange: boolean;
+  // graph: { type: Object, notify: true },
+  // graphToControls: { ...; };
+  // graphToControls: { type: Object },
+  // graphToControls: any;
+  // immediateSlider: { notify: boolean; observer: string; };
+  // immediateSlider: { notify: true, observer: "onSlider" },
+  // immediateSlider: any;
+  // pickedChoice: { ...; };
+  // pickedChoice: { observer: "onChange" },
+  // pickedChoice: any;
+  // sliderWriteValue: { ...; };
+  // sliderWriteValue: { type: Number },
+  // sliderWriteValue: { value: any; };
+  // value: { type: any; notify: boolean; }; // null, Uri, float, str
+  // value: { type: Object, notify: true }, // null, Uri, float, str
+  // value: any;
+  
   constructor() {
     super();
     this.enableChange = false; // until 1st graph read
--- a/light9/web/live/Light9LiveControls.ts	Wed May 25 01:11:41 2022 -0700
+++ b/light9/web/live/Light9LiveControls.ts	Wed May 25 22:58:35 2022 -0700
@@ -1,11 +1,20 @@
 import debug from "debug";
-const log = debug("controls");
 import { css, html, LitElement } from "lit";
-import { customElement, property } from "lit/decorators.js";
+import { customElement } from "lit/decorators.js";
+import { NamedNode } from "n3";
+import { sortBy, uniq } from "underscore";
+import { Patch } from "../patch";
+import { getTopGraph } from "../RdfdbSyncedGraph";
+import { SyncedGraph } from "../SyncedGraph";
 import { GraphToControls } from "./GraphToControls";
+export { Light9DeviceControl as Light9LiveDeviceControl } from "./Light9DeviceControl";
+
+const log = debug("controls");
 
 @customElement("light9-live-controls")
 export class Light9LiveControls extends LitElement {
+  graph!: SyncedGraph;
+
   static styles = [
     css`
       :host {
@@ -32,85 +41,67 @@
 
   render() {
     return html`
-      <rdfdb-synced-graph graph="{{graph}}"></rdfdb-synced-graph>
+      <rdfdb-synced-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>
+          <edit-choice .uri=${this.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>
+        ${this.devices.map(
+          (device: NamedNode) => html`
+            <light9-device-control .uri=${device} .effect=${this.effectChoice} .graphToControls=${this.graphToControls}></light9-device-control>
+          `
+        )}
       </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)"];
-  }
+  devices: Array<NamedNode> = [];
+  // uri of the effect being edited, or null. This is the
+  // master value; GraphToControls follows.
+  effectChoice: NamedNode | null = null;
+  graphToControls!: GraphToControls;
+  okToWriteUrl: boolean = false;
 
   constructor() {
     super();
-    this.graphToControls = null;
-    this.okToWriteUrl = false;
-  }
-
-  ready() {
-    super.ready(...arguments).ready();
-    return (this.currentSettings = {});
+    getTopGraph().then((g) => {
+      this.graph = g;
+      this.graph.runHandler(this.findDevices.bind(this), "findDevices");
+      this.graphToControls = new GraphToControls(this.graph);
+      // this.graph.runHandler(this.xupdate.bind(this), "Light9LiveControls update");
+      this.setEffectFromUrl.bind(this);
+    });
   }
 
-  onGraph() {
-    this.graphToControls = new GraphToControls(this.graph);
-    this.graph.runHandler(this.update.bind(this), "Light9LiveControls update");
+  findDevices(patch?: Patch) {
+    const U = this.graph.U();
 
-    // need graph to be loaded, so we don't make double settings? not sure.
-    return setTimeout(this.setFromUrl.bind(this), 1);
+    this.devices = [];
+    let classes = this.graph.subjects(U("rdf:type"), U(":DeviceClass"));
+    uniq(sortBy(classes, "value"), true).forEach((dc) => {
+      sortBy(this.graph.subjects(U("rdf:type"), dc), "value").forEach((dev) => {
+        this.devices.push(dev as NamedNode);
+      });
+    });
+    this.requestUpdate();
   }
-
-  setFromUrl() {
+  setEffectFromUrl() {
     // 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;
+      this.effectChoice = this.graph.Uri(effect);
     }
-    return (this.okToWriteUrl = true);
+    this.okToWriteUrl = true;
   }
 
   writeToUrl(effectStr: any) {
@@ -140,7 +131,7 @@
     } else {
       log("load", this.effectChoice);
       if (this.graphToControls != null) {
-        this.graphToControls.setEffect(this.graph.Uri(this.effectChoice));
+        this.graphToControls.setEffect(this.effectChoice);
       }
     }
     return this.writeToUrl(this.effectChoice);
@@ -151,7 +142,7 @@
     return this.graphToControls.emptyEffect();
   }
 
-  update() {
+  configureFromGraph() {
     const U = (x: string) => this.graph.Uri(x);
 
     const newDevs = [];
--- a/light9/web/live/Light9LiveDeviceControl.ts	Wed May 25 01:11:41 2022 -0700
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,197 +0,0 @@
-import debug from "debug";
-const log = debug("devcontrol");
-import { css, html, LitElement } from "lit";
-import { customElement, property } from "lit/decorators.js";
-
-@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;
-      }
-    `,
-  ];
-
-  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>
-    `;
-  }
-
-  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 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);
-  }
-}
-// select
--- a/light9/web/live/index.html	Wed May 25 01:11:41 2022 -0700
+++ b/light9/web/live/index.html	Wed May 25 22:58:35 2022 -0700
@@ -3,7 +3,8 @@
   <head>
     <title>device control</title>
     <meta charset="utf-8" />
-    <link rel="stylesheet" href="/style.css">
+    <link rel="stylesheet" href="../style.css">
+    <script type="module" src="../live/Light9LiveControls"></script>
   </head>
   <body>
     <style>