Skip to content

feat(ui): the kit holds everything the custom tabs repeat: the house look, banners, tabs, the variable browser and the expression editor - #197

Merged
derek-miller merged 8 commits into
mainfrom
feat/ui-kit-v2
Nov 10, 2026
Merged

derek-miller merged 8 commits into
mainfrom
feat/ui-kit-v2

Conversation

@derek-miller

@derek-miller derek-miller commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

This is UI kit v2: everything the custom Composer tabs and their drivers repeat moves into src/lib/ui/, so a driver keeps only its own views and commands. Derek, on the first round's merge ask: "the original UI extraction should have pulled all of this out. The goal is simpler business and shared templates is where the mundane complexity lives. Do it now and re-roll all the PRs."

Across the five pages adopted onto v0.9.83 to v0.9.86 (Network Requests, Variable Expressions, InfluxDB, Dynamic Buttons, zigbee3), I measured 93 CSS rules byte-identical in two or more pages, 105 selectors styled in two or more, and 26 functions of one name in two or more. The four drivers also each wrote their own answer helper, _SET_PROPERTY and device listing. The open adoption PRs (finitelabs/control4-finite-labs-essentials#89, finitelabs/control4-influxdb#97, finitelabs/control4-dynamic-buttons#48, finitelabs/control4-zigbee3#433) are held and get re-rolled onto this.

Trimmed on review. Derek asked whether the PR carried slop. I checked every export and option against the four drivers and cut about 1,100 lines:

  • The demo-mode expression evaluator goes. It was a 633-line Lua parser in JavaScript that only previewed three expressions in doc screenshots. A page now answers its demo previews from its own demo data, as Variable Expressions and Dynamic Buttons do in their re-rolls, with the evaluator's exact answers.
  • Code nothing called goes: Kit.api, encodeParams, markPropertyError and apiToken as exports; the test-only exports; the browser's onPick and token; the editor's formatError, minHeight and selector forms; and guards for states that cannot happen.
  • The three kit test files share one fake DOM (tests/ui_dom.mjs) instead of three copies, and CONTRIBUTING drops a paragraph the files' headers already say.

House look (style.css). The pages' copies are reconciled to one version each, by plain selector, since Composer's engine may be old:

  • form fields, with the same arrow on every select
  • tables
  • notes, the empty state and a save's refusal
  • chips, status dots and the segmented switch
  • secondary, ghost and outlined-danger buttons
  • the card heading row with its count
  • kit-pill.info

The properties card's field rules now come from the base. test_ui_pages.lua now fails a page that restyles any selector the kit styles (a page may still add its own tokens in :root), and checks each component's files come as a pair.

Page core (page.js).

  • A push arrives as one object: the driver sends it with lib.ui.page's send, one json parameter, so numbers and booleans survive the socket.
  • The kit owns the live-link banner (a drop, or no push within 10 s of a connect, cleared by the next push) and the demo banner.
  • Also new: an optional poll; Kit.call (a command and its answer push, with an ERROR push as a refusal); Kit.follow(itemId) for another item on the same link; tabs kept in the address (#tab/detail), with Kit.setTabs for a set built per state; Kit.ago; and Kit.listen/Kit.demoAnswer for components.
  • The properties card asks _GET_PROPERTIES on each connect and after each property change, since a Support Code changes Log Level's list.
  • In demo mode connectDriver asks what a connect asks, so the page's demo answer reaches its handlers.
  • An OSS build opened with a controller address and no token logs in through the dev proxy's saved login, so a live check needs no page shim. build_checks.py checks the DriverCentral page has no dev login.

Variable browser (variables.js and .css, lib.ui.devices).

  • lib.ui.devices replaces lib.expressions' Device Browser (removed, with its tests) and the inline copies in Network Requests and InfluxDB.
  • It lists agents too, and takes exclude, withoutVariables, valueBytes and wrap (a license gate).
  • Kit.devices is the store, which InfluxDB's own picker can share. Kit.variableBrowser is the tree, with Essentials' search and Dynamic Buttons' buttons and delegated clicks, and it inserts into any field a page names.

Expression editor (expressions.js and .css, lib.ui.expression-tab), with expressions.

  • Kit.expressionEditor provides the overlay highlighting from one names list, help sections, a live preview routed per editor, plain-language errors, a watch count and a hold that Save waits on.
  • Kit.expressionHelp makes a shared help panel, as Network Requests needs.
  • ExpressionTab.serve answers the preview, with modes, maxLength, hold, extra and wrap. Expressions.compileError comes from Dynamic Buttons' Looks.compileError.
  • In demo mode a preview goes to the page's demo answer.

Driver side. page-params never raises: a body that does not decode is {} with a warning. TabProperties:serve() answers the card's two commands.

Template.

  • copier gates the editor's three files and its test on ui and expressions together, and adds the browser's test to ui.
  • A new ui-expressions render covers that pair; it joins the CI matrix by itself. Its render (ui-expressions) and build (ui-expressions) checks need adding to the ruleset.
  • make render-ui-js runs each kit script's Node tests that the render ships.
  • CONTRIBUTING's Custom Composer Tabs section is rewritten for all of this.

Tests.

  • Node, on one shared fake DOM: core 20, variable browser 11, expression editor 13.
  • Lua: devices 71, expression-tab 61, expressions 259, expression limits 155, tab-properties 51, page-params 7, ui pages 5.
  • The builders broke the browser's and the editor's code on purpose; each break failed a test.
  • Locally, make render-checks passes for ui, ui-expressions and default, make build-checks for ui and ui-expressions, and make invalid-combinations.

Behavior changes the re-roll carries to the four drivers.

  • Network Requests, Variable Expressions and Dynamic Buttons list agents in the variable browser too.
  • The watch count counts each variable once.
  • Variable Expressions' highlighter colors comparisons, and/or/not and device_name/room_name, and no longer map or value.
  • The pages pick up the house field and table look, so their doc shots get reshot where it moved.
  • Outside Composer, text typed into Variable Expressions' or Dynamic Buttons' editor gets a note instead of a preview; the demo expressions still preview.

…look, banners, tabs, the variable browser and the expression editor

@svc-finitelabs svc-finitelabs Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for three findings inline: the poll interval leaks on each failed socket load, the expression preview's UTF-8 cutter drops a character that fits (and duplicates devices.lua's correct one), and a comment in expressions-demo.js repeats its header.

Beyond CI I ran the render leg for ui-expressions locally, reproduced the poll leak against the page test's harness and the cutter under Lua 5.1, and checked d17d1da's tab and properties-card changes: a switch is shown once (the hashchange guard compares like with like, "" for no detail, including when onSwitch calls setDetail), and back, forward and edited addresses still switch.

Comment thread template/src/lib/ui/page.js Resolved
Comment thread template/src/lib/ui/expression-tab.lua Resolved
Comment thread template/src/lib/ui/expressions-demo.js Resolved
Comment thread template/src/lib/expressions.lua Resolved
Comment thread template/src/lib/ui/variables.js Resolved
Comment thread template/src/lib/ui/page.js Resolved
…ut, compact fields, the select arrow as a token, a refused call answering at once, and a card that leaves out what a page shows elsewhere

@svc-finitelabs svc-finitelabs Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes. The three must-fixes from my review at d17d1da are still open, with no change or reply: the poll interval leaking one setInterval per failed socket load (page.js, now :329), cut in expression-tab.lua:28 dropping a character whose bytes all fit, and expressions-demo.js:16-17 repeating its file header. This head adds two more, inline.

Beyond CI, I ran a node test against this head that hands Kit.expressionEditor a selector for its textarea (it throws at insertBefore, while the same call with the element passes), and checked the exclude, chevron and not-taken paths against the existing page.js tests.

Comment thread template/src/lib/ui/expressions.js Resolved
Comment thread template/src/lib/ui/page.js Resolved
Comment thread template/src/lib/ui/expressions.js Resolved
Comment thread template/src/lib/ui/page.lua Resolved
…le last character, the editor's textarea by selector, compileError reading tokens as evaluate does, and a browser a redraw replaced stops following
svc-finitelabs[bot]
svc-finitelabs Bot previously approved these changes Nov 10, 2026

@svc-finitelabs svc-finitelabs Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All five must-fixes from my last two reviews are fixed. A socket that fails to load now starts no second poll: the new test fails against 9ab73a8's page.js and passes here. One page.cut keeps a whole last character for both callers. The editor resolves a selector before it moves the textarea. The repeated headers are gone. The nits are handled too: compileError's __p[1] turns two tokens in a row into an error, and a browser that onChange unsubscribes from inside the callback is safe because the watcher loop iterates a slice() copy. In demo mode the properties card now waits for the connect, and the connect sends _GET_PROPERTIES once.

@svc-finitelabs svc-finitelabs Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. The trim holds: each guard it drops covers a state its callers cannot reach. api() is called only by sendTo past its !apiHost branch and by subscribe, which needs a live socket. A pick builds the same PARAM{owner,var} as before, from attributes render already reduced to digits. Without expressions-demo.js, a demo preview takes the same path it took at ab95a1d when the file was left out. The merge of main brings in #198 unchanged.

Beyond CI I ran the ui-expressions build leg through packaging, and swapped owner and variable in pick's token: the browser's tests fail 2 of 11.

Comment thread template/src/lib/ui/page.js Resolved

@svc-finitelabs svc-finitelabs Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This head merges main (#199, #200) into bb55335. The PR's own changes are the same as before.

@derek-miller
derek-miller merged commit 71371ca into main Nov 10, 2026
22 checks passed
@derek-miller
derek-miller deleted the feat/ui-kit-v2 branch November 10, 2026 21:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant