Skip to content

stb: per-element scoped CSS facet (R1 escape hatch) — model → edit → live preview → export - #5508

Merged
norman-abramovitz merged 10 commits into
cloudfoundry:developfrom
nabramovitz:norm/feat/stb-facet-scheme
Jun 30, 2026
Merged

stb: per-element scoped CSS facet (R1 escape hatch) — model → edit → live preview → export#5508
norman-abramovitz merged 10 commits into
cloudfoundry:developfrom
nabramovitz:norm/feat/stb-facet-scheme

Conversation

@nabramovitz

Copy link
Copy Markdown
Contributor

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 in values.json or edited in the lever popover → emitted as [stb-snapshot-id] rules → applied live in the preview iframe → exported into theme.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)

  • ScopedBlock type + optional ElementNode.scopedBlock; buildModel carries it from the values.json sidecar (additive — regen guard stays green with no regen).
  • emitScopedBlocks(nodes) — deterministic [stb-snapshot-id] rules, blank blocks skipped.
  • Export: theme.css = tokens + scoped blocks (scoped appended after :root for load order). Projector untouched; token-first locked by test.

Phase B — edit + live preview

  • Per-element CSS editor in the lever popover (CodeMirror extracted to a shared mountCssEditor; setNodeScopedBlock mutates the model).
  • STB_APPLY_BLOCKS message → the preview shim upserts one late <style> so the block applies live (the in-tool analog of the deferred runtime loader).
  • Popover is now movable (drag handle) and fully resizable.

Robustness (from live testing)

  • Declaration lines are auto-terminated with ;, so typing one declaration per line (no trailing ;) stays valid instead of silently no-op'ing.
  • The rule's attribute selector is repeated to specificity (0,3,0) so a scoped block beats the snapshot's compound rules (.login-card h1 etc.) in light and dark — without !important, so company-config inline styles (the runtime-faithful path) still win.

Notes

  • Architecture doc corrected: the stba-*↔ARIA projection is split — inbound dual-read is now, outbound emit-to-real-ARIA stays phased.
  • A scoped-block edit is session + export only (whole-model reload persistence is deferred, matching how content/asset edits already behave).

Testing

  • 160 tests (unit + chromium/firefox/webkit) green; typecheck + lint clean.
  • Live-verified in the running app (Playwright): edits apply live including typography; color/font-size work on the title in light and dark; popover drag + both-axis resize; 0 console errors.

Review boundary

  • Phase A: Add optional scopedBlock…Export stb scoped blocks…
  • Phase B: Edit an element's scoped CSS block…Make the stb lever popover movable…

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 norman-abramovitz 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.

LGTM - with follow on cleanup

@norman-abramovitz
norman-abramovitz merged commit 4dbe2cc into cloudfoundry:develop Jun 30, 2026
2 checks passed
@nabramovitz
nabramovitz deleted the norm/feat/stb-facet-scheme branch July 1, 2026 22:52
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.

2 participants