WORK-472
ID:WORK-472Status:done

Lift the carousel behavior out of gallery (block-agnostic)

Generalise gallery's setupCarousel into a standalone, contract-bound behavior bound on [data-layout="carousel"], leaving gallery's rendered output unchanged. Per SPEC-100 Phase A.3.

Priority:highComplexity:moderateMilestone:v0.26.0Source:SPEC-100

Criteria completion

Criteria completion: 4 of 4 (100%) checked; history from Jun 25 to Jun 250%25%50%75%100%Jun 25Jun 25
Branches 3
History 4
  1. f916348
    • ☑ The carousel behavior is block-agnostic — bound on `[data-layout="carousel"]`, not `[data-rune="gallery"]` — and uses shared `rf-carousel__*` (not `rf-gallery__nav`) chrome.
    • ☑ Nav chrome mounts relative to the track/items container (not the host root), so multi-region hosts position correctly.
    • ☑ `gallery` consumes the shared behavior, keeps its lightbox, and its rendered output/markup semantics are unchanged.
    • ☑ Behavior tests cover prev/next, keyboard nav, and cleanup for the generalised behavior.
    by bjornolofandersson
  2. 8058ef6
    Content editedby Claude
    WORK-472: lift the carousel behavior out of gallery (block-agnostic)
  3. ad0a142
    Created (ready)by bjornolofandersson
  4. 17d2678
    Content editedby Claude
    v0.26.0: accept ADR-018 + SPEC-099/100/108, break down into 15 work item

Scope

  • Extract setupCarousel (packages/behaviors/src/behaviors/gallery.ts) into a standalone block-agnostic carousel behavior dispatched via WORK-471 on [data-layout="carousel"] (not [data-rune="gallery"]).
  • It finds the track via the shared contract (WORK-470); tighten queries to :scope > where possible so nested non-slide [data-name="item"]s aren't matched.
  • Mount nav relative to the track/items container, not the host rootel.appendChild only works in gallery because host ≈ track; multi-region runes (e.g. feature) would misposition.
  • Move nav chrome classes from rf-gallery__nav to shared rf-carousel__*; the CSS for those lives in a shared carousel stylesheet (imported once), not per-rune.
  • gallery keeps its lightbox and now consumes the shared carousel behavior for its carousel layout; its rendered markup semantics are unchanged.
  • Behavior tests: prev/next, keyboard nav, and cleanup for the generalised behavior.

Acceptance Criteria

  • The carousel behavior is block-agnostic — bound on [data-layout="carousel"], not [data-rune="gallery"] — and uses shared rf-carousel__* (not rf-gallery__nav) chrome.
  • Nav chrome mounts relative to the track/items container (not the host root), so multi-region hosts position correctly.
  • gallery consumes the shared behavior, keeps its lightbox, and its rendered output/markup semantics are unchanged.
  • Behavior tests cover prev/next, keyboard nav, and cleanup for the generalised behavior.

Dependencies

  • WORK-470 — the shared DOM contract the behavior queries.
  • WORK-471 — the attribute-triggered dispatch that mounts it.

References

  • Spec: SPEC-100 Phase A.3 + Design notes (nav mount point, scoped queries).
  • packages/behaviors/src/behaviors/gallery.ts (galleryBehavior, setupCarousel, setupLightbox).

Resolution

Completed: 2026-06-25

Branch: claude/spec-100-carousel-layout-mode

What was done

  • packages/behaviors/src/behaviors/carousel.ts (new) — block-agnostic carousel behavior generalised from gallery's setupCarousel. Finds the track via [data-name="items"], scopes slides to :scope > [data-name="item"], mounts nav into the track's container (not the rune root; ensures it's a positioning context), uses rf-carousel__* classes, prev/next + arrow-key scroll, full cleanup.
  • packages/behaviors/src/behaviors/gallery.tssetupCarousel removed; galleryBehavior now owns only the lightbox.
  • packages/behaviors/src/index.ts — registers carousel in layoutModeBehaviors; exports carouselBehavior.
  • packages/skeleton/styles/runes/carousel.css + packages/lumina/styles/runes/carousel.css (new) — shared track scroll-snap mechanics + rf-carousel__nav positioning (skeleton) and nav chrome (lumina), keyed on the contract; imported in both index.css (entry parity holds).
  • packages/skeleton/styles/runes/gallery.css / packages/lumina/styles/runes/gallery.css — removed the now-shared carousel track + rf-gallery__nav rules; gallery keeps only its column-based item width.
  • packages/runes/src/tags/gallery.tslayout matches migrated onto the const (layoutMatches([LAYOUT.grid, LAYOUT.carousel], 'masonry')); same values, ADR-018 hygiene.
  • packages/behaviors/test/carousel.test.ts (new) — prev/next, keyboard, multi-region nav mount point, cleanup, and the no-track/no-items no-op.

Notes

  • Gallery's markup/output unchanged (same data-layout/data-name tokens); only the JS-injected nav class changed (rf-gallery__navrf-carousel__nav). 1348 runes/behaviors/lumina tests green incl. contracts + CSS coverage + entry parity. Hardened the position-context guard to also treat an empty computed position (jsdom) as non-positioned.