fix(carousel): vertical overflow + close gaps vs embla/ark-ui/mantine
Real reference audit walked the published embla `.d.ts`, Mantine docs,
ark-ui, shadcn-svelte and Swiper. Found three classes of gaps in the
initial port (a) the vertical bug breaking the only non-default
orientation, (b) layout primitives every other lib ships (slidesPerView,
slidesToScroll, align, gap), (c) autoplay granularity (playOnInit,
stopOnInteraction, stopOnFocusIn, stopOnMouseEnter, stopOnLastSnap),
plus an imperative API every consumer needs.
## Vertical bug fix
Recipe now gives the root a default `block-size` in vertical orientation
(`16rem`, overridable via `--_carousel-vertical-block-size` or the new
eidos `verticalBlockSize` prop) and stretches the viewport to fill it.
Without this the viewport collapses to content height and items spill.
## Soma extensions
- `defaultValue` (uncontrolled init)
- `slidesPerView` (1+) — each Item basis = (100% − gap·(n−1)) / n
- `slidesToScroll` (1+) — paged group navigation
- `align: 'start' | 'center' | 'end'` — viewport alignment
- `gap: string` (CSS length) — between-slide spacing on the ItemGroup
- `playOnInit` — start autoplay on mount (default true)
- `stopOnInteraction` — latch-stop after user clicks prev/next/indicator or drags
- `stopOnFocusIn` — pause while focus is inside (default true)
- `stopOnMouseEnter` — pause while cursor over root (Embla parity)
- `stopOnLastSnap` — latch-stop at last snap when loop=false
- Snippet exposes `isPlaying` + an `api` handle with
`scrollNext / scrollPrev / scrollTo / play / pause / reset`
`commit()` now respects `slidesPerView` (last snap = count − spv).
`next()` / `prev()` step by `slidesToScroll`. Translate calc derives
from `slideSize = viewportSize / slidesPerView` so multi-view works.
Item flex-basis adapts to slidesPerView + gap.
## Eidos additions
- `verticalBlockSize` prop — forwarded as CSS custom property for the
vertical recipe override
- Recipe selectors for vertical orientation enforcing the block-size +
viewport stretch
- Style composition so consumers can still pass inline `style`
## Demo
Live tab now has 4 organized sections (layout / drag / autoplay /
imperative API) covering every new prop. The imperative API row drives
the carousel from external buttons via the snippet handle so consumers
see the pattern in action. `verticalBlockSize` slider exercises the
vertical fix. Snippet preview emits every set prop.
## Reference comparison
Following the canon's mandate to compare with references BEFORE
declaring done, the actual feature matrix from embla published `.d.ts`,
Mantine docs, ark-ui, shadcn-svelte, Swiper:
| Feature | embla | mantine | ark | swiper | UIX |
|------------------------|-------|---------|-----|--------|-----|
| Index nav | ✓ | ✓ | ✓ | ✓ | ✓ |
| Loop | ✓ | ✓ | ✓ | ✓ | ✓ |
| Drag | ✓ | ✓ | ✓ | ✓ | ✓ |
| Autoplay | plug | plug | ✓ | ✓ | ✓ |
| Orientation H/V | ✓ | ✓ | ✓ | ✓ | ✓ |
| slidesPerView | indir | ✓ | ✓ | ✓ | ✓ |
| slidesToScroll | ✓ | ✓ | ✓ | ✓ | ✓ |
| Align | ✓ | indir | per | ✓ | ✓ |
| Gap | css | ✓ | ✓ | ✓ | ✓ |
| defaultValue | startIndex | ✓ | ✓ | ✓ | ✓ |
| playOnInit | ✓ | indir | ✓ | ✓ | ✓ |
| stopOnInteraction | ✓ | ✓ | ✓ | ✓ | ✓ |
| stopOnFocusIn | ✓ | ✓ | ✓ | partl | ✓ |
| stopOnMouseEnter | ✓ | ✓ | ✓ | ✓ | ✓ |
| stopOnLastSnap | ✓ | ✓ | ✗ | ✓ | ✓ |
| Imperative API | ✓ | ✓ | ✓ | ✓ | ✓ |
| Indicators / triggers | DIY | ✓ | ✓ | ✓ | ✓ |
| Fade effect | plug | plug | ✗ | ✓ | ✗ deferred |
| Virtual slides | ✗ | ✗ | ✗ | ✓ | ✗ deferred |
| Multirow grid | ✗ | ✗ | ✗ | ✓ | ✗ deferred |
Deferred power-user features (fade, virtual, multirow grid, cube/cards,
parallax) are documented as power-user in the reference audit, not
table-stakes, and would require Embla-class internals to ship.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>