You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/web/routes/uix/components/carousel/+page.svelte

786 lines
36 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<script lang="ts">
import {
Carousel,
type CarouselSize,
type CarouselVariant,
type CarouselColor
} from '$uix/eidos/components/carousel';
import type { CarouselAlign, CarouselProviderSnippetProps } from '$soma/components/carousel';
import { compileMorfo } from '$uix/morfo';
import { carouselMorfo } from '@/uix/morfo/components/carousel';
import { getActiveUix } from '$active-uix';
const uix = getActiveUix();
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
let tab = $state<Tab>('live');
// ── Live state ────────────────────────────────────────────────────────
let value = $state(0);
let orientation = $state<'horizontal' | 'vertical'>('horizontal');
// Layout
let slidesPerView = $state(1);
let slidesToScroll = $state(1);
let align = $state<CarouselAlign>('start');
let gap = $state(0); // in rem; converted to `${gap}rem` when forwarded
let verticalBlockSize = $state(16); // rem
let loop = $state(false);
// Drag
let dragEnabled = $state(true);
let dragThreshold = $state(0.25);
// Autoplay
let autoplay = $state(false);
let autoplayDelay = $state(4000);
let playOnInit = $state(true);
let pauseOnHover = $state(true);
let stopOnInteraction = $state(false);
let stopOnFocusIn = $state(true);
let stopOnMouseEnter = $state(false);
let stopOnLastSnap = $state(false);
let disabled = $state(false);
// `dir` controls keyboard arrow semantics (RTL swaps next/prev) and
// flips the translation sign so horizontal advance follows reading
// direction. `locale` is forwarded for downstream lang-aware behavior
// (announcer text, future label formatting).
type Direction = 'ltr' | 'rtl';
let dir = $state<Direction>('ltr');
let locale = $state('en-US');
let size = $state<CarouselSize>('md');
let variant = $state<CarouselVariant>('surface');
let color = $state<CarouselColor>('primary');
// Imperative-API demo: store the handle exposed via the snippet so the
// footer buttons + keyboard shortcut can drive the carousel.
let api = $state<CarouselProviderSnippetProps['api'] | null>(null);
let isPlaying = $state(false);
type TraceEntry = { event: string; family: string; intent?: string; at: number };
let trace = $state<TraceEntry[]>([]);
let stageRef = $state<HTMLElement | null>(null);
$effect(() => {
const el = stageRef;
if (!el) return;
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.attributeName !== 'data-event') continue;
const target = m.target as Element;
const ev = target.getAttribute('data-event');
if (!ev) continue;
trace = [
{
event: ev,
family: target.getAttribute('data-event-family') ?? '—',
intent: target.getAttribute('data-event-intent') ?? undefined,
at: Date.now()
},
...trace
].slice(0, 6);
}
});
obs.observe(el, { attributes: true, subtree: true, attributeFilter: ['data-event'] });
return () => obs.disconnect();
});
const compiled = compileMorfo(carouselMorfo);
const partsList = $derived([...compiled.parts.byKebab.values()]);
const events = $derived([...compiled.actions.byName.values()]);
function fmtTime(at: number): string {
const d = new Date(at);
return `${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
}
const slides = [
{ tone: 'var(--color-primary-element)', label: 'Aurora · primary element', sub: 'slide 1 of 4' },
{ tone: 'var(--color-affirm-element)', label: 'Meadow · affirm element', sub: 'slide 2 of 4' },
{ tone: 'var(--color-risk-element)', label: 'Ember · risk element', sub: 'slide 3 of 4' },
{ tone: 'var(--color-fulfill-element)', label: 'Tide · fulfill element', sub: 'slide 4 of 4' }
];
const eidosSnippet = $derived(
[
"<script lang='ts'>",
" import { Carousel } from '$uix/eidos/components/carousel';",
' let value = $state(0);',
'</' + 'script>',
'',
'<Carousel',
' bind:value',
orientation !== 'horizontal' && ` orientation="${orientation}"`,
slidesPerView !== 1 && ` slidesPerView={${slidesPerView}}`,
slidesToScroll !== 1 && ` slidesToScroll={${slidesToScroll}}`,
align !== 'start' && ` align="${align}"`,
gap !== 0 && ` gap="${gap}rem"`,
orientation === 'vertical' && verticalBlockSize !== 16 && ` verticalBlockSize="${verticalBlockSize}rem"`,
size !== 'md' && ` size="${size}"`,
variant !== 'surface' && ` variant="${variant}"`,
color !== 'primary' && ` color="${color}"`,
loop && ' loop',
!dragEnabled && ' dragEnabled={false}',
dragEnabled && dragThreshold !== 0.25 && ` dragThreshold={${dragThreshold}}`,
autoplay && ' autoplay',
autoplay && autoplayDelay !== 4000 && ` autoplayDelay={${autoplayDelay}}`,
autoplay && !playOnInit && ' playOnInit={false}',
autoplay && !pauseOnHover && ' pauseOnHover={false}',
autoplay && stopOnInteraction && ' stopOnInteraction',
autoplay && !stopOnFocusIn && ' stopOnFocusIn={false}',
autoplay && stopOnMouseEnter && ' stopOnMouseEnter',
autoplay && stopOnLastSnap && ' stopOnLastSnap',
disabled && ' disabled',
dir && ` dir="${dir}"`,
'>',
' <Carousel.Viewport>',
' <Carousel.ItemGroup>',
' <Carousel.Item index={0}>Slide A</Carousel.Item>',
' <Carousel.Item index={1}>Slide B</Carousel.Item>',
' <Carousel.Item index={2}>Slide C</Carousel.Item>',
' </Carousel.ItemGroup>',
' <Carousel.PrevTrigger />',
' <Carousel.NextTrigger />',
' </Carousel.Viewport>',
' <Carousel.IndicatorGroup>',
' {#each slides as _, i}',
' <Carousel.Indicator index={i} />',
' {/each}',
' </Carousel.IndicatorGroup>',
'</Carousel>'
]
.filter(Boolean)
.join('\n')
);
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Media · Carousel</div>
<h1 data-uix-page-title>Carousel</h1>
<p data-uix-page-lede>
Horizontally or vertically scrollable rail of slides with keyboard,
button, indicator, drag and autoplay navigation. One slide per page.
Pattern follows the WAI-ARIA carousel
pattern with <code>role="region"</code> + <code>aria-roledescription="carousel"</code>;
indicators map 1:1 with slides as a <code>tablist</code>.
</p>
<div data-uix-page-meta>
<span data-uix-meta-pill>
<span data-uix-meta-key>parts</span>{compiled.parts.order.length}
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>events</span>{events.length}
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>sizes</span>5
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>apg</span>
<a href={carouselMorfo.apg} target="_blank" rel="noreferrer">carousel ↗</a>
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>index</span> {value}
</span>
</div>
</header>
<div data-uix-stage>
<div data-uix-stage-area bind:this={stageRef} style="padding: var(--uix-space-3); display: flex; justify-content: center;">
<div style="inline-size: min(40rem, 100%);">
<Carousel
bind:value
{orientation}
{slidesPerView}
{slidesToScroll}
{align}
gap="{gap}rem"
verticalBlockSize="{verticalBlockSize}rem"
{loop}
{dragEnabled}
{dragThreshold}
{autoplay}
{autoplayDelay}
{playOnInit}
{pauseOnHover}
{stopOnInteraction}
{stopOnFocusIn}
{stopOnMouseEnter}
{stopOnLastSnap}
{disabled}
{dir}
{locale}
{size}
{variant}
{color}
>
{#snippet children(snippet: CarouselProviderSnippetProps)}
{(api = snippet.api, isPlaying = snippet.isPlaying, '')}
<Carousel.Viewport>
<Carousel.ItemGroup>
{#each slides as slide, i (slide.label)}
<Carousel.Item index={i}>
<div
style="
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: {slide.tone};
color: var(--color-content-primary);
padding: var(--space-6) var(--space-4);
border-radius: var(--radius-md);
{orientation === 'vertical' ? 'block-size: 100%;' : 'min-block-size: 12rem;'}
user-select: none;
"
>
<strong style="font-size: var(--font-size-lg); margin-block-end: var(--space-2);">{slide.label}</strong>
<span style="font-size: var(--font-size-sm); color: var(--color-content-secondary);">{slide.sub}</span>
</div>
</Carousel.Item>
{/each}
</Carousel.ItemGroup>
<Carousel.PrevTrigger />
<Carousel.NextTrigger />
</Carousel.Viewport>
<Carousel.IndicatorGroup>
{#each slides as _, i (i)}
<Carousel.Indicator index={i} />
{/each}
</Carousel.IndicatorGroup>
{/snippet}
</Carousel>
</div>
</div>
<div data-uix-stage-trace>
<span data-uix-stage-trace-key>trace</span>
{#if trace.length === 0}
<span>arrow keys · drag · click indicators</span>
{:else}
{#each trace.slice(0, 3) as entry (entry.at)}
<span><span data-uix-stage-trace-event>{entry.event}</span> · {entry.family}{entry.intent ? ' · ' + entry.intent : ''}</span>
<span style="color: var(--uix-text-faint)">{fmtTime(entry.at)}</span>
{/each}
{/if}
<span style="margin-inline-start: auto;">
<span data-uix-stage-trace-key>index</span> {value}
</span>
</div>
</div>
<div data-uix-tabs role="tablist">
<button data-uix-tab data-active={tab === 'live'} onclick={() => (tab = 'live')}>Live</button>
<button data-uix-tab data-active={tab === 'api'} onclick={() => (tab = 'api')}>
API <span data-uix-tab-count>8</span>
</button>
<button data-uix-tab data-active={tab === 'morfo'} onclick={() => (tab = 'morfo')}>
<span data-uix-layer-badge="morfo">morfo</span>
<span data-uix-tab-count>{partsList.length}p · {events.length}e</span>
</button>
<button data-uix-tab data-active={tab === 'sema'} onclick={() => (tab = 'sema')}>
<span data-uix-layer-badge="sema">sema</span>
<span data-uix-tab-count>{events.length}</span>
</button>
<button data-uix-tab data-active={tab === 'recipe'} onclick={() => (tab = 'recipe')}>Recipe</button>
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
</div>
{#if tab === 'live'}
<section data-uix-section>
<h2 data-uix-section-title>Controls</h2>
<p data-uix-section-desc>
Soma owns the index, autoplay timer, drag gesture, keyboard nav
and orientation. Eidos adds chrome (variant / size / color) and
renders the prev/next triggers as eidos <code>&lt;Button iconOnly variant="ghost"&gt;</code> via
the canonical Button consumer pattern.
</p>
<div data-uix-subsection-head>
<span data-uix-layer-badge="soma">soma</span> layout
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>orientation</span>
<span data-uix-chips role="radiogroup">
{#each ['horizontal', 'vertical'] as o (o)}
<button data-uix-chip data-active={orientation === o} onclick={() => (orientation = o as 'horizontal' | 'vertical')}>{o}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>slidesPerView</span>
<span data-uix-chips role="radiogroup">
{#each [1, 2, 3, 4] as n (n)}
<button data-uix-chip data-active={slidesPerView === n} onclick={() => (slidesPerView = n)}>{n}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>slidesToScroll</span>
<span data-uix-chips role="radiogroup">
{#each [1, 2, 3] as n (n)}
<button data-uix-chip data-active={slidesToScroll === n} onclick={() => (slidesToScroll = n)}>{n}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>align</span>
<span data-uix-chips role="radiogroup">
{#each ['start', 'center', 'end'] as a (a)}
<button data-uix-chip data-active={align === a} onclick={() => (align = a as CarouselAlign)}>{a}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>gap (rem)</span>
<input type="range" min="0" max="2" step="0.25" bind:value={gap} />
<span data-uix-control-label-aux>{gap}</span>
</label>
<label data-uix-control>
<span data-uix-control-label>verticalBlockSize (rem)</span>
<input type="range" min="8" max="32" step="1" bind:value={verticalBlockSize} disabled={orientation !== 'vertical'} />
<span data-uix-control-label-aux>{verticalBlockSize}</span>
</label>
<label data-uix-control>
<span data-uix-control-label>loop</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={loop} />
<span data-uix-switch-label>{loop ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>disabled</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={disabled} />
<span data-uix-switch-label>{disabled ? 'on' : 'off'}</span>
</span>
</label>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="soma">soma</span> drag
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>dragEnabled</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={dragEnabled} />
<span data-uix-switch-label>{dragEnabled ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>dragThreshold (0–1)</span>
<input type="range" min="0.05" max="0.6" step="0.05" bind:value={dragThreshold} disabled={!dragEnabled} />
<span data-uix-control-label-aux>{dragThreshold.toFixed(2)}</span>
</label>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="soma">soma</span> autoplay
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>autoplay</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={autoplay} />
<span data-uix-switch-label>{autoplay ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>autoplayDelay (ms)</span>
<input type="range" min="1000" max="8000" step="500" bind:value={autoplayDelay} disabled={!autoplay} />
<span data-uix-control-label-aux>{autoplayDelay}</span>
</label>
<label data-uix-control>
<span data-uix-control-label>playOnInit</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={playOnInit} disabled={!autoplay} />
<span data-uix-switch-label>{playOnInit ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>pauseOnHover</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={pauseOnHover} disabled={!autoplay} />
<span data-uix-switch-label>{pauseOnHover ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>stopOnInteraction</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={stopOnInteraction} disabled={!autoplay} />
<span data-uix-switch-label>{stopOnInteraction ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>stopOnFocusIn</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={stopOnFocusIn} disabled={!autoplay} />
<span data-uix-switch-label>{stopOnFocusIn ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>stopOnMouseEnter</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={stopOnMouseEnter} disabled={!autoplay} />
<span data-uix-switch-label>{stopOnMouseEnter ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>stopOnLastSnap</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={stopOnLastSnap} disabled={!autoplay || loop} />
<span data-uix-switch-label>{stopOnLastSnap ? 'on' : 'off'}</span>
</span>
</label>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="soma">soma</span> imperative API
<span style="margin-inline-start: auto; color: var(--color-content-secondary); font-size: var(--font-size-xs);">
exposed via the <code>api</code> snippet prop — drive the carousel from external buttons
</span>
</div>
<div data-uix-controls>
<button data-uix-chip onclick={() => api?.scrollPrev()}>scrollPrev()</button>
<button data-uix-chip onclick={() => api?.scrollNext()}>scrollNext()</button>
<button data-uix-chip onclick={() => api?.scrollTo(0)}>scrollTo(0)</button>
<button data-uix-chip onclick={() => api?.scrollTo(slides.length - 1)}>scrollTo(last)</button>
<button data-uix-chip onclick={() => api?.play()} disabled={!autoplay || isPlaying}>play()</button>
<button data-uix-chip onclick={() => api?.pause()} disabled={!autoplay || !isPlaying}>pause()</button>
<button data-uix-chip onclick={() => api?.reset()} disabled={!autoplay}>reset()</button>
<span data-uix-control-label-aux>isPlaying = {isPlaying}</span>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="soma">soma</span> i18n
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>locale</span>
<select bind:value={locale}>
<option value="en-US">en-US</option>
<option value="es-ES">es-ES</option>
<option value="fr-FR">fr-FR</option>
</select>
</label>
<label data-uix-control>
<span data-uix-control-label>direction</span>
<span data-uix-chips role="radiogroup">
{#each ['ltr', 'rtl'] as value (value)}
<button data-uix-chip data-active={dir === value} onclick={() => (dir = value as Direction)}>{value}</button>
{/each}
</span>
</label>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> visual props
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>variant</span>
<span data-uix-chips role="radiogroup">
{#each ['surface', 'outline', 'ghost'] as v (v)}
<button data-uix-chip data-active={variant === v} onclick={() => (variant = v as CarouselVariant)}>{v}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>size</span>
<span data-uix-chips role="radiogroup">
{#each ['xs', 'sm', 'md', 'lg', 'xl'] as s (s)}
<button data-uix-chip data-active={size === s} onclick={() => (size = s as CarouselSize)}>{s}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>color</span>
<span data-uix-chips role="radiogroup">
{#each ['primary', 'secondary', 'neutral', 'affirm', 'fulfill', 'risk', 'threat', 'loss'] as c (c)}
<button data-uix-chip data-active={color === c} onclick={() => (color = c as CarouselColor)}>{c}</button>
{/each}
</span>
</label>
</div>
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="eidos">eidos</span>
<span>composition · with bind:value</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{eidosSnippet}</code></pre>
</div>
</section>
{/if}
{#if tab === 'api'}
<section data-uix-section>
<h2 data-uix-section-title>API reference</h2>
<p data-uix-section-desc>8 parts: Provider, Viewport, ItemGroup, Item, PrevTrigger, NextTrigger, IndicatorGroup, Indicator.</p>
<div data-uix-subsection-head>Provider (Carousel)</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td class="name">value <span data-uix-tag data-kind="bindable">bindable</span></td><td class="type">number</td><td class="default">0</td><td>Current slide index (0-based).</td></tr>
<tr><td class="name">defaultValue</td><td class="type">number</td><td class="default">0</td><td>Initial slide index when uncontrolled.</td></tr>
<tr><td class="name">onValueChange</td><td class="type">(i: number) =&gt; void</td><td class="default empty">—</td><td>Called on every index change.</td></tr>
<tr><td class="name">orientation</td><td class="type">'horizontal' | 'vertical'</td><td class="default">'horizontal'</td><td>Scroll axis.</td></tr>
<tr><td class="name">slidesPerView</td><td class="type">number</td><td class="default">1</td><td>Slides visible at once (each Item basis = 100% / slidesPerView).</td></tr>
<tr><td class="name">slidesToScroll</td><td class="type">number</td><td class="default">1</td><td>Slides advanced per next() / prev().</td></tr>
<tr><td class="name">align</td><td class="type">'start' | 'center' | 'end'</td><td class="default">'start'</td><td>Alignment of active slide(s) within viewport.</td></tr>
<tr><td class="name">gap</td><td class="type">string (CSS length)</td><td class="default">'0px'</td><td>Inline-axis gap between slides.</td></tr>
<tr><td class="name">loop</td><td class="type">boolean</td><td class="default">false</td><td>Wrap at edges (also wraps autoplay).</td></tr>
<tr><td class="name">dragEnabled</td><td class="type">boolean</td><td class="default">true</td><td>Allow pointer drag to scrub.</td></tr>
<tr><td class="name">dragThreshold</td><td class="type">number (0–1)</td><td class="default">0.25</td><td>Release fraction past which next slide commits.</td></tr>
<tr><td class="name">autoplay</td><td class="type">boolean</td><td class="default">false</td><td>Enable timer-driven advance.</td></tr>
<tr><td class="name">autoplayDelay</td><td class="type">number (ms)</td><td class="default">4000</td><td>Interval in milliseconds.</td></tr>
<tr><td class="name">playOnInit</td><td class="type">boolean</td><td class="default">true</td><td>Start the autoplay timer on mount.</td></tr>
<tr><td class="name">pauseOnHover</td><td class="type">boolean</td><td class="default">true</td><td>Pause autoplay while hovered.</td></tr>
<tr><td class="name">stopOnInteraction</td><td class="type">boolean</td><td class="default">false</td><td>Latch-stop after user clicks prev/next/indicator or drags.</td></tr>
<tr><td class="name">stopOnFocusIn</td><td class="type">boolean</td><td class="default">true</td><td>Pause while focus is inside root.</td></tr>
<tr><td class="name">stopOnMouseEnter</td><td class="type">boolean</td><td class="default">false</td><td>Pause while cursor over root (Embla parity).</td></tr>
<tr><td class="name">stopOnLastSnap</td><td class="type">boolean</td><td class="default">false</td><td>Latch-stop when last snap reached (loop=false only).</td></tr>
<tr><td class="name">disabled</td><td class="type">boolean</td><td class="default">false</td><td>Disable all navigation.</td></tr>
<tr><td class="name">aria-label</td><td class="type">string</td><td class="default empty">—</td><td>Override accessible name (falls back to lang ref).</td></tr>
<tr><td class="name">variant <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">ControlVariant: 'surface' | 'outline' | 'ghost'</td><td class="default">'surface'</td><td>Container chrome.</td></tr>
<tr><td class="name">size <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">'xs' | 'sm' | 'md' | 'lg' | 'xl'</td><td class="default">'md'</td><td>Cascades to indicator + trigger size.</td></tr>
<tr><td class="name">color <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">ColorRole</td><td class="default">'primary'</td><td>Indicator + focus accent.</td></tr>
<tr><td class="name">block <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">boolean</td><td class="default">true</td><td>Stretch root inline-size to container.</td></tr>
<tr><td class="name">verticalBlockSize <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">string (CSS length)</td><td class="default">'16rem'</td><td>Viewport block-size in vertical orientation.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Viewport · ItemGroup</div>
<p data-uix-section-desc style="margin-top: 0.25rem;">
<code>Viewport</code> is the <code>overflow: hidden</code> clipping
box. <code>ItemGroup</code> is the translatable strip whose
<code>transform</code> the provider drives. Both are pass-through —
no eidos-specific props.
</p>
<div data-uix-subsection-head>Item</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td class="name">index</td><td class="type">number</td><td class="default empty">required</td><td>0-based slide index. Used for <code>aria-roledescription</code> + indicator wiring.</td></tr>
<tr><td class="name">children</td><td class="type">Snippet&lt;[{`{ index, isActive }`}]&gt;</td><td class="default empty">—</td><td>Slide body. Receives active flag for per-slide effects.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>PrevTrigger · NextTrigger</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td class="name">variant <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">ControlVariant: 'surface' | 'outline' | 'ghost'</td><td class="default">'ghost'</td><td>Button variant.</td></tr>
<tr><td class="name">size <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">'xs' | 'sm' | 'md' | 'lg' | 'xl'</td><td class="default">cascades from Carousel</td><td>Button size override.</td></tr>
<tr><td class="name">rounded <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">ButtonRounded</td><td class="default">'full'</td><td>Corner radius.</td></tr>
<tr><td class="name">intent <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">Intent</td><td class="default empty">—</td><td>Evaluative intent.</td></tr>
<tr><td class="name">color <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">ColorRole</td><td class="default empty">—</td><td>Hierarchical color (under intent='neutral').</td></tr>
<tr><td class="name">iconOnly <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">boolean</td><td class="default">true</td><td>Square aspect, label sr-only.</td></tr>
<tr><td class="name">icon <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">Snippet</td><td class="default">chevron</td><td>Override default arrow glyph.</td></tr>
<tr><td class="name">child <span data-uix-tag data-kind="soma">soma</span></td><td class="type">Snippet</td><td class="default empty">—</td><td>Bypass Button entirely; render custom element.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>IndicatorGroup · Indicator</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td class="name">index (Indicator)</td><td class="type">number</td><td class="default empty">required</td><td>0-based slide this indicator navigates to.</td></tr>
<tr><td class="name">aria-label (Indicator)</td><td class="type">string</td><td class="default empty">—</td><td>Override accessible name; falls back to "Go to slide {`{n}`}".</td></tr>
<tr><td class="name">children (Indicator)</td><td class="type">Snippet&lt;[{`{ index, isActive }`}]&gt;</td><td class="default empty">—</td><td>Custom indicator body (default is a dot).</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'morfo'}
<section data-uix-section>
<h2 data-uix-section-title>
<span data-uix-layer-badge="morfo">morfo</span>
· declarative contract
</h2>
<p data-uix-section-desc>Source: <code>src/uix/morfo/components/carousel.ts</code>.</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Field</th><th>Value</th></tr></thead>
<tbody>
<tr><td class="name">name</td><td class="type">"{carouselMorfo.name}"</td></tr>
<tr><td class="name">kebab</td><td class="type">"{carouselMorfo.kebab}"</td></tr>
<tr><td class="name">apg</td><td><a href={carouselMorfo.apg} target="_blank" rel="noreferrer">{carouselMorfo.apg}</a></td></tr>
<tr><td class="name">parts.length</td><td class="default">{carouselMorfo.parts.length}</td></tr>
<tr><td class="name">events.length</td><td class="default">{events.length}</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Parts</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Part</th><th>Marker</th><th>Element</th><th>Role</th><th>Archetype</th><th>States</th><th>Optional</th></tr></thead>
<tbody>
{#each partsList as part (part.kebab)}
<tr>
<td class="name">{part.kebab}</td>
<td><code data-uix-part-marker>[{part.marker}]</code></td>
<td class="type">&lt;{part.defaultElement}&gt;</td>
<td class="type">{part.role ?? '—'}</td>
<td class="default">{part.archetype ?? '—'}</td>
<td class="default">{part.states.length ? part.states.join(' | ') : '—'}</td>
<td class="default">{part.optional ? 'yes' : 'no'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
<div data-uix-subsection-head>Events declaration</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>name</th><th>family</th><th>verb</th><th>sequence</th><th>intent</th><th>target</th></tr></thead>
<tbody>
{#each events as action (action.name)}
{@const sem = action.semantic}
{@const intentDecl = 'intent' in sem ? sem.intent : undefined}
{@const intentStr = typeof intentDecl === 'string' ? intentDecl : intentDecl ? `fromProp:${(intentDecl as { fromProp?: string }).fromProp ?? '?'}` : '—'}
<tr>
<td class="name">{action.name}</td>
<td class="type">{sem.family}</td>
<td>{sem.verb ?? '—'}</td>
<td>{sem.sequence ?? 'pre'}</td>
<td class="default">{intentStr}</td>
<td>{action.target}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'sema'}
<section data-uix-section id="sema">
<h2 data-uix-section-title>
<span data-uix-layer-badge="sema">sema</span>
· events
</h2>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Name</th><th>Family</th><th>Verb</th><th>Sequence</th><th>Intent</th><th>Play</th></tr></thead>
<tbody>
{#each events as action (action.name)}
{@const intentDecl = 'intent' in action.semantic ? action.semantic.intent : undefined}
{@const effectiveIntent = typeof intentDecl === 'string' ? intentDecl : undefined}
<tr>
<td class="name">{action.name}</td>
<td class="type">{action.semantic.family}</td>
<td>{action.semantic.verb ?? '—'}</td>
<td>{action.semantic.sequence ?? 'pre'}</td>
<td class="default">{effectiveIntent ?? '—'}</td>
<td>
<button
data-uix-play
onclick={() => {
const target = (stageRef?.querySelector('[data-carousel]') ?? stageRef) as HTMLElement | null;
if (!target) return;
void uix.events?.emit({
name: action.name,
family: action.semantic.family,
target,
...(effectiveIntent ? { intent: effectiveIntent } : {})
});
}}
>▶ play</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'recipe'}
<section data-uix-section>
<h2 data-uix-section-title>Eidos recipe</h2>
<p data-uix-section-desc>Selectors at <code>src/uix/eidos/components/carousel/carousel.css</code>.</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Selector</th><th>Source</th><th>What it paints</th></tr></thead>
<tbody>
<tr><td class="name">[data-carousel]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Root — flex column, gap, font.</td></tr>
<tr><td class="name">[data-carousel][data-variant]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Surface / outline / ghost chrome around the rail.</td></tr>
<tr><td class="name">[data-carousel][data-size]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Indicator dot diameter + gap.</td></tr>
<tr><td class="name">[data-carousel][data-color]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Active-indicator accent + focus ring.</td></tr>
<tr><td class="name">[data-carousel-viewport]</td><td><span data-uix-tag data-kind="soma">morfo</span></td><td>Clipping box (overflow hidden, inline soma styles).</td></tr>
<tr><td class="name">[data-carousel-item-group][data-dragging]</td><td><span data-uix-tag data-kind="soma">morfo</span></td><td>Transform transition off while dragging.</td></tr>
<tr><td class="name">[data-carousel-item]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Slide — 100% basis along axis.</td></tr>
<tr><td class="name">[data-carousel-prev-trigger] · [data-carousel-next-trigger]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Absolute-positioned over viewport edges; vertical flips to top/bottom.</td></tr>
<tr><td class="name">[data-carousel-indicator][data-state='active']</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Active indicator: accent fill + 2.25× inline-size stretch.</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'a11y'}
<section data-uix-section>
<h2 data-uix-section-title>Accessibility</h2>
<p data-uix-section-desc>
Follows the APG <a href={carouselMorfo.apg} target="_blank" rel="noreferrer">carousel pattern</a>.
The root is <code>role="region"</code> with <code>aria-roledescription="carousel"</code> +
an accessible name. Each slide is <code>role="group"</code> with
<code>aria-roledescription="slide"</code>. Indicators form a
<code>role="tablist"</code> with <code>role="tab"</code> children.
</p>
<div data-uix-subsection-head>Keyboard</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Key</th><th>Action</th></tr></thead>
<tbody>
<tr><td><span data-uix-kbd>ArrowRight</span> / <span data-uix-kbd>ArrowDown</span></td><td>Next slide.</td></tr>
<tr><td><span data-uix-kbd>ArrowLeft</span> / <span data-uix-kbd>ArrowUp</span></td><td>Previous slide.</td></tr>
<tr><td><span data-uix-kbd>Home</span></td><td>First slide.</td></tr>
<tr><td><span data-uix-kbd>End</span></td><td>Last slide.</td></tr>
<tr><td><span data-uix-kbd>Tab</span></td><td>Move focus through prev / next / indicators / inner slide controls.</td></tr>
<tr><td>pointer drag</td><td>Scrub between slides; commits when past <code>dragThreshold</code>.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>ARIA contract</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Part</th><th>Attribute</th><th>Value</th></tr></thead>
<tbody>
<tr><td class="name">provider</td><td>role</td><td class="type">"region"</td></tr>
<tr><td class="name">provider</td><td>aria-roledescription</td><td class="type">"carousel"</td></tr>
<tr><td class="name">provider</td><td>aria-label</td><td class="type">localized "Carousel"</td></tr>
<tr><td class="name">item</td><td>role</td><td class="type">"group"</td></tr>
<tr><td class="name">item</td><td>aria-roledescription</td><td class="type">"slide"</td></tr>
<tr><td class="name">prev-trigger / next-trigger</td><td>type / aria-label</td><td class="type">"button" / "Previous slide" | "Next slide"</td></tr>
<tr><td class="name">indicator-group</td><td>role / aria-orientation</td><td class="type">"tablist" / orientation</td></tr>
<tr><td class="name">indicator</td><td>role / aria-selected</td><td class="type">"tab" / "true|false"</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
</div>

Powered by TurnKey Linux.