stb: composite facet framework - #5546
Merged
norman-abramovitz merged 27 commits intoJul 3, 2026
Merged
Conversation
…ge (cloudfoundry#5517) Composite background emit + projection (plan Commit 2, Tasks 3-5): - facets.ts: facetValueCss/gradientCss/backgroundCss composite emit helpers; FACET_PROPS carries the flat longhands + background.color (the projector routes background.color per-property; the emitter emits background only via backgroundCss). backgroundCss emits a literal backstop color + reversed image layers, and skips {token} colors (projected to :root, not scoped-overridden). - css-emitter.ts: wire backgroundCss into the light + dark scoped blocks; the flat facet loop skips background.* so a literal backstop color is not emitted twice. - projector.ts: colorOf reads text.color ?? background.color; leafValueOf returns backstop color, else content, else the topmost image layer ref, else legacy asset.ref. Minimal UI plumbing pulled forward so the tree stays green (composite model removed surface.background): element-edit leverValueToFacets writes background.color; element-tree/element-columns read it. Migrates the remaining test fixtures to the composite shape. 297 tests green; typecheck + lint clean.
…loudfoundry#5517) Task 8: facets-edit background helpers (setBackstop/addLayer/setLayer/ removeLayer/reorderLayer). element-edit.ts's color->background.color route was already landed in a prior commit; verified, not duplicated. Task 9 slice 4a: renderBackground in facet-tree.ts — backstop color swatch row + image layer rows (file input, thumbnail label) + gradient layers as a read-only summary row (full editing is slice 4b) + remove/ move-up/move-down controls + "+ image" footer button. Callbacks wired through lever-editor.ts -> main.ts following the onAddGroup pattern. Standalone asset leaf now guarded to <img>-role elements only.
Review fixes for the gradient layer editor (C4b): - type switch now constructs the target Gradient arm explicitly (carries stops/repeating, position where supported) instead of spreading the old object, so arm-specific fields (angle, shape, size, fromAngle) can't leak into arms that don't own them - DOM tests for stop add/remove/position edit, conic position editing, and cross-arm field dropping
…ndry#5517) Task 2's migration renamed surface.background to background.color but deriveDark's role selection wasn't updated, so background swatches derived with the foreground role (contrast lift) instead of darkening.
…yer clear (cloudfoundry#5517) Final-review fix wave, four findings: 1 (critical): composite edit handlers in main.ts no longer call selectElement after mutating — rebuilding the popover mid-edit tore down the focused input (one typed character per interaction). The lever editor's model-change effect re-renders the tree instead; suppression narrowed to text-entry controls so file inputs refresh. Companion: the gradient row tracks its current gradient in a let updated on every write, so consecutive edits to two gradient fields compose instead of clobbering (mount-time closure was stale). 2: leverPatchesFor is dark-aware — in dark preview the inline background composes from facetsDark.background only; with no dark override no patch is sent (scoped blocks + built-in dark CSS own it). previewDark is subscribed in the levers effect so a toggle re-sends patches. 3: applyLevers background branch always assigns both inline props, so a patch that omits a component clears the stale value (removing the last layer drops the old background-image). No patch = no touch. 4: leafValueOf comment now states the precedence IS the CSS cascade/paint order (deliberate) and why color must not win over the image (it is also token-routed; winning here would double-emit). Tests: +10 (gradient clobber x3 browsers, spacing focus survival, file-input re-render, dark patch composition x3, stale clear + no-touch); one stop-add/remove test updated to composed semantics. 406 green.
…cloudfoundry#5517) - background is now a collapsible GroupEntry (branch header + Expand/Collapse all + Isolate), same as text/surface/spacing; no remove affordance since background isn't wired to onRemoveGroup - each background row gets a kind subtitle: "color", "image — <ref>" / "image — (no image)", "gradient — <type>" - backstop label renamed to "color" to match the CSS property it emits (background-color); model field stays background.color - usability-grade CSS for .stb-facet-bg-row/.stb-facet-bg-gradient*: bordered tinted rows, indent + left-rule for layers under the color base - lever popover growth: raised the max-height cap to calc(100vh - 2rem), bounded max-width growth to min(640px, 92vw); resize:both (manual) and the drag handle are unchanged — v1 auto-grows until the user's first manual resize, documented in the CSS comment
…, Oklch stop literals (cloudfoundry#5517)
…placement (cloudfoundry#5517) Systemic blank-value guard at emit: a blank literal (empty/whitespace string) means "unset, emit nothing" everywhere CSS is emitted from a facet: facetLiteralCss, fontFamilyCss, spacingDeclarations, backgroundCss/composeLayerImage, and the contentAssetDeclarations topmost-image backward scan (which now skips a blank-ref image layer instead of masking a real lower one). Token entries are never blank. Two comment-only closures: css-emitter.ts documents that the dark axis is color-only (no dark fontFamilyCss); migrate-facets.ts documents itself as one-shot fixture tooling with no production callers. Compare-mode dark column: the lever-editor panel's stb-preview-dark class now also tracks compareMode, not just previewDark — compare mode pins previewDark false while a live dark pane is visible, so the dark column was dimming backwards. Color picker placement: reuses the lever-editor's positionAbovePanes (moved to popover.ts, exported) so the picker no longer collapses onto the light pane in compare mode.
…loudfoundry#5517) - Instrument the shared confirm-dialog <p> as shared.confirm-dialog.message (stba-role=paragraph) with a values sidecar entry; regenerate the shared branding model. - Content facet gains format?: 'plain'|'subset' (absent = plain, byte-identical to the old shape everywhere). - New closed-grammar subset parser (src/content/subset-format.ts): **bold**/_italic_/newline only; DOM construction + escaped-text HTML serialization; no innerHTML; unterminated markers stay literal. - apply-levers + preview shim render subset via DOM construction (shim mirrors the parser in plain JS — it accepts '*' postMessage, so no innerHTML sink); plain content keeps textContent. - Editor content leaf gets a plain|formatted select wired through onContentEdit/contentValue; dialog message is the first subset user.
…5517) - Lever editor remembers its dragged position (and size, when actually resized) module-level across the selectElement teardown/rebuild; reopen reuses the rect clamped to the viewport. Explicit Close clears the memory (one-line CLEAR_MEMORY_ON_CLOSE policy, easy to flip). - Gradient stops now honor the blank-literal guard: blank stop literals are skipped (no dangling empty stop), and a gradient whose stops are ALL blank emits nothing — its layer is skipped in composition on both the scoped-CSS and live-preview patch paths.
Norm's call: re-placing the editor on every reopen would irritate. Broader editor-session memory (open groups, isolate, unsaved values) deferred until it demonstrably bothers users.
…ploads (cloudfoundry#5517) Deletes the dead background LeverPatch leg (public/preview-shim.js never handled kind:'background', so it never reached the real preview iframe) and adds blob-URL rewriting to the scoped-blocks CSS path, so user-uploaded background images actually resolve inside the iframe instead of 404ing. - src/state/branding-assets.ts: new rewriteAssetUrls(css, store, callsiteKey), with per-callsite revoke-on-replace so live panes don't clobber each other's object URLs; attachAssetBlobs collapsed to asset-only. - src/ui/preview-pane.ts: applyScopedBlocksToPreview rewrites asset refs before sending STB_APPLY_BLOCKS; leverPatchesFor drops the dead background patch + its now-unused dark param. - src/ui/export-dialog.ts: comment documenting the deliberate asymmetry (export keeps raw refs; the bundle ships real files at those paths). - src/metadata/facets.ts: deleted backgroundPatch. - src/iframe-bridge/apply-levers.ts: deleted the 'background' kind, its branch, and its fields. Tests: deleted 11 dead-leg-only tests, added 4 (3 rewriteAssetUrls TDD + 1 re-pointing the pinned-dark-pane requirement at the scoped-blocks leg). 586 baseline - 11 + 4 = 579 tests / 61 files, green. typecheck/test/lint clean.
…cloudfoundry#5517) Review fixes for the last commit's rewriteAssetUrls: - strip one pair of matching surrounding quotes (+ trim whitespace) from captured url() refs before store lookup, so user-authored scopedBlock CSS (which conventionally quotes) resolves; unknown refs pass through with original text untouched. - double-buffer minted object URLs per callsite key instead of revoking synchronously at mint time — the previous batch is still what the iframe's current stylesheet paints until STB_APPLY_BLOCKS is applied, so only the batch two calls back is revoked. - de-dupe object URL mints within one call: a ref repeated N times shares one mint. - fix stale composeLayerImage comment referencing deleted backgroundPatch.
…loudfoundry#5517) Only the outside-click path unregistered its document-level handler; the Close button and open-replaces-open paths leaked it, so the next picker was closed instantly by the previous picker's handler — every swatch click after the first close appeared dead.
…dry#5517) Close is not delete: every edit commits live, and nothing offered the way back. Adds the delete tier — clearFacetProp (one value back to snapshot-decides), resetNode (whole element back to the pristine loaded model), a Clear button in the color picker, and the 'transparent' keyword as a deliberate paint-nothing literal.
…ry#5517) An invisible edit reads as a broken editor. Live-pass findings: - backdrop hint + jump: a page's visible background may belong to a full-bleed child (login's Background overlay); background edits on the page node are painted over by it — say so and link there - covered-backstop note when image/gradient layers hide the color - Reset button (element back to snapshot) and picker Clear/dark-clear wired through the lever editor to the new state primitives - MDN help opens a docked reusable popup, not a new tab (MDN and every peer block framing; verified 2026-07-02)
…5517) Verified WCAG luminance core (0.179 break-even, sqrt(21) AA guarantee), the three failure modes of naive black/white pickers (unverified derived shades, wrong reference surface, uncomposited alpha), gradient/image/ scrim handling via the facet layer stack, OKLCH-derive-then-WCAG-verify, and Stratos platform notes (.dark-theme class, Tailwind v4 theme inline).
…ry#5517) The delete-a-layer control was an unlabeled 12px glyph — invisible as an affordance during the live pass. Layer remove is now '× remove' with explanatory titles on remove/reorder/stop buttons, and the background group shows a live read-only summary of the exact CSS it emits (paint order, real values) so what-covers-what is readable at a glance.
norman-abramovitz
approved these changes
Jul 3, 2026
norman-abramovitz
left a comment
Contributor
There was a problem hiding this comment.
Initial stb that has some of the features stratos needs
norman-abramovitz
marked this pull request as ready for review
July 3, 2026 05:21
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.
Closes #5517
WIP — still collecting look-and-feel corrections from live testing; the underlying framework is complete (601 stb tests green).
Adds the composite facet framework to the Stratos Theme Builder:
transparentkeyword, and per-node Reset to the pristine loaded model.tools/stb/docs/text-contrast-and-dark-mode.md) — break-even luminance, AA guarantees, gradient/scrim handling.