import type { Snippet } from 'svelte'; import type { HTMLAttributes } from 'svelte/elements'; import type { ScrollerSlots, ScrollerVariants } from './scroller.variants.js'; /** * @summary A row that scrolls only when it has to — otherwise an ordinary row. * @description Horizontal row of equal-rank items that becomes scrollable only * when it runs out of room — an overflow behaviour, not a navigation pattern. * On a wide viewport it is an ordinary row: no scrolling, no arrows, no dots, * and none of the accessibility duties a scroll container carries. Once it does * overflow it snaps to item boundaries, takes a tab stop (`role="group"` + * `label`, so the keyboard can scroll it at all — the bug in most media rows), * and can show jump buttons and dots. `align="center"` turns it into a centred * stage whose middle item lifts via `animation-timeline: view(inline)` — pure * CSS, and where that is unsupported the row is identical minus the lift. Use it * for feature cards, product rows, media strips and chip/filter bars. It never * auto-rotates. For one item at a time with paging semantics use Tab; for * page-number navigation use Pagination. * * @tag layout * @tag display * @related Tab * @related Toolbar * @related Pagination * @stability experimental * * @example * ```svelte * * {#each features as feature (feature.id)} * * {/each} * * ``` * * @example * ```svelte * * * {#each features as feature (feature.id)} * * {/each} * * ``` */ export interface ScrollerProps extends Omit, Omit, 'children' | 'class'> { /** The row's items. Each direct child becomes one snap target — width, snap alignment and (with `emphasis`) the lift are applied for you. Required. */ children: Snippet; /** * Accessible name for the row, e.g. "Main features". Required: once the row * overflows it becomes a focusable `role="group"`, and an unnamed group is a * nameless box to a screen reader. It is deliberately not optional-with-a- * fallback — a generic default name would be worse than none. */ label: string; /** * Where an item comes to rest when the row snaps. `start` is the ordinary * overflow row. `center` is a stage: the middle item is the subject, and the * track is padded so the first and last item can reach the middle too. * * `center` needs items narrow enough that roughly three are visible with two * peeking. Make them much wider and the centring padding takes over the row — * one card adrift in empty space, which reads as a layout bug rather than a * stage. The component warns about that in DEV. * @default 'start' * @summary Where an item comes to rest: an ordinary row, or a centre stage. */ align?: 'start' | 'center'; /** * Snap strictness. `proximity` snaps when you release nearby and otherwise * leaves scrolling alone; `mandatory` always lands on an item — right when one * item at a time is the unit, but it can skip past content sitting between two * snap points. `none` scrolls freely. * * The default follows `align`, because the two alignments mean different * things: a `start` row is a list you sweep across (`proximity`), a `center` * row is a stage whose middle has to land on something (`mandatory`). * @default 'proximity' — or 'mandatory' when align="center" * @summary How firmly scrolling lands on an item when you let go. */ snap?: 'proximity' | 'mandatory' | 'none'; /** Space between items. @default 'md' */ gap?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; /** * Width of each item, as any CSS length (`'18rem'`, `'clamp(14rem,60vw,22rem)'`). * This is what decides when the row overflows, so it is a prop rather than a * class: with `align="center"` the component needs it to compute the edge * padding. Override per-item layout via `slotClasses.viewport` if you need * responsive widths. * @default '16rem' * @summary Width of each item — what decides when the row starts to overflow. */ itemBasis?: string; /** * Previous/next buttons. `auto` shows them only while the row overflows — * on a viewport where everything fits there is nothing to navigate, and * controls for a problem that does not exist are just clutter. `always` keeps * them mounted (disabled at the ends); `none` omits them. * @default 'auto' * @summary Whether the previous/next buttons show — always, or only while the row overflows. */ controls?: 'auto' | 'always' | 'none'; /** * Position indicator. `dots` renders one button per **resting place** — * every dot jumps to its own destination and the current one carries * `aria-current`. On a centred row that is one dot per item. On a * `start`-aligned row the trailing items that share the end of the scroll * range share one dot, labelled with their range ("Items 4–5 of 5"): a row * has only as many distinct resting places as it can scroll to, and a dot * per item would light up elsewhere than the press. Leave it off for long * chip bars, where a dot per position is noise either way. * @default 'none' * @summary A dot per resting place: shows how far the row goes, and jumps there. */ indicator?: 'none' | 'dots'; /** * Lift the item in the middle of the scrollport — a scale plus an elevation * step, driven by scroll position via CSS. `subtle` is a light touch, `strong` * is visible across a room; past roughly `strong` a row wobbles while * scrolling and pulls attention away from reading, which is why this is a * scale and not a free number. Retune per instance with * `--blocks-scroller-emphasis-scale` / `-shadow`. * * **Requires `align="center"`** and is a no-op otherwise (with a DEV warning): * the lift marks the item that has arrived in the middle, and a start-aligned * row has no middle. Neighbours are never dimmed or blurred — the point is to * mark the middle, not to hide the rest. Respects `prefers-reduced-motion`, * and where `animation-timeline` is unsupported it simply does nothing. * @default 'none' * @summary How strongly the middle item is lifted. Needs the centre alignment. */ emphasis?: 'none' | 'subtle' | 'strong'; /** * Fires when the item at the snap anchor changes, with its zero-based index. * Intentionally coarse — there is no per-frame scroll-position callback, * because the CSS-native carousel primitives this component is meant to be * swapped for one day could not honour one. */ onActiveChange?: (index: number) => void; /** Accessible label for the previous button. @default 'Previous' (localised) */ previousLabel?: string; /** Accessible label for the next button. @default 'Next' (localised) */ nextLabel?: string; /** Extra classes merged onto the root container (the column holding the row and its control bar). */ class?: string; /** Remove all default tv() classes; combine with `slotClasses` to rebuild the look. Note that this also strips the layout rules that make the row scroll and snap. */ unstyled?: boolean; /** * Per-slot class overrides merged with tv() styles. Slots: root (the column — * what `class` also targets) | viewport (the scroll container; also where * per-item rules like width live, via `[&>*]:…`) | controls (the bar under the * row) | control (a previous/next button) | indicator (the dot group) | dot. */ slotClasses?: Partial>; /** * Apply a named preset registered via ``. * Prefer this over `class` overrides when the requested look falls outside the * semantic intent palette — presets keep hover/active/dark-mode logic coherent * and make the custom look reusable across the project. */ preset?: string; } export { default as Scroller } from './Scroller.svelte'; export type { ScrollerAlign } from './scroller.utils.js'; export { type ScrollerVariants, scrollerVariants } from './scroller.variants.js';