|
|
|
|
@ -0,0 +1,477 @@
|
|
|
|
|
<script lang="ts">
|
|
|
|
|
import {
|
|
|
|
|
CardGroup,
|
|
|
|
|
type CardGroupDepth,
|
|
|
|
|
type CardGroupSize
|
|
|
|
|
} from '$uix/eidos/components/card-group';
|
|
|
|
|
import { Card } from '$uix/eidos/components/card';
|
|
|
|
|
import { UNLIMITED, isSelectable, type SelectionLimit } from '$libs/selection';
|
|
|
|
|
|
|
|
|
|
type Tab = 'live' | 'api' | 'composition' | 'a11y';
|
|
|
|
|
let tab = $state<Tab>('live');
|
|
|
|
|
|
|
|
|
|
// ── Live state ──────────────────────────────────────────────────────────
|
|
|
|
|
let toggleable = $state(true);
|
|
|
|
|
let open = $state(true);
|
|
|
|
|
let depth = $state<CardGroupDepth>('raised');
|
|
|
|
|
let size = $state<CardGroupSize>('md');
|
|
|
|
|
let motion = $state<'none' | 'scale-fade' | 'slide-fade' | 'fade'>('scale-fade');
|
|
|
|
|
let staggerEach = $state(60);
|
|
|
|
|
let layout = $state<'fluid' | 2 | 3 | 4>('fluid');
|
|
|
|
|
let minChildWidth = $state(240);
|
|
|
|
|
let whenFull = $state<'reject' | 'replace-oldest'>('reject');
|
|
|
|
|
let disabled = $state(false);
|
|
|
|
|
let value = $state<string[]>([]);
|
|
|
|
|
|
|
|
|
|
// Selection cardinality presets — the single `number | [min, max]` knob.
|
|
|
|
|
const selectionPresets: { label: string; value: SelectionLimit }[] = [
|
|
|
|
|
{ label: 'none · 0', value: 0 },
|
|
|
|
|
{ label: 'single · 1', value: 1 },
|
|
|
|
|
{ label: 'single req · [1,1]', value: [1, 1] },
|
|
|
|
|
{ label: 'up to 2 · 2', value: 2 },
|
|
|
|
|
{ label: 'range · [1,3]', value: [1, 3] },
|
|
|
|
|
{ label: 'multiple · ∞', value: UNLIMITED }
|
|
|
|
|
];
|
|
|
|
|
let selPreset = $state(3);
|
|
|
|
|
const enabledSelections = $derived(selectionPresets[selPreset].value);
|
|
|
|
|
const selectable = $derived(isSelectable(enabledSelections));
|
|
|
|
|
const capped = $derived(
|
|
|
|
|
(typeof enabledSelections === 'number' && enabledSelections > 1) ||
|
|
|
|
|
(Array.isArray(enabledSelections) && enabledSelections[1] > 1)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Demo cards — different "types" the group is agnostic to.
|
|
|
|
|
type DemoCard = {
|
|
|
|
|
id: string;
|
|
|
|
|
kind: 'metric' | 'content' | 'product';
|
|
|
|
|
title: string;
|
|
|
|
|
sub?: string;
|
|
|
|
|
body?: string;
|
|
|
|
|
metric?: string;
|
|
|
|
|
delta?: string;
|
|
|
|
|
price?: string;
|
|
|
|
|
rating?: string;
|
|
|
|
|
};
|
|
|
|
|
const cards: DemoCard[] = [
|
|
|
|
|
{ id: 'rev', kind: 'metric', title: 'Revenue', metric: '$48.2k', delta: '▲ 12.4%' },
|
|
|
|
|
{ id: 'usr', kind: 'metric', title: 'Active users', metric: '8,931', delta: '▲ 3.1%' },
|
|
|
|
|
{
|
|
|
|
|
id: 'note',
|
|
|
|
|
kind: 'content',
|
|
|
|
|
title: 'Release notes',
|
|
|
|
|
sub: '2 min read',
|
|
|
|
|
body: 'CardGroup now composes ToggleGroup (selection) and Collapsible (disclosure) — no reinvented behaviour.'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'guide',
|
|
|
|
|
kind: 'content',
|
|
|
|
|
title: 'Onboarding guide',
|
|
|
|
|
sub: 'Updated today',
|
|
|
|
|
body: 'A walkthrough of composing cards inside a toggleable, selectable group.'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'pro',
|
|
|
|
|
kind: 'product',
|
|
|
|
|
title: 'Pro plan',
|
|
|
|
|
body: 'Everything in Starter, plus advanced analytics and priority support.',
|
|
|
|
|
price: '$29 / mo',
|
|
|
|
|
rating: '★ 4.8'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'team',
|
|
|
|
|
kind: 'product',
|
|
|
|
|
title: 'Team plan',
|
|
|
|
|
body: 'Shared workspaces, roles, and audit logs for growing teams.',
|
|
|
|
|
price: '$79 / mo',
|
|
|
|
|
rating: '★ 4.9'
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// ── Trace ─────────────────────────────────────────────────────────────
|
|
|
|
|
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();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function fmtTime(at: number): string {
|
|
|
|
|
const d = new Date(at);
|
|
|
|
|
return `${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Snippet ──────────────────────────────────────────────────────────────
|
|
|
|
|
const eidosSnippet = $derived(
|
|
|
|
|
[
|
|
|
|
|
"<script lang='ts'>",
|
|
|
|
|
" import { CardGroup } from '$uix/eidos/components/card-group';",
|
|
|
|
|
" import { Card } from '$uix/eidos/components/card';",
|
|
|
|
|
selectable && ' let value = $state<string[]>([]);',
|
|
|
|
|
toggleable && ' let open = $state(true);',
|
|
|
|
|
'</' + 'script>',
|
|
|
|
|
'',
|
|
|
|
|
'<CardGroup',
|
|
|
|
|
toggleable && ' toggleable bind:open',
|
|
|
|
|
selectable &&
|
|
|
|
|
` enabledSelections={${Array.isArray(enabledSelections) ? `[${enabledSelections.join(', ')}]` : enabledSelections === UNLIMITED ? 'UNLIMITED' : enabledSelections}}`,
|
|
|
|
|
selectable && ' bind:value',
|
|
|
|
|
capped && whenFull !== 'reject' && ` whenFull="${whenFull}"`,
|
|
|
|
|
depth !== 'raised' && ` depth="${depth}"`,
|
|
|
|
|
size !== 'md' && ` size="${size}"`,
|
|
|
|
|
layout === 'fluid' ? ` minChildWidth={${minChildWidth}}` : ` columns={${layout}}`,
|
|
|
|
|
motion !== 'none' && ` motion="${motion}"`,
|
|
|
|
|
staggerEach !== 60 && ` staggerEach={${staggerEach}}`,
|
|
|
|
|
'>',
|
|
|
|
|
' <CardGroup.Title>Workspace</CardGroup.Title>',
|
|
|
|
|
' <CardGroup.Content>',
|
|
|
|
|
` <CardGroup.Item value="rev"${selectable ? '' : ''}>`,
|
|
|
|
|
' <Card.Header><Card.Title>Revenue</Card.Title></Card.Header>',
|
|
|
|
|
' <Card.Body>$48.2k</Card.Body>',
|
|
|
|
|
' </CardGroup.Item>',
|
|
|
|
|
' <!-- …more items… -->',
|
|
|
|
|
' </CardGroup.Content>',
|
|
|
|
|
'</CardGroup>'
|
|
|
|
|
]
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.join('\n')
|
|
|
|
|
);
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<div data-uix-canvas-inner>
|
|
|
|
|
<header>
|
|
|
|
|
<div data-uix-eyebrow>Layout · CardGroup</div>
|
|
|
|
|
<h1 data-uix-page-title>CardGroup</h1>
|
|
|
|
|
<p data-uix-page-lede>
|
|
|
|
|
A responsive group of cards. CardGroup is a <strong>composition shell</strong>, not a new
|
|
|
|
|
primitive: selection is delegated to <code>ToggleGroup</code> (single / multiple),
|
|
|
|
|
disclosure to <code>Collapsible</code>, and the eidos layer owns the responsive grid,
|
|
|
|
|
concentric cards, <code>data-depth</code> chrome and the coordinated cascade. Each
|
|
|
|
|
selectable item IS a card (structural identity <code>data-card</code>) AND a ToggleGroup
|
|
|
|
|
control — the same pattern as <code>RadioCards</code> ↔ <code>RadioGroup</code>.
|
|
|
|
|
</p>
|
|
|
|
|
<div data-uix-page-meta>
|
|
|
|
|
<span data-uix-meta-pill><span data-uix-meta-key>composes</span>ToggleGroup · Collapsible</span>
|
|
|
|
|
<span data-uix-meta-pill><span data-uix-meta-key>select</span>3</span>
|
|
|
|
|
<span data-uix-meta-pill><span data-uix-meta-key>sizes</span>3</span>
|
|
|
|
|
<span data-uix-meta-pill><span data-uix-meta-key>scope</span>eidos</span>
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<!-- Live preview always rendered -->
|
|
|
|
|
<div data-uix-stage>
|
|
|
|
|
<div data-uix-stage-area bind:this={stageRef}>
|
|
|
|
|
<CardGroup
|
|
|
|
|
{toggleable}
|
|
|
|
|
bind:open
|
|
|
|
|
{enabledSelections}
|
|
|
|
|
bind:value
|
|
|
|
|
{whenFull}
|
|
|
|
|
{depth}
|
|
|
|
|
{size}
|
|
|
|
|
{disabled}
|
|
|
|
|
minChildWidth={layout === 'fluid' ? minChildWidth : undefined}
|
|
|
|
|
columns={layout === 'fluid' ? undefined : layout}
|
|
|
|
|
motion={motion === 'none' ? undefined : motion}
|
|
|
|
|
{staggerEach}
|
|
|
|
|
>
|
|
|
|
|
<CardGroup.Title>Workspace</CardGroup.Title>
|
|
|
|
|
<CardGroup.Content>
|
|
|
|
|
<CardGroup.Description>Metrics, notes and plans — grouped.</CardGroup.Description>
|
|
|
|
|
{#each cards as card (card.id)}
|
|
|
|
|
<CardGroup.Item value={card.id}>
|
|
|
|
|
{#if card.kind === 'metric'}
|
|
|
|
|
<Card.Description>{card.title}</Card.Description>
|
|
|
|
|
<div class="metric-value">{card.metric}</div>
|
|
|
|
|
<div class="metric-delta">{card.delta}</div>
|
|
|
|
|
{:else if card.kind === 'product'}
|
|
|
|
|
<Card.Header>
|
|
|
|
|
<Card.Title>{card.title}</Card.Title>
|
|
|
|
|
</Card.Header>
|
|
|
|
|
<Card.Body>{card.body}</Card.Body>
|
|
|
|
|
<Card.Footer>
|
|
|
|
|
<strong>{card.price}</strong>
|
|
|
|
|
<span class="rating">{card.rating}</span>
|
|
|
|
|
</Card.Footer>
|
|
|
|
|
{:else}
|
|
|
|
|
<Card.Header>
|
|
|
|
|
<Card.Title>{card.title}</Card.Title>
|
|
|
|
|
<Card.Description>{card.sub}</Card.Description>
|
|
|
|
|
</Card.Header>
|
|
|
|
|
<Card.Body>{card.body}</Card.Body>
|
|
|
|
|
{/if}
|
|
|
|
|
</CardGroup.Item>
|
|
|
|
|
{/each}
|
|
|
|
|
</CardGroup.Content>
|
|
|
|
|
</CardGroup>
|
|
|
|
|
</div>
|
|
|
|
|
<div data-uix-stage-trace>
|
|
|
|
|
<span data-uix-stage-trace-key>trace</span>
|
|
|
|
|
{#if trace.length === 0}
|
|
|
|
|
<span>toggle the group or select a card to see events</span>
|
|
|
|
|
{:else}
|
|
|
|
|
{#each trace.slice(0, 3) as entry}
|
|
|
|
|
<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>open</span> {String(open)} ·
|
|
|
|
|
<span data-uix-stage-trace-key>selected</span> {value.length ? value.join(', ') : '—'}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Tabs -->
|
|
|
|
|
<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</button>
|
|
|
|
|
<button data-uix-tab data-active={tab === 'composition'} onclick={() => (tab = 'composition')}>Composition</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>
|
|
|
|
|
|
|
|
|
|
<div data-uix-subsection-head>Disclosure · <code>Collapsible</code></div>
|
|
|
|
|
<div data-uix-controls>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>toggleable</span>
|
|
|
|
|
<span data-uix-switch>
|
|
|
|
|
<input type="checkbox" bind:checked={toggleable} />
|
|
|
|
|
<span data-uix-switch-label>{toggleable ? 'on' : 'off'}</span>
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>open <span data-uix-control-hint>bindable</span></span>
|
|
|
|
|
<span data-uix-switch>
|
|
|
|
|
<input type="checkbox" bind:checked={open} disabled={!toggleable} />
|
|
|
|
|
<span data-uix-switch-label>{open ? 'open' : 'closed'}</span>
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div data-uix-subsection-head>Selection · <code>enabledSelections</code> <span data-uix-control-hint>delegated to ToggleGroup</span></div>
|
|
|
|
|
<div data-uix-controls>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>cardinality</span>
|
|
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
|
|
{#each selectionPresets as p, i}
|
|
|
|
|
<button data-uix-chip data-active={selPreset === i} onclick={() => { selPreset = i; value = []; }}>{p.label}</button>
|
|
|
|
|
{/each}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>whenFull <span data-uix-control-hint>at the cap → fires commit-block</span></span>
|
|
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
|
|
{#each ['reject', 'replace-oldest'] as w}
|
|
|
|
|
<button data-uix-chip data-active={whenFull === w} onclick={() => (whenFull = w as typeof whenFull)} disabled={!capped}>{w}</button>
|
|
|
|
|
{/each}
|
|
|
|
|
</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="eidos">eidos</span> visual + layout
|
|
|
|
|
</div>
|
|
|
|
|
<div data-uix-controls>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>depth</span>
|
|
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
|
|
{#each ['raised', 'recessed', 'flush'] as d}
|
|
|
|
|
<button data-uix-chip data-active={depth === d} onclick={() => (depth = d as CardGroupDepth)}>{d}</button>
|
|
|
|
|
{/each}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>size</span>
|
|
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
|
|
{#each ['sm', 'md', 'lg'] as s}
|
|
|
|
|
<button data-uix-chip data-active={size === s} onclick={() => (size = s as CardGroupSize)}>{s}</button>
|
|
|
|
|
{/each}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>layout</span>
|
|
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
|
|
{#each ['fluid', 2, 3, 4] as l}
|
|
|
|
|
<button data-uix-chip data-active={layout === l} onclick={() => (layout = l as typeof layout)}>{l === 'fluid' ? 'fluid' : `${l} cols`}</button>
|
|
|
|
|
{/each}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>minChildWidth <span data-uix-control-hint>fluid · {minChildWidth}px</span></span>
|
|
|
|
|
<input type="range" min="160" max="360" step="20" bind:value={minChildWidth} disabled={layout !== 'fluid'} />
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div data-uix-subsection-head>Motion · cascade</div>
|
|
|
|
|
<div data-uix-controls>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>motion <span data-uix-control-hint>cascade on toggle</span></span>
|
|
|
|
|
<span data-uix-chips role="radiogroup">
|
|
|
|
|
{#each ['none', 'scale-fade', 'slide-fade', 'fade'] as m}
|
|
|
|
|
<button data-uix-chip data-active={motion === m} onclick={() => (motion = m as typeof motion)}>{m}</button>
|
|
|
|
|
{/each}
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
<label data-uix-control>
|
|
|
|
|
<span data-uix-control-label>staggerEach <span data-uix-control-hint>{staggerEach}ms</span></span>
|
|
|
|
|
<input type="range" min="0" max="150" step="10" bind:value={staggerEach} />
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div data-uix-code style="margin-top: var(--uix-space-4);">
|
|
|
|
|
<div data-uix-code-head>
|
|
|
|
|
<span data-uix-layer-badge="eidos">eidos</span>
|
|
|
|
|
<span>composition · item = card via structural identity</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>Props on <code><CardGroup></code>. Selection forwards to <code>ToggleGroup</code>, disclosure to <code>Collapsible</code>.</p>
|
|
|
|
|
|
|
|
|
|
<div data-uix-subsection-head>CardGroup</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">toggleable</td><td class="type">boolean</td><td class="default">false</td><td>Make the group collapsible (composes Collapsible; Title becomes the trigger).</td></tr>
|
|
|
|
|
<tr><td class="name">open <span data-uix-tag data-kind="bindable">bindable</span></td><td class="type">boolean</td><td class="default">true</td><td>Whether expanded. Only meaningful when toggleable.</td></tr>
|
|
|
|
|
<tr><td class="name">enabledSelections</td><td class="type">{`number | [min, max]`}</td><td class="default">0</td><td>Selection cardinality (delegated to ToggleGroup via $libs/selection). 0 none · 1 single · N up to N · UNLIMITED · [min,max] range.</td></tr>
|
|
|
|
|
<tr><td class="name">value <span data-uix-tag data-kind="bindable">bindable</span></td><td class="type">string[]</td><td class="default">[]</td><td>Selected item values (0–1 in single mode).</td></tr>
|
|
|
|
|
<tr><td class="name">whenFull</td><td class="type">'reject' | 'replace-oldest'</td><td class="default">'reject'</td><td>At a multi cap (max > 1): reject the pick (fires commit-block) or drop the oldest.</td></tr>
|
|
|
|
|
<tr><td class="name">depth</td><td class="type">'raised' | 'recessed' | 'flush'</td><td class="default">'raised'</td><td>Canonical elevation (data-depth): surface + border + shadow.</td></tr>
|
|
|
|
|
<tr><td class="name">size</td><td class="type">'sm' | 'md' | 'lg'</td><td class="default">'md'</td><td>Padding / gap / title scale — propagated to the cards.</td></tr>
|
|
|
|
|
<tr><td class="name">minChildWidth</td><td class="type">ResponsiveProp<number | string></td><td class="default empty">—</td><td>Fluid grid — repeat(auto-fill, minmax(MIN, 1fr)).</td></tr>
|
|
|
|
|
<tr><td class="name">columns</td><td class="type">ResponsiveProp<number></td><td class="default empty">—</td><td>Fixed column count. Ignored when minChildWidth is set.</td></tr>
|
|
|
|
|
<tr><td class="name">motion</td><td class="type">MotionPresetName</td><td class="default empty">—</td><td>Coordinated entrance cascade for the cards on toggle.</td></tr>
|
|
|
|
|
<tr><td class="name">staggerEach</td><td class="type">number</td><td class="default">60</td><td>Stagger rhythm (ms) for the cascade. 0 = parallel.</td></tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div data-uix-subsection-head>CardGroup.Item</div>
|
|
|
|
|
<div data-uix-table-wrap>
|
|
|
|
|
<table data-uix-table>
|
|
|
|
|
<thead><tr><th>Prop</th><th>Type</th><th>Description</th></tr></thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<tr><td class="name">value</td><td class="type">string</td><td>Selection identity. Required when the group is selectable.</td></tr>
|
|
|
|
|
<tr><td class="name">variant</td><td class="type">CardVariant</td><td>Card visual treatment (forwarded to the card recipe). @default 'soft'</td></tr>
|
|
|
|
|
<tr><td class="name">color</td><td class="type">CardColor</td><td>Card accent palette. @default 'neutral'</td></tr>
|
|
|
|
|
<tr><td class="name">rounded</td><td class="type">'sm' | 'md' | 'lg'</td><td>Card corner radius. @default 'md'</td></tr>
|
|
|
|
|
<tr><td class="name">disabled</td><td class="type">boolean</td><td>Disable selecting this item.</td></tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if tab === 'composition'}
|
|
|
|
|
<section data-uix-section>
|
|
|
|
|
<h2 data-uix-section-title>Composition</h2>
|
|
|
|
|
<p data-uix-section-desc>
|
|
|
|
|
CardGroup owns no behavioural primitive of its own. It composes existing components
|
|
|
|
|
and adds only layout + chrome.
|
|
|
|
|
</p>
|
|
|
|
|
<div data-uix-table-wrap>
|
|
|
|
|
<table data-uix-table>
|
|
|
|
|
<thead><tr><th>Concern</th><th>Owned by</th><th>What it provides</th></tr></thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<tr><td class="name">Selection</td><td><a href="/uix/components/toggle-group">ToggleGroup</a></td><td>Cardinality (single / multiple), value, onValueChange, roving focus, keyboard, aria-pressed, the <code>commit-toggle</code> sema.</td></tr>
|
|
|
|
|
<tr><td class="name">Disclosure</td><td><a href="/uix/components/collapsible">Collapsible</a></td><td>Open state, the <code>expand</code>/<code>collapse</code> sema, the content collapse, the Trigger button (Button) + aria.</td></tr>
|
|
|
|
|
<tr><td class="name">Card visual</td><td><a href="/uix/components/card">Card</a></td><td>The item IS a card (<code>data-card</code> structural identity): variant / size / color / selected ring / hover / press.</td></tr>
|
|
|
|
|
<tr><td class="name">Layout</td><td><span data-uix-layer-badge="eidos">eidos</span> CardGroup</td><td>Responsive grid, concentric cards, <code>data-depth</code> chrome, the coordinated cascade, size propagation.</td></tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
<div data-uix-subsection-head>Reference comparison</div>
|
|
|
|
|
<div data-uix-table-wrap>
|
|
|
|
|
<table data-uix-table>
|
|
|
|
|
<thead><tr><th>Library</th><th>Closest equivalent</th><th>Difference</th></tr></thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<tr><td class="name">chakra-ui</td><td><code><CardGroup></code></td><td>Layout-only. UIX adds disclosure (Collapsible) + selection (ToggleGroup).</td></tr>
|
|
|
|
|
<tr><td class="name">ant design</td><td><code><Card.Grid></code></td><td>Static grid cells. UIX is a collapsible, selectable group.</td></tr>
|
|
|
|
|
<tr><td class="name">radix / ark / bits</td><td>—</td><td>No CardGroup; selection = ToggleGroup/Radio, disclosure = Collapsible — exactly what UIX composes.</td></tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if tab === 'a11y'}
|
|
|
|
|
<section data-uix-section>
|
|
|
|
|
<h2 data-uix-section-title>Accessibility</h2>
|
|
|
|
|
<div data-uix-table-wrap>
|
|
|
|
|
<table data-uix-table>
|
|
|
|
|
<thead><tr><th>Concern</th><th>Contract</th></tr></thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<tr><td class="name">Group</td><td><code>role="group"</code> (from ToggleGroup, or the wrapper when not selectable), labelled by the Title via <code>aria-labelledby</code>.</td></tr>
|
|
|
|
|
<tr><td class="name">Selection</td><td>Each card is a <code>aria-pressed</code> toggle button (toggle-button-group pattern) with roving focus + arrow-key navigation — all from ToggleGroup.</td></tr>
|
|
|
|
|
<tr><td class="name">Disclosure</td><td>When toggleable, the Title is a Collapsible.Trigger: <code>aria-expanded</code> + <code>aria-controls</code> to the content region, Enter/Space toggles.</td></tr>
|
|
|
|
|
<tr><td class="name">Reduced motion</td><td>The cascade is a foundation motion preset — reduced-motion-aware for free.</td></tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
/* The CardGroup is a full-width, top-anchored block. The shared stage area
|
|
|
|
|
* uses `place-items: center`, which makes the group jump to the vertical
|
|
|
|
|
* center when it collapses (its height drops below the stage min-height).
|
|
|
|
|
* Pin it to the top for this demo so collapsing keeps its position. */
|
|
|
|
|
:global([data-uix-stage-area]:has(> [data-card-group])) {
|
|
|
|
|
align-items: start;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.metric-value {
|
|
|
|
|
font-size: var(--uix-font-size-xl, 1.5rem);
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
line-height: 1.1;
|
|
|
|
|
margin-block: var(--uix-space-1) var(--uix-space-1);
|
|
|
|
|
}
|
|
|
|
|
.metric-delta {
|
|
|
|
|
font-size: var(--uix-font-size-sm);
|
|
|
|
|
color: var(--uix-text-muted);
|
|
|
|
|
}
|
|
|
|
|
.rating {
|
|
|
|
|
margin-inline-start: auto;
|
|
|
|
|
color: var(--uix-text-muted);
|
|
|
|
|
font-size: var(--uix-font-size-sm);
|
|
|
|
|
}
|
|
|
|
|
</style>
|