WORK-520
ID:WORK-520Status:done

Migrate the background axis to a facet and exercise the layers channel

Move the ~200-line background block (step 1f) into a bg facet. This is the first facet that builds its own element tree, so it is the first real use of FacetResult.layers — declared since WORK-517 and unexercised. If layers cannot express the bg layer's construction and placement, the channel needs redesigning before the remaining migrations depend on it.

Priority:highComplexity:complexMilestone:v0.30.1Source:SPEC-124

Criteria completion

Criteria completion: 10 of 10 (100%) checked; history from Aug 27 to Aug 270%25%50%75%100%Aug 27Aug 27
Branches 2
History 7
  1. 49e1c7a
    • ☑ `bg` is a facet; step 1f and its `bgMetaProps` / `bgDataAttrs` / `bgElement` locals are gone from `engine.ts`
    • ☑ `FacetResult.layers` carries the bg layer with its placement; the engine's manual splice at step 5b is replaced by generic layer insertion
    • ☑ Preset resolution (including the single-level `extends` chain), gradients, image, video, blur, position, fit, opacity, fixed, overlay and scrim all behave unchanged
    • ☑ The bg guest from {% ref "SPEC-104" /%} is relocated into the layer and dropped from the flow, as today
    • ☑ The scrim reroute reads cover state via `ctx.axis()`, and `bg` declares `after: ['cover', 'tint']`
    • ☑ `data-color-scheme` scrim polarity yields to tint via the facet channel, not a seeded read
    • ☑ The `--has-bg` modifier lands in the same position in the class string
    • ☑ All 630 pre-existing transform tests pass **unmodified**, including `bg-gradient`, `bg-overlay-scrim` and `bg-guest`
    • ☑ Unit tests cover gradient construction, preset `extends`, overlay token vs raw CSS, and the scrim/cover split
    • ☑ `npm run build`, the full repo suite, and `refrakt contracts --check` pass
    by bjornolofandersson
  2. 18fbeeb
    Content editedby Claude
    plan: complete WORK-520
  3. 51b07d1
    statuspendingready
    by bjornolofandersson
  4. 9380524
    Content editedby Claude
    plan: complete WORK-519
  5. 70b5550
    Created (pending)by bjornolofandersson
  6. 7b91f9a
    Content editedby Claude
    plan: retarget the facet milestone to v0.30.1 and require a patch change
  7. de0030a
    Content editedby Claude
    plan: draft facet registry for the transform engine's universal axes (SP

Acceptance Criteria

  • bg is a facet; step 1f and its bgMetaProps / bgDataAttrs / bgElement locals are gone from engine.ts
  • FacetResult.layers carries the bg layer with its placement; the engine's manual splice at step 5b is replaced by generic layer insertion
  • Preset resolution (including the single-level extends chain), gradients, image, video, blur, position, fit, opacity, fixed, overlay and scrim all behave unchanged
  • The bg guest from SPEC-104 is relocated into the layer and dropped from the flow, as today
  • The scrim reroute reads cover state via ctx.axis(), and bg declares after: ['cover', 'tint']
  • data-color-scheme scrim polarity yields to tint via the facet channel, not a seeded read
  • The --has-bg modifier lands in the same position in the class string
  • All 630 pre-existing transform tests pass unmodified, including bg-gradient, bg-overlay-scrim and bg-guest
  • Unit tests cover gradient construction, preset extends, overlay token vs raw CSS, and the scrim/cover split
  • npm run build, the full repo suite, and refrakt contracts --check pass

Approach

The block's size is mostly branch surface, not depth: a trigger condition raising the layer, then a sequence of independent facets (image, video, guest, overlay, scrim) appending children. It should decompose into a resolve that returns one layer plus classes / dataAttrs / consumes.

Two theme values live inside it — BLUR_PRESETS (4px/8px/16px, declared inside the function body) and SCRIM_STRENGTH (0.3/0.55/0.8). Move them with the facet and inventory them for the token follow-on; do not hoist them into a shared constants module, which would entrench hard-coded design values in a framework-agnostic engine.

warnRawOverlay is one of the eight warn-once sets; convert it to a FacetWarning with a dedupeKey, preserving process-wide dedupe scope and the exact message.

Blocked by

  • WORK-519

References

  • SPEC-124 — facet registry (the spec this work item realizes)
  • SPEC-088 — bg gradients and the custom-CSS escape hatch
  • SPEC-104 — live sandbox guests in the bg backdrop layer
  • SPEC-089 — cover mode, which reroutes the scrim away from this layer

Resolution

Completed: 2026-08-27

Branch: claude/transform-package-refactor-7mxi8a

What was done

  • packages/transform/src/facets/bg.ts — the SPEC-088 axis: preset resolution with one extends level, buildBgGradient / resolveBgStop, image and video bases, the flat overlay wash, the legibility scrim, and the SPEC-104 sandbox-guest relocation. Owns BG_GRADIENT_DIRECTIONS, SCRIM_STRENGTH, BLUR_PRESETS and TOKEN_REF.
  • packages/transform/src/facets/types.ts + driver.ts — added FacetResult.absorbs.
  • packages/transform/src/engine.ts — step 1f, step 5b's hand-written splice, the bgMetaProps / bgDataAttrs / bgElement / bgGuestNode locals and the gradient/scrim/overlay helpers all gone. Down to 1549 lines from 2223 before the facet work began.
  • packages/transform/test/facets/bg.test.ts — 44 tests covering gradient construction, preset extends, overlay token vs raw CSS, the scrim/cover split and guest absorption.

Notes

layers held. This was the item that would show whether the channel was designed right, and it was: the engine's bespoke bg-layer splice became a generic before-content insertion any facet can use.

But relocation turned out to be two halves. layers puts the new subtree in; nothing took the original out. The bg sandbox guest moves from the host's children into the layer, and without an explicit channel it would render twice. Added FacetResult.absorbs — nodes the facet took ownership of, matched by identity, filtered from the flow by the engine.

Two more seed reads retired. bg declares after: ['cover', 'tint'], so cover's scrim reroute and tint's scheme claim arrive through ctx.axis() instead of engine internals. With both tint and bg publishing their claim as facet state, the color-scheme seed is gone entirely — only media-position and content-place remain, both going in WORK-523.

A test I got wrong, worth recording. My first draft asserted that an out-of-range gradient alpha (primary/900) falls back to the plain token. It does not: the fallback interpolates the whole stop, emitting var(--rf-color-primary/900), which is not a valid custom-property name. The code was right about what it does; the assumption was mine. Preserved verbatim and now pinned by a test documenting the quirk — fixing it would be a behaviour change and belongs in its own item.

Every declared channel on FacetResult now has a real user. All 630 pre-existing transform tests pass unmodified, including the bg-gradient, bg-overlay-scrim and bg-guest suites.