WORK-444
ID:WORK-444Status:done

Implement frame-overflow facet with sandbox as first consumer

Build the frame-overflow: clip | bleed facet per SPEC-116 / ADR-023 — a universal frame facet (policy) gated by a per-guest runtime data-overflowing signal — and land sandbox as its first consumer by reworking the held bleed="crop" PR (refrakt-md/refrakt#530).

Priority:mediumComplexity:moderateSource:SPEC-116

Criteria completion

Criteria completion: 6 of 6 (100%) checked; history from Jun 22 to Jun 230%25%50%75%100%Jun 22Jun 23
Branches 4
History 5
  1. 64cdb03
    • ☑ `frame-overflow="bleed"` on a bleed host emits `data-frame-overflow="bleed"` on its `[data-section="media"]` zone; `clip`/unset emits nothing; resolved via `resolveFrameChrome`.
    • ☑ The shared CSS bleeds `[data-frame-overflow="bleed"] > [data-overflowing]` on a narrow viewport (inline-end to screen, inline-end corners squared, inline-start anchored); no per-rune bleed CSS remains.
    • ☑ The sandbox behaviour sets `data-overflowing` from measured content width with hysteresis, independent of host policy; the sandbox `bleed` attribute is removed.
    • ☑ `frame-overflow="bleed"` on a clip host (`guestFit: 'clip'`) is inert and emits a hard build warning naming the rune and a bleed host; covered by an engine test.
    • ☑ Default (`clip`/unset) output is byte-identical to today for every rune (regression-safe); a guest that fits stays inset and rounded.
    • ☑ Engine tests for facet emission + the clip-host warning; the sandbox detection/hysteresis tests carry over; docs updated.
    by bjornolofandersson
  2. 1650ab5
    Created (ready)by bjornolofandersson
  3. 45ecaca
    Content editedby Claude
    docs(plan): bleed boundary is layout-owned, never the raw viewport
  4. eaefb7e
    Content editedby Claude
    docs(plan): record frame-anchor relationship + deferred bleed direction/
  5. b7c6829
    Content editedby Claude
    docs(plan): SPEC-116 + ADR-023 + WORK-444 — frame-overflow facet

Scope

  • Facet plumbing (engine + types). Add frame-overflow to UNIVERSAL_ATTRIBUTE_NAMES, the frame-facet meta list (FRAME_FACET_META), and FramePresetDefinition. resolveFrameChrome reads it and emits data-frame-overflow on the frame target (the host [data-section="media"] zone). Default clip emits nothing.
  • Hard build warning. When frame-overflow="bleed" resolves on a host whose config.guestFit is clip (or otherwise has no bleed-capable media zone), emit a warn-once naming the rune and pointing to a bleed host — same shape as warnFrameNoTarget.
  • Shared CSS. One guest-agnostic rule in the frame/split layer ([data-frame-overflow="bleed"] > [data-overflowing], mobile breakpoint): inline-end → the layout-owned boundary (--rf-bleed-room-end, default page gutter — never the raw viewport, SPEC-116 §5), inline-end corners squared, inline-start anchored. Remove the sandbox-specific bleed CSS. v1 ships the variable seam with the default; per-layout --rf-bleed-room-* values land with the deferred desktop/side-by-side work.
  • Sandbox first consumer (rework #530). Keep the scrollWidth measurement on the rf-sandbox-resize bridge and the nextBleedState hysteresis + tests. Set data-overflowing unconditionally when overflowing (policy now lives on the host, not the guest). Remove the bleed attribute from the sandbox rune schema (it never shipped — no deprecation needed).
  • Docs + contract. frame-overflow reference (Surfaces / frame docs), the runtime-gate model, the clip-host warning; structure-contract/coverage updates.

Acceptance Criteria

  • frame-overflow="bleed" on a bleed host emits data-frame-overflow="bleed" on its [data-section="media"] zone; clip/unset emits nothing; resolved via resolveFrameChrome.
  • The shared CSS bleeds [data-frame-overflow="bleed"] > [data-overflowing] on a narrow viewport (inline-end to screen, inline-end corners squared, inline-start anchored); no per-rune bleed CSS remains.
  • The sandbox behaviour sets data-overflowing from measured content width with hysteresis, independent of host policy; the sandbox bleed attribute is removed.
  • frame-overflow="bleed" on a clip host (guestFit: 'clip') is inert and emits a hard build warning naming the rune and a bleed host; covered by an engine test.
  • Default (clip/unset) output is byte-identical to today for every rune (regression-safe); a guest that fits stays inset and rounded.
  • Engine tests for facet emission + the clip-host warning; the sandbox detection/hysteresis tests carry over; docs updated.

Approach

  • Mirror frame-displace end-to-end: it is the closest existing facet (meta read → data-displace on the frame target → CSS), so frame-overflow follows the same read/emit path in resolveFrameChrome.
  • The warning needs config.guestFit in scope at frame-resolution time — it already is (the displace-mode default added in the guestFit work reads config.guestFit there).
  • Keep policy (host) and signal (guest) decoupled: the sandbox reports overflow regardless of whether the host bleeds, so a too-wide sandbox in a clip slot is a harmless no-op (well clips) and the warning catches the author error.

Risks / Open

  • Mobile geometry needs a real device pass. The reach-to-screen width: calc(100% + gutter) assumes the full-width host's mobile padding equals the gutter (true with contentMeasure anchoring), and rounded-iframe corner clipping is historically finicky in Safari. Verify on mobile Safari/Chrome.
  • Guest-self route deferred (ADR-023) — host-set only for now.
  • Generalisation deferred — a shared data-overflowing signal for codegroup/table/datatable is follow-on work, not in this item.
  • Bleed direction deferred — v1 is inline-end only. Direction (via frame-anchor's inline keyword: start→end, end→start, center→both) is SPEC-116 §4 follow-on. Keep v1's emitted data-frame-overflow="bleed" forward-compatible (no rename when direction lands).
  • Side-by-side bleed deferred — v1's trigger is the collapsed/stacked viewport only; bleeding the outer edge of a side-by-side media column (SPEC-116 §4) is the heavier-geometry follow-on.
  • Per-layout bleed boundary deferred — v1 ships --rf-bleed-room-* with the default (page gutter); the docs/chrome'd-layout values that cap the bleed at the wide track (SPEC-116 §5) land with the desktop work. v1's ≤640px trigger does not expose the collision (docs hides the sidebar/TOC by then).

References

  • Spec: SPEC-116. Decision: ADR-023.
  • Frame facets / resolveFrameChrome: SPEC-086.
  • Warning precedent: SPEC-090.
  • Held PR to rework: refrakt-md/refrakt#530 (packages/behaviors/src/elements/sandbox.ts nextBleedState + measurement, packages/runes/src/tags/sandbox.ts, packages/lumina/styles/runes/sandbox.css).

Resolution

Completed: 2026-06-23

Branch: claude/work-444-frame-overflow

What was done

  • Facet plumbing. frame-overflow added as a universal frame attribute (packages/runes/src/lib/index.ts schema def + FRAME_FACET_NAMES; packages/runes/src/attribute-presets.ts UNIVERSAL_ATTRIBUTE_NAMES). FramePresetDefinition.overflow (packages/transform/src/types.ts). resolveFrameChrome reads frame-overflow and emits data-frame-overflow="bleed"; FRAME_FACET_META extended (packages/transform/src/engine.ts).
  • Clip-host hard warning. At the frame-resolution site, when data-frame-overflow="bleed" lands on a host whose guestFit !== 'bleed', the marker is stripped (clean output) and warnFrameOverflowClip warns once.
  • Shared CSS. [data-frame-overflow="bleed"] > [data-overflowing] in packages/lumina/styles/dimensions/frame.css (mobile breakpoint): width: calc(100% + var(--rf-bleed-room-end, var(--rf-content-gutter))), inline-end corners squared — a layout-owned boundary, not the raw viewport.
  • Sandbox first consumer. packages/behaviors/src/elements/sandbox.ts: the resize bridge also reports scrollWidth; updateOverflow toggles data-overflowing unconditionally (policy lives on the host) via the exported nextBleedState hysteresis. No sandbox bleed attribute was added.
  • Tests. Engine facet emission + clip-host warning (frames.test.ts); nextBleedState + the always-on detection (behaviors/test/sandbox.test.ts); reference snapshot updated for the new universal attr.
  • Docs. frame-overflow row + explainer in site/content/runes/surfaces.md.

Notes

  • v1 is collapsed-viewport, inline-end only. Direction (via frame-anchor), side-by-side outer-edge bleed, the per-layout --rf-bleed-room-* values, and generalising the data-overflowing signal to codegroup/table are deferred (SPEC-116 §4–5).
  • The bleed geometry (reach-to-screen + rounded-iframe corners) is unit-tested for detection but still wants a real mobile/Safari pass.