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/date-range-field/+page.svelte

387 lines
12 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 {
DateRangeField,
type DateRangeFieldSize,
type DateRangeFieldVariant,
type DateRangeFieldColor
} from '$uix/eidos/components/date-range-field';
import { CalendarDate, type DateRange } from '$libs/days';
import { compileMorfo } from '$uix/morfo';
import { dateRangeFieldMorfo } from '@/uix/morfo/components/date-range-field';
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
let tab = $state<Tab>('live');
const sizes: DateRangeFieldSize[] = ['xs', 'sm', 'md', 'lg', 'xl'];
const variants: DateRangeFieldVariant[] = ['surface', 'outline', 'ghost'];
const colors: DateRangeFieldColor[] = ['primary', 'secondary', 'neutral', 'affirm', 'risk', 'threat'];
const kinds: Array<'date' | 'month' | 'year'> = ['date', 'month', 'year'];
const placeholderDate = new CalendarDate(2026, 1, 1);
let value = $state<DateRange>({
start: new CalendarDate(2026, 5, 19),
end: new CalendarDate(2026, 6, 3)
});
let placeholder = $state<CalendarDate>(placeholderDate);
let size = $state<DateRangeFieldSize>('md');
let variant = $state<DateRangeFieldVariant>('surface');
let color = $state<DateRangeFieldColor>('primary');
let kind = $state<'date' | 'month' | 'year'>('date');
let disabled = $state(false);
let readonly = $state(false);
let required = $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(dateRangeFieldMorfo);
const partsList = $derived([...compiled.parts.byKebab.values()]);
function fmtTime(at: number): string {
const d = new Date(at);
return `${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
}
function fmtValue(v: DateRange): string {
const s = v.start?.toString() ?? '—';
const e = v.end?.toString() ?? '—';
return `${s} → ${e}`;
}
const somaSnippet = $derived(
[
"<script lang='ts'>",
" import * as DateRangeField from '$soma/components/date-range-field';",
" import { CalendarDate, type DateRange } from '$libs/days';",
' let value = $state<DateRange>({',
' start: new CalendarDate(2026, 5, 19),',
' end: new CalendarDate(2026, 6, 3)',
' });',
'</' + 'script>',
'',
'<DateRangeField.Provider',
' bind:value',
kind !== 'date' && ` kind="${kind}"`,
disabled && ' disabled',
readonly && ' readonly',
required && ' required',
'>',
' <DateRangeField.Label>Period</DateRangeField.Label>',
' <DateRangeField.Input type="start">',
' {#snippet children({ segments })}',
' {#each segments as seg, i (i)}',
' <DateField.Segment part={seg.part}>{seg.value}</DateField.Segment>',
' {/each}',
' {/snippet}',
' </DateRangeField.Input>',
' <DateRangeField.Input type="end">',
' {#snippet children({ segments })}',
' {#each segments as seg, i (i)}',
' <DateField.Segment part={seg.part}>{seg.value}</DateField.Segment>',
' {/each}',
' {/snippet}',
' </DateRangeField.Input>',
'</DateRangeField.Provider>'
]
.filter(Boolean)
.join('\n')
);
const eidosSnippet = $derived(
[
"<script lang='ts'>",
" import { DateRangeField } from '$uix/eidos/components/date-range-field';",
" import { CalendarDate, type DateRange } from '$libs/days';",
' let value = $state<DateRange>({',
' start: new CalendarDate(2026, 5, 19),',
' end: new CalendarDate(2026, 6, 3)',
' });',
'</' + 'script>',
'',
'<DateRangeField',
' bind:value',
variant !== 'surface' && ` variant="${variant}"`,
color !== 'primary' && ` color="${color}"`,
size !== 'md' && ` size="${size}"`,
kind !== 'date' && ` kind="${kind}"`,
disabled && ' disabled',
readonly && ' readonly',
required && ' required',
'>',
' <DateRangeField.Label>Period</DateRangeField.Label>',
' <DateRangeField.Input type="start">',
' {#snippet children({ segments })}',
' {#each segments as seg, i (i)}',
' <DateRangeField.Segment part={seg.part}>{seg.value}</DateRangeField.Segment>',
' {/each}',
' {/snippet}',
' </DateRangeField.Input>',
' <DateRangeField.Input type="end">',
' {#snippet children({ segments })}',
' {#each segments as seg, i (i)}',
' <DateRangeField.Segment part={seg.part}>{seg.value}</DateRangeField.Segment>',
' {/each}',
' {/snippet}',
' </DateRangeField.Input>',
'</DateRangeField>'
]
.filter(Boolean)
.join('\n')
);
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Form · DateRangeField</div>
<h1 data-uix-page-title>DateRangeField</h1>
<p data-uix-page-lede>
Two-endpoint date entry, segment-by-segment. Mirrors
<code>DateField</code> × 2 with shared validation and a single
label naming the whole range.
<span data-uix-layer-badge="soma">soma</span> scopes a
DateField context per endpoint; eidos cascades size / variant
/ color from the root.
</p>
<div data-uix-page-meta>
<span data-uix-meta-pill>
<span data-uix-meta-key>value</span>{fmtValue(value)}
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>kind</span>{kind}
</span>
</div>
</header>
<div data-uix-stage>
<div data-uix-stage-area bind:this={stageRef}>
<DateRangeField
bind:value
bind:placeholder
{size}
{variant}
{color}
{kind}
{disabled}
{readonly}
{required}
>
<DateRangeField.Label>Period</DateRangeField.Label>
<DateRangeField.Input type="start">
{#snippet children({ segments })}
{#each segments as seg, i (i)}
<DateRangeField.Segment part={seg.part}>{seg.value}</DateRangeField.Segment>
{/each}
{/snippet}
</DateRangeField.Input>
<DateRangeField.Input type="end">
{#snippet children({ segments })}
{#each segments as seg, i (i)}
<DateRangeField.Segment part={seg.part}>{seg.value}</DateRangeField.Segment>
{/each}
{/snippet}
</DateRangeField.Input>
</DateRangeField>
</div>
<div data-uix-stage-trace>
<span data-uix-stage-trace-key>trace</span>
<span>{trace.length === 0 ? 'no semantic events' : `${trace.length} event(s)`}</span>
{#if trace[0]}
<span style="color: var(--uix-text-faint)">·</span>
<span data-uix-stage-trace-key>last</span>
<span>{trace[0].event} ({trace[0].family}) @{fmtTime(trace[0].at)}</span>
{/if}
</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</span>
</button>
<button data-uix-tab data-active={tab === 'sema'} onclick={() => (tab = 'sema')}>
<span data-uix-layer-badge="sema">sema</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 + behaviour
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>kind</span>
<span data-uix-chips role="radiogroup">
{#each kinds as opt}
<button data-uix-chip data-active={kind === opt} onclick={() => (kind = opt)}>{opt}</button>
{/each}
</span>
</label>
<label data-uix-control data-control-kind="switch">
<input type="checkbox" bind:checked={disabled} /> disabled
</label>
<label data-uix-control data-control-kind="switch">
<input type="checkbox" bind:checked={readonly} /> readonly
</label>
<label data-uix-control data-control-kind="switch">
<input type="checkbox" bind:checked={required} /> required
</label>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props · chrome
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>size</span>
<span data-uix-chips role="radiogroup">
{#each sizes as opt}
<button data-uix-chip data-active={size === opt} onclick={() => (size = opt)}>{opt}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>variant</span>
<span data-uix-chips role="radiogroup">
{#each variants as opt}
<button data-uix-chip data-active={variant === opt} onclick={() => (variant = opt)}>{opt}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>color</span>
<span data-uix-chips role="radiogroup">
{#each colors as opt}
<button data-uix-chip data-active={color === opt} onclick={() => (color = opt)}>{opt}</button>
{/each}
</span>
</label>
</div>
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="soma">soma</span>
<span>headless · two endpoint contexts</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 · cascading size / variant / color</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>
Inherits the full soma API (see
<code>src/uix/soma/components/date-range-field/types.ts</code>).
The eidos layer adds <code>size</code> /
<code>variant</code> / <code>color</code> with the same
vocabulary as <code>&lt;DateField&gt;</code>.
</p>
</section>
{/if}
{#if tab === 'morfo'}
<section data-uix-section>
<h2 data-uix-section-title>Morfo contract</h2>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Kebab</th><th>Role</th><th>Marker</th></tr></thead>
<tbody>
{#each partsList as part}
{@const partAny = part as unknown as Record<string, unknown>}
<tr>
<td class="name">{partAny.kebab}</td>
<td>{partAny.role ?? '—'}</td>
<td><code>{partAny.marker ?? '—'}</code></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>
<p data-uix-section-desc>
Range-field morfo declares no events of its own. Perceptual
feedback comes from the two endpoint DateField runtimes
scoped inside each Input.
</p>
</section>
{/if}
{#if tab === 'recipe'}
<section data-uix-section>
<h2 data-uix-section-title>Eidos recipe</h2>
<p data-uix-section-desc>
<code>date-range-field.css</code> wraps the label + the two
inputs with an em-dash separator rendered via
<code>::before</code> on the end input. Cell visuals come
from <code>date-field.css</code> — each Input scopes its
own DateField context.
</p>
</section>
{/if}
{#if tab === 'a11y'}
<section data-uix-section>
<h2 data-uix-section-title>A11y</h2>
<p data-uix-section-desc>
WAI-ARIA <a
href="https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/"
target="_blank"
rel="noopener">Spinbutton</a
> pattern per segment. Soma owns segment focus, arrow
stepping per endpoint, and paste distribution.
</p>
</section>
{/if}
</div>

Powered by TurnKey Linux.