stb: per-element scoped CSS facet (R1 escape hatch) — model → edit → live preview → export - #5508
Merged
norman-abramovitz merged 10 commits intoJun 30, 2026
Conversation
Replace the "designed, not yet wired" projection section with the resolved direction: the stba-*->aria-* projection is deliberately deferred (futurism) until real stbx data exists, and is safe to defer because stba-*/stb-* are a private namespace that can't regress a11y. Document the composite-description shape: identity in the ARIA-correct stba-* slot (which varies per element), the theming aspect in stb-facet, composed as "facet for subject" with stb-composite-order as the override.
The prior commit framed the whole stba-*->aria-* projection as deferred futurism. That conflated two directions. The inbound dual-read is a present requirement: model-generation reads either aria-*/role or stba-*, with stba-* (a strict 1-1 ARIA mirror, held to ARIA conventions) taking precedence when both are present. Only the outbound emission of stba-* as real ARIA into the shipped DOM stays phased. Elements that already carry real ARIA need no duplicate stba-role, so the old dedup worry dissolves into the precedence rule.
The R1 facet escape hatch: an element may carry a raw element-scoped CSS declaration body (ScopedBlock) alongside its color/content/asset lever. buildModel copies it from the values.json sidecar onto the node. Additive and optional — no committed values.json sets it, so the generated branding-model.json is unchanged and the regen guard stays green.
emitScopedBlocks(nodes) renders one [stb-snapshot-id="…"] { … } rule per
element that carries a scopedBlock, ordered by snapshotId for deterministic
output and skipping blank blocks. Kept separate from emitCss because these
rules don't round-trip through parseCss (which reads only :root/.dark-theme).
buildBundle gains scopedCss, appended after the :root/.dark-theme block so the element-scoped rules win the source-order tie at load. exportInputs emits the model's scoped blocks via emitScopedBlocks. Token-first is unaffected: a color node with a routing token still projects to :root even when it also carries a scopedBlock.
Extract the CodeMirror CSS editor into mountCssEditor (shared by the token editor and the new per-element editor). The lever popover gains a Scoped CSS section seeded with node.scopedBlock; edits flow through setNodeScopedBlock into the branding model. The editor view is destroyed when the popover closes.
New STB_APPLY_BLOCKS message: the shim upserts one late <style id="stb-scoped-blocks"> at the end of <head> so the element-scoped rules win the source-order tie over the snapshot stylesheet. preview-pane emits the model's scoped blocks on ready and whenever the branding model changes. This is the live analog of the deferred Phase 3 runtime loader.
Two robustness fixes from live testing: - Terminate each declaration line with a semicolon, so a user typing one declaration per line (no trailing ';') gets valid CSS instead of one invalid run-on declaration that silently no-ops. - Repeat the attribute selector to specificity (0,3,0) so the block beats the snapshot's compound rules (.login-card h1 etc.) in light and dark, without !important — company-config inline still wins.
Add a drag handle (makeDraggable, viewport-clamped) so the editor can be moved off the auto-placed gutter position. Switch the content textarea to resize: both (was vertical-only) and make the scoped-CSS editor fill the popover width so it widens with the panel.
Vite was discovering jszip/codemirror lazily during the browser test
run, forcing a mid-run reload that broke in-flight test imports
("runner is undefined" / "Importing a module script failed"). The flake
hit different test files on different browsers per run. Pin these deps
in optimizeDeps.include so they bundle before the run starts.
norman-abramovitz
approved these changes
Jun 30, 2026
norman-abramovitz
left a comment
Contributor
There was a problem hiding this comment.
LGTM - with follow on cleanup
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the R1 facet "scoped block" to stb: an element can carry a raw, element-scoped CSS block that brands many CSS properties (including typography) without a 1‑1
stb-*→CSS attribute explosion. End to end: authored invalues.jsonor edited in the lever popover → emitted as[stb-snapshot-id]rules → applied live in the preview iframe → exported intotheme.css. Token-first is preserved — a colour with a routing token still routes to the token, not a scoped rule.Built in two logical phases. They ship as one PR because Phase B's code depends on Phase A's (it imports
emitScopedBlocks,ScopedBlock, etc.), so they can't be independent PRs — review in two passes via the commit boundary below.Phase A — scoped-block foundation (no UI)
ScopedBlocktype + optionalElementNode.scopedBlock;buildModelcarries it from thevalues.jsonsidecar (additive — regen guard stays green with no regen).emitScopedBlocks(nodes)— deterministic[stb-snapshot-id]rules, blank blocks skipped.theme.css = tokens + scoped blocks(scoped appended after:rootfor load order). Projector untouched; token-first locked by test.Phase B — edit + live preview
mountCssEditor;setNodeScopedBlockmutates the model).STB_APPLY_BLOCKSmessage → the preview shim upserts one late<style>so the block applies live (the in-tool analog of the deferred runtime loader).Robustness (from live testing)
;, so typing one declaration per line (no trailing;) stays valid instead of silently no-op'ing..login-card h1etc.) in light and dark — without!important, so company-config inline styles (the runtime-faithful path) still win.Notes
stba-*↔ARIA projection is split — inbound dual-read is now, outbound emit-to-real-ARIA stays phased.Testing
typecheck+lintclean.color/font-sizework on the title in light and dark; popover drag + both-axis resize; 0 console errors.Review boundary
Add optional scopedBlock…→Export stb scoped blocks…Edit an element's scoped CSS block…→Make the stb lever popover movable…