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/month-grid/+page.svelte

633 lines
26 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 {
MonthGrid,
type MonthGridColor,
type MonthGridProviderSnippetProps,
type MonthGridSize,
type MonthGridVariant,
type MonthValue
} from '$uix/eidos/components/month-grid';
import { compileMorfo } from '$uix/morfo';
import { monthGridMorfo } from '@/uix/morfo/components/month-grid';
import { getActiveUix } from '$active-uix';
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
type TraceEntry = { event: string; family: string; intent?: string; at: number };
type MorfoEventView = {
name: string;
target: string;
semantic: { family: string; verb?: string; sequence?: string; intent?: unknown };
};
const uix = getActiveUix();
type EmitSignal = Parameters<NonNullable<typeof uix.events>['emit']>[0];
let tab = $state<Tab>('live');
let trace = $state<TraceEntry[]>([]);
let stageRef = $state<HTMLElement | null>(null);
let value = $state<MonthValue | undefined>({ year: 2026, month: 5 });
let placeholder = $state<{ year: number; month?: number }>({ year: 2026 });
let disabled = $state(false);
let readonly = $state(false);
let monthFormat = $state<'long' | 'short' | 'narrow'>('short');
let locale = $state('en-US');
let dir = $state<'ltr' | 'rtl'>('ltr');
let size = $state<MonthGridSize>('md');
let variant = $state<MonthGridVariant>('surface');
let color = $state<MonthGridColor>('primary');
let useMin = $state(false);
let useMax = $state(false);
const compiled = compileMorfo(monthGridMorfo);
const partsList = $derived([...compiled.parts.byKebab.values()]);
const events = $derived([...compiled.actions.byName.values()] as MorfoEventView[]);
const minValue = $derived<MonthValue | undefined>(useMin ? { year: 2026, month: 3 } : undefined);
const maxValue = $derived<MonthValue | undefined>(useMax ? { year: 2026, month: 10 } : undefined);
const valueLabel = $derived(value ? `${value.year}-${value.month}` : 'empty');
const placeholderLabel = $derived(`${placeholder.year}`);
function fmtTime(at: number): string {
const d = new Date(at);
return `${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
}
function playEvent(action: MorfoEventView): void {
const intent = typeof action.semantic.intent === 'string' ? action.semantic.intent : undefined;
const signal: EmitSignal = {
name: action.name,
family: action.semantic.family as EmitSignal['family'],
target: stageRef ?? document.body
};
if (intent) (signal as unknown as { intent: string }).intent = intent;
uix.events?.emit(signal);
}
$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 somaSnippet = $derived(
[
"<script lang='ts'>",
" import * as MonthGrid from '$soma/components/month-grid';",
'',
` let value = $state<{ year: number; month: number } | undefined>(${value ? `{ year: ${value.year}, month: ${value.month} }` : 'undefined'});`,
` let placeholder = $state({ year: ${placeholder.year} });`,
'</' + 'script>',
'',
'<MonthGrid.Provider',
' bind:value',
' bind:placeholder',
disabled && ' disabled',
readonly && ' readonly',
minValue && ` minValue={{ year: ${minValue.year}, month: ${minValue.month} }}`,
maxValue && ` maxValue={{ year: ${maxValue.year}, month: ${maxValue.month} }}`,
monthFormat !== 'short' && ` monthFormat="${monthFormat}"`,
locale !== 'en-US' && ` locale="${locale}"`,
dir !== 'ltr' && ` dir="${dir}"`,
'>',
' {#snippet children({ months })}',
' <MonthGrid.Header>',
' <MonthGrid.PrevButton>‹</MonthGrid.PrevButton>',
' <MonthGrid.Heading />',
' <MonthGrid.NextButton>›</MonthGrid.NextButton>',
' </MonthGrid.Header>',
' <MonthGrid.Grid>',
' {#each months as month (month)}',
' <MonthGrid.Cell {month} />',
' {/each}',
' </MonthGrid.Grid>',
' {/snippet}',
'</MonthGrid.Provider>'
]
.filter(Boolean)
.join('\n')
);
const eidosSnippet = $derived(
[
"<script lang='ts'>",
" import { MonthGrid } from '$uix/eidos/components/month-grid';",
'',
` let value = $state<{ year: number; month: number } | undefined>(${value ? `{ year: ${value.year}, month: ${value.month} }` : 'undefined'});`,
` let placeholder = $state({ year: ${placeholder.year} });`,
'</' + 'script>',
'',
'<MonthGrid',
' bind:value',
' bind:placeholder',
size !== 'md' && ` size="${size}"`,
variant !== 'surface' && ` variant="${variant}"`,
color !== 'primary' && ` color="${color}"`,
disabled && ' disabled',
readonly && ' readonly',
minValue && ` minValue={{ year: ${minValue.year}, month: ${minValue.month} }}`,
maxValue && ` maxValue={{ year: ${maxValue.year}, month: ${maxValue.month} }}`,
monthFormat !== 'short' && ` monthFormat="${monthFormat}"`,
locale !== 'en-US' && ` locale="${locale}"`,
dir !== 'ltr' && ` dir="${dir}"`,
'>',
' {#snippet children({ months })}',
' <MonthGrid.Header>',
' <MonthGrid.PrevButton>‹</MonthGrid.PrevButton>',
' <MonthGrid.Heading />',
' <MonthGrid.NextButton>›</MonthGrid.NextButton>',
' </MonthGrid.Header>',
' <MonthGrid.Grid>',
' {#each months as month (month)}',
' <MonthGrid.Cell {month} />',
' {/each}',
' </MonthGrid.Grid>',
' {/snippet}',
'</MonthGrid>'
]
.filter(Boolean)
.join('\n')
);
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Form · MonthGrid</div>
<h1 data-uix-page-title>MonthGrid</h1>
<p data-uix-page-lede>
Standalone month picker — 12 cells (Jan..Dec) for a single year
with prev/next year navigation and full keyboard nav.
<span data-uix-layer-badge="soma">soma</span> owns the value
coordination, locale-aware month label formatting, min/max bounds
and the 2 doctrinal events
(<code>commit-set</code> · <code>nav-step</code>);
<span data-uix-layer-badge="eidos">eidos</span> adds size /
variant / color recipe. Parallel to the inline Calendar's month
view but works without a calendar context — used as the standalone
month picker (<code>DatePicker kind='month'</code>) and the
composed month-view inside DatePicker's popover.
</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>3
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>apg</span>
<a href={monthGridMorfo.apg} target="_blank" rel="noreferrer">grid ↗</a>
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>value</span>{valueLabel}
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>year</span>{placeholderLabel}
</span>
</div>
</header>
<div data-uix-stage>
<div data-uix-stage-area bind:this={stageRef}>
<MonthGrid
bind:value
bind:placeholder
{disabled}
{readonly}
{minValue}
{maxValue}
{monthFormat}
{locale}
{dir}
{size}
{variant}
{color}
>
{#snippet children({ months }: MonthGridProviderSnippetProps)}
<MonthGrid.Header>
<MonthGrid.PrevButton>&lt;</MonthGrid.PrevButton>
<MonthGrid.Heading />
<MonthGrid.NextButton>&gt;</MonthGrid.NextButton>
</MonthGrid.Header>
<MonthGrid.Grid>
{#each months as month (month)}
<MonthGrid.Cell {month} />
{/each}
</MonthGrid.Grid>
{/snippet}
</MonthGrid>
</div>
<div data-uix-stage-trace>
<span data-uix-stage-trace-key>trace</span>
{#if trace.length === 0}
<span>click a month or use prev/next</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>value</span>
{valueLabel}
</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</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>
<div data-uix-subsection-head>
<span data-uix-layer-badge="soma">soma</span> props · value, placeholder, bounds
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>value <span data-uix-control-hint>{valueLabel}</span></span>
<span data-uix-chips role="group">
<button data-uix-chip data-active={value?.month === 5} onclick={() => (value = { year: 2026, month: 5 })}>May 2026</button>
<button data-uix-chip data-active={value?.month === 6} onclick={() => (value = { year: 2026, month: 6 })}>Jun 2026</button>
<button data-uix-chip data-active={value === undefined} onclick={() => (value = undefined)}>empty</button>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>placeholder year</span>
<span data-uix-chips role="radiogroup">
{#each [2025, 2026, 2027] as y (y)}
<button data-uix-chip data-active={placeholder.year === y} onclick={() => (placeholder = { year: y })}>{y}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>state flags</span>
<span data-uix-switch><input type="checkbox" bind:checked={disabled} /><span data-uix-switch-label>disabled</span></span>
<span data-uix-switch><input type="checkbox" bind:checked={readonly} /><span data-uix-switch-label>readonly</span></span>
</label>
<label data-uix-control>
<span data-uix-control-label>bounds</span>
<span data-uix-switch><input type="checkbox" bind:checked={useMin} /><span data-uix-switch-label>min=Mar 2026</span></span>
<span data-uix-switch><input type="checkbox" bind:checked={useMax} /><span data-uix-switch-label>max=Oct 2026</span></span>
</label>
<label data-uix-control>
<span data-uix-control-label>monthFormat</span>
<span data-uix-chips role="radiogroup">
{#each ['long', 'short', 'narrow'] as v (v)}
<button data-uix-chip data-active={monthFormat === v} onclick={() => (monthFormat = v as typeof monthFormat)}>{v}</button>
{/each}
</span>
</label>
<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 v (v)}
<button data-uix-chip data-active={dir === v} onclick={() => (dir = v as 'ltr' | 'rtl')}>{v}</button>
{/each}
</span>
</label>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props · visual treatment
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>size</span>
<span data-uix-chips role="radiogroup">
{#each ['xs', 'sm', 'md', 'lg'] as v (v)}
<button data-uix-chip data-active={size === v} onclick={() => (size = v as MonthGridSize)}>{v}</button>
{/each}
</span>
</label>
<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 MonthGridVariant)}>{v}</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', 'risk'] as v (v)}
<button data-uix-chip data-active={color === v} onclick={() => (color = v as MonthGridColor)}>{v}</button>
{/each}
</span>
</label>
</div>
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="soma">soma</span>
<span>headless · standalone month picker</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{somaSnippet}</code></pre>
</div>
<div data-uix-code style="margin-top: var(--uix-space-3);">
<div data-uix-code-head>
<span data-uix-layer-badge="eidos">eidos</span>
<span>visual · root plus parts</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>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Owner</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>soma</td><td class="default">undefined</td><td>Selected `{`{ year, month }`}` pair.</td></tr>
<tr><td class="name">placeholder <span data-uix-tag data-kind="bindable">bindable</span></td><td>soma</td><td class="default">today's year</td><td>Anchor year for the 12-month grid.</td></tr>
<tr><td class="name">minValue / maxValue</td><td>soma</td><td class="default empty">-</td><td>Disables cells outside the bound.</td></tr>
<tr><td class="name">disabled / readonly</td><td>soma</td><td class="default">false</td><td>Root state.</td></tr>
<tr><td class="name">monthFormat</td><td>soma</td><td class="default">'short'</td><td>Intl month-name length.</td></tr>
<tr><td class="name">locale / dir</td><td>soma</td><td class="default">services</td><td>Label formatting and directional arrow keys.</td></tr>
<tr><td class="name">size <span data-uix-tag data-kind="eidos">eidos</span></td><td>eidos</td><td class="default">md</td><td>Visual scale.</td></tr>
<tr><td class="name">variant <span data-uix-tag data-kind="eidos">eidos</span></td><td>eidos</td><td class="default">surface</td><td>Surface treatment.</td></tr>
<tr><td class="name">color <span data-uix-tag data-kind="eidos">eidos</span></td><td>eidos</td><td class="default">primary</td><td>Intent palette for selected month.</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>Feature</th><th>Ark UI</th><th>React Aria</th><th>Chakra</th><th>Bits UI</th><th>UIX</th></tr></thead>
<tbody>
<tr><td class="name">Standalone month grid</td><td>part of DatePicker</td><td>via composition</td><td>no</td><td>via Calendar</td><td>first-class</td></tr>
<tr><td class="name">Bounds (min/max)</td><td>yes</td><td>yes</td><td>-</td><td>yes</td><td>yes</td></tr>
<tr><td class="name">Keyboard nav</td><td>yes</td><td>yes</td><td>-</td><td>yes</td><td>yes</td></tr>
<tr><td class="name">Locale labels</td><td>yes</td><td>yes</td><td>-</td><td>yes</td><td>yes</td></tr>
<tr><td class="name">Semantic events</td><td>callbacks</td><td>callbacks</td><td>-</td><td>callbacks</td><td>commit-set / nav-step</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>
7 parts (Provider, Header, Heading, PrevButton, NextButton,
Grid, Cell), 2 events. Source:
<code>src/uix/morfo/components/month-grid.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">"{monthGridMorfo.name}"</td></tr>
<tr><td class="name">kebab</td><td class="type">"{monthGridMorfo.kebab}"</td></tr>
<tr><td class="name">scope</td><td class="type">[{monthGridMorfo.scope.map((s) => `"${s}"`).join(', ')}]</td></tr>
<tr><td class="name">apg</td><td><a href={monthGridMorfo.apg} target="_blank" rel="noreferrer">{monthGridMorfo.apg}</a></td></tr>
<tr><td class="name">parts.length</td><td class="default">{monthGridMorfo.parts.length}</td></tr>
<tr><td class="name">events.length</td><td class="default">{monthGridMorfo.events?.length ?? 0}</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>
{#each monthGridMorfo.parts as rawPart (rawPart.kebab)}
{@const partAny = rawPart as unknown as { kebab: string; aria?: ReadonlyArray<{ attr: string; value: { kind: string }; condition?: { when: string; prop?: string; part?: string }; severity?: string }>; keyboard?: ReadonlyArray<{ key: string; action: string }> }}
{@const dataAttrs = compiled.contracts.dataAttrsByPart.get(partAny.kebab) ?? []}
{@const ariaAttrs = partAny.aria ?? []}
{@const keyboard = partAny.keyboard ?? []}
{#if dataAttrs.length || ariaAttrs.length || keyboard.length}
<div data-uix-subsection-head>{partAny.kebab}</div>
{#if dataAttrs.length}
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>data-attr</th><th>Values</th><th>Source kind</th></tr></thead>
<tbody>
{#each dataAttrs as attr (attr.attr)}
<tr>
<td class="name">{attr.attr}</td>
<td class="type">{attr.values ? attr.values.join(' | ') : '—'}</td>
<td class="default">{'value' in attr && attr.value ? (attr as { value: { kind: string } }).value.kind : '—'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
{#if ariaAttrs.length}
<div data-uix-table-wrap style="margin-top: var(--uix-space-2);">
<table data-uix-table>
<thead><tr><th>aria-attr</th><th>Source kind</th><th>Condition</th><th>Severity</th></tr></thead>
<tbody>
{#each ariaAttrs as a (a.attr)}
<tr>
<td class="name">{a.attr}</td>
<td class="type">{a.value.kind}</td>
<td class="default">{a.condition ? `when ${a.condition.when}${a.condition.prop ? ` (${a.condition.prop})` : ''}${a.condition.part ? ` (${a.condition.part})` : ''}` : 'always'}</td>
<td class="default">{a.severity ?? 'required'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
{#if keyboard.length}
<div data-uix-table-wrap style="margin-top: var(--uix-space-2);">
<table data-uix-table>
<thead><tr><th>key</th><th>action</th></tr></thead>
<tbody>
{#each keyboard as kb (kb.key)}
<tr>
<td><span data-uix-kbd>{kb.key === ' ' ? 'Space' : kb.key}</span></td>
<td class="default">{kb.action}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
{/if}
{/each}
<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 = sem.intent}
{@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>
<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>Target</th><th>Play</th></tr></thead>
<tbody>
{#each events as action (action.name)}
{@const intentDecl = action.semantic.intent}
<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">{typeof intentDecl === 'string' ? intentDecl : intentDecl ? 'fromProp' : '-'}</td>
<td class="type">{action.target}</td>
<td><button data-uix-chip onclick={() => playEvent(action)}>▶ 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>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Selector</th><th>Purpose</th></tr></thead>
<tbody>
<tr><td class="name"><code>[data-month-grid]</code></td><td>Root surface, size and variant tokens.</td></tr>
<tr><td class="name"><code>[data-month-grid-header]</code></td><td>Three-column nav header (prev · heading · next).</td></tr>
<tr><td class="name"><code>[data-month-grid-grid]</code></td><td>3×4 month grid.</td></tr>
<tr><td class="name"><code>[data-month-grid-cell][data-selected]</code></td><td>Selected month tile.</td></tr>
<tr><td class="name"><code>[data-month-grid-cell][data-current]</code></td><td>Today's month accent.</td></tr>
<tr><td class="name"><code>[data-month-grid-cell][data-disabled]</code></td><td>Out of bounds / disabled.</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'a11y'}
<section data-uix-section>
<h2 data-uix-section-title>Accessibility</h2>
<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>←</span> / <span data-uix-kbd>→</span></td><td>Move focus ±1 month (RTL-flipped).</td></tr>
<tr><td><span data-uix-kbd>↑</span> / <span data-uix-kbd>↓</span></td><td>Move focus ±3 months (row width).</td></tr>
<tr><td><span data-uix-kbd>Home</span> / <span data-uix-kbd>End</span></td><td>First / last month of year.</td></tr>
<tr><td><span data-uix-kbd>PageUp</span> / <span data-uix-kbd>PageDown</span></td><td>±1 year (also moves placeholder).</td></tr>
<tr><td><span data-uix-kbd>Enter</span> / <span data-uix-kbd>Space</span></td><td>Select focused month.</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>aria-label</td><td class="type">{`{ariaLabel}`} or localised default</td></tr>
<tr><td class="name">heading</td><td>id</td><td class="type">referenced by <code>grid[aria-labelledby]</code></td></tr>
<tr><td class="name">prev-button / next-button</td><td>type / aria-label</td><td class="type">"button" / localised "Previous year" · "Next year"</td></tr>
<tr><td class="name">grid</td><td>role / aria-labelledby</td><td class="type">"grid" / {`<heading id>`}</td></tr>
<tr><td class="name">cell</td><td>role</td><td class="type">"gridcell"</td></tr>
<tr><td class="name">cell</td><td>aria-selected / aria-current / aria-disabled</td><td class="type">resolved per cell state</td></tr>
<tr><td class="name">cell</td><td>tabindex</td><td class="type">0 on focused cell · -1 on the rest (roving)</td></tr>
<tr><td class="name">cell</td><td>aria-label</td><td class="type">full month name + year (e.g. "May 2026")</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>APG</div>
<p>Follows <a href="https://www.w3.org/WAI/ARIA/apg/patterns/grid/" rel="noreferrer" target="_blank">WAI-ARIA APG Grid</a> pattern: <code>role="grid"</code> on the grid container, <code>role="gridcell"</code> + roving <code>tabindex</code> on cells. Cells expose <code>aria-selected</code>, <code>aria-current</code> (for today's month) and <code>aria-disabled</code> for out-of-bounds states.</p>
</section>
{/if}
</div>

Powered by TurnKey Linux.