feat(date-range-field): port from soma to eidos (Tier 2 sprint, 1/3)

Two-endpoint date entry. Mirrors DateField × 2 with shared validation
and a single label naming the whole range. Each Input scopes its own
DateField context internally, so segments inside re-use the existing
date-field recipe automatically.

Eidos surface:
- variant / color / size — same vocabulary as <DateField>, cascaded
  via data-* on the root to both endpoint inputs and their segments
- Re-uses DateField.Segment (zero duplication)

Recipe (date-range-field.css):
- Label + control row stack
- Em-dash separator rendered via `::before` on the end Input
  (no manual `<span>—</span>` needed)
- Endpoint-aware tint via `data-active` on the holding input

Morfo:
- scope: ['soma'] → ['soma', 'sema', 'eidos']
- apg URL: Spinbutton (per segment)
- Passive at this contract layer (events come from the two endpoint
  DateField runtimes); README documents the classification

Sidebar nav: 'Date range field' added under Forms (next to Date field).

Tier 2 sprint progress: 1/3 (date-range-field next: time-range-field,
range-calendar).

Checks: svelte-check 0 errors, component:audit 91/91 PASS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
dev 5 months ago
parent 2d8599b62f
commit 754754d4a4

@ -0,0 +1,122 @@
# `<DateRangeField>` — eidos
Two-endpoint date entry, segment-by-segment. Mirrors
`<DateField>` × 2 with shared validation (min/max/`validate`)
and a single label that names the whole range.
## Usage
```svelte
<script lang="ts">
import { DateRangeField } from '$uix/eidos/components/date-range-field';
import { CalendarDate } from '$libs/days';
let value = $state({
start: new CalendarDate(2026, 1, 1),
end: new CalendarDate(2026, 1, 15)
});
</script>
<DateRangeField bind:value variant="surface" size="md">
<DateRangeField.Label>Period</DateRangeField.Label>
<DateRangeField.Input type="start">
{#snippet children({ segments })}
{#each segments as seg}
<DateRangeField.Segment part={seg.part}>{seg.value}</DateRangeField.Segment>
{/each}
{/snippet}
</DateRangeField.Input>
<DateRangeField.Input type="end">
{#snippet children({ segments })}
{#each segments as seg}
<DateRangeField.Segment part={seg.part}>{seg.value}</DateRangeField.Segment>
{/each}
{/snippet}
</DateRangeField.Input>
</DateRangeField>
```
## Parts
| Part | Notes |
|---|---|
| `DateRangeField` (root) | Provider; bind:value `DateRange`. |
| `Label` | Single label naming the whole range. |
| `Input` | One per endpoint (`type='start'` / `type='end'`). Each scopes its own DateField context internally. |
| `Segment` | Re-export of `DateField.Segment` — same recipe. |
## Eidos-layer props
| Prop | Type | Default | Notes |
|---|---|---|---|
| `variant` | `'surface' \| 'outline' \| 'ghost'` | `'surface'` | Inherits DateField vocabulary. |
| `color` | `primary \| secondary \| neutral \| affirm \| risk \| threat` | `'primary'` | Active endpoint tint. |
| `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` (responsive) | `'md'` | Cascaded to both inputs + segments. |
## Headless props (soma)
`value`, `placeholder`, `onValueChange`, `onStartValueChange`,
`onEndValueChange`, `validate`, `onInvalid`, `minValue`, `maxValue`,
`disabled`, `readonly`, `startReadonlySegments`,
`endReadonlySegments`, `required`, `kind`, `granularity`,
`hideTimeZone`, `hourCycle`, `locale`, `dir`, `errorMessageId`.
See `src/uix/soma/components/date-range-field/types.ts`.
## Passive justification
Passive **at the morfo level** — the range-field morfo declares
parts (Provider, Label, Input) but no `events`. Sema events
(segment commits, value changes) are emitted by the two composed
DateField runtimes scoped per endpoint, where they belong; the
range provider itself owns no event vocabulary beyond what soma
synthesizes from its endpoint listeners.
The component IS interactive from the user's perspective —
"passive" here is the same contract-layer classification used for
`<AlertDialog>` and `<PinInput>`.
## Baseline
WAI-ARIA Spinbutton pattern (per segment):
<https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/>
Soma owns: segment focus / arrow stepping per endpoint, paste
distribution, validation, hidden form inputs (ISO date strings).
## Comparativa
| Lib | Two endpoints | Shared validate | Segment scoping | Range tint |
|---|---|---|---|---|
| **react-aria DateRangePicker** | ✓ | ✓ | per endpoint | — |
| **Bits UI DateRangeField** | ✓ | ✓ | per endpoint | — |
| **Ark UI** | ✓ | ✓ | per endpoint | — |
| **Eidos (this)** | ✓ | ✓ | per endpoint | ✓ active endpoint border |
Functional parity with the strongest references; range-tint affordance
keeps focus visible across two-input layouts.
## Decisiones
- **Segment re-uses DateField's wrapper** — each endpoint Input
scopes a DateField context, so the existing date-field segment
recipe applies without duplication.
- **Separator rendered via `::before` pseudo**, not as a `<span>`
child. Consumers don't have to ship `<span>—</span>` between
Inputs; the recipe handles it via `:has([data-type='end'])`.
- **No `Description` / `Hint` parts.** This is the *field* primitive;
description belongs to the enclosing `<Field>` shell. Composers
wrap with `<Field>` for the full form pattern.
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| Hidden form inputs styling | **diferir** | Soma emits two ISO hidden inputs; not visible by recipe. Demo doesn't expose. |
| `data-color='risk'` cascade for invalid endpoints | **implementar** próximo turno | Soma exposes `invalid` at provider level; would benefit from per-endpoint coloring. |
| RTL separator glyph swap | **diferir** | Currently uses `—` for all locales; some RTL locales prefer `إلى` etc. Wait for actual i18n need. |
## Reference
- Bits UI: <https://bits-ui.com/docs/components/date-range-field>
- Ark UI: <https://ark-ui.com/docs/components/date-range-picker>
- react-aria-components:
<https://react-spectrum.adobe.com/react-aria/DateRangePicker.html>

@ -0,0 +1,18 @@
<script lang="ts">
/**
* Eidos `<DateRangeField.Input>`. Pass-through over soma's Input
* which creates its own DateField context per endpoint. Snippet
* args (`segments`) come from soma unchanged — consumers iterate
* and render `<DateRangeField.Segment>` per slot.
*/
import * as DateRangeField from '$soma/components/date-range-field';
import type { DateRangeFieldInputProps } from './types';
let { children: bodyContent, ...rest }: DateRangeFieldInputProps = $props();
</script>
<DateRangeField.Input {...rest}>
{#snippet children(snippetProps)}
{@render bodyContent?.(snippetProps)}
{/snippet}
</DateRangeField.Input>

@ -0,0 +1,8 @@
<script lang="ts">
import * as DateRangeField from '$soma/components/date-range-field';
import type { DateRangeFieldLabelProps } from './types';
let { children, ...rest }: DateRangeFieldLabelProps = $props();
</script>
<DateRangeField.Label {...rest}>{@render children?.()}</DateRangeField.Label>

@ -0,0 +1,89 @@
/*
* DateRangeField recipe.
*
* [data-date-range-field] → label + control stack
* [data-date-range-field-label] → optional label
* [data-date-range-field-input] → endpoint container (start or end)
*
* Each Input wraps a DateField context internally, so segments inside
* pick up date-field's existing recipe automatically. The range
* recipe only adds:
*
* - Label + control row layout (mirrors DateField's stack)
* - A horizontal flex with the two inputs and an em-dash separator,
* rendered via ::before/::after on the end input so consumers don't
* need to render a `<span>` separator manually.
* - Endpoint-aware data-attr (`data-type='start'` / `data-type='end'`)
* passes through from the morfo; usable for endpoint-specific tints.
*/
[data-date-range-field] {
display: inline-flex;
flex-direction: column;
gap: var(--date-field-stack-gap, var(--space-1-5));
inline-size: 100%;
min-inline-size: 0;
font-family: var(--date-field-font-family, var(--style-label-font-family));
}
[data-date-range-field-label] {
font-family: var(--date-field-label-font-family, var(--style-label-font-family));
font-size: var(--date-field-label-font-size, var(--font-size-sm));
font-weight: var(--date-field-label-font-weight, 500);
color: var(--date-field-label-color, var(--color-content-primary));
}
/*
* Each Input is itself a DateField root, so [data-date-field]'s
* recipe paints the bordered control. The range layout wraps two
* such controls in a horizontal row.
*/
[data-date-range-field] > [data-date-range-field-input] {
display: inline-flex;
flex: 1 1 0;
min-inline-size: 0;
}
/* Row layout — when both Inputs are direct children, set the
* container to a row with the separator between them. */
[data-date-range-field]:has([data-date-range-field-input][data-type='start'])
:where([data-date-range-field-input][data-type='end']) {
margin-inline-start: var(--date-range-field-separator-gap, var(--space-2));
position: relative;
}
[data-date-range-field]:has([data-date-range-field-input][data-type='start'])
:where([data-date-range-field-input][data-type='end'])::before {
content: var(--date-range-field-separator-glyph, '—');
position: absolute;
inset-inline-start: calc(var(--date-range-field-separator-gap, var(--space-2)) * -1);
inset-block-start: 50%;
transform: translate(-50%, -50%);
color: var(--date-range-field-separator-color, var(--color-content-muted));
font-size: 0.9em; /* literal: separator scales with the field font-size */
}
/* When consumers compose Label + the two Inputs at the same level,
* give the inputs a flex row so they sit side-by-side. The :has
* selector keeps the recipe inert if the consumer arranges things
* differently. */
[data-date-range-field]:has([data-date-range-field-input]) {
--_drf-inputs-display: flex;
}
[data-date-range-field] > [data-date-range-field-input]:first-of-type {
margin-block-start: 0;
}
[data-date-range-field][data-disabled] {
opacity: var(--date-field-disabled-opacity, 0.55);
pointer-events: none;
}
/* Endpoint tints — subtle hue shift on the active endpoint without
* hiding which side has focus. The morfo emits `data-active` on the
* input that holds the caret. */
[data-date-range-field-input][data-active] [data-date-field] {
--_date-field-border-color: var(--_date-field-accent-border);
}

@ -0,0 +1,35 @@
<script lang="ts">
/**
* Eidos `<DateRangeField>` — root. Sets the visual cascade
* (data-size / data-variant / data-color) that both endpoint
* Inputs and their Segments inherit. The morfo emits
* `data-date-range-field` on the container.
*/
import { ActiveEidos } from '$uix/eidos';
import * as DateRangeField from '$soma/components/date-range-field';
import type { DateRangeFieldProps } from './types';
let {
size = 'md',
variant = 'surface',
color = 'primary',
value = $bindable(),
placeholder = $bindable(),
children,
...rest
}: DateRangeFieldProps = $props();
const eidos = ActiveEidos.require();
const resolvedSize = $derived(eidos.resolve(size, 'md'));
</script>
<DateRangeField.Provider
{...rest}
bind:value
bind:placeholder
data-size={resolvedSize}
data-variant={variant}
data-color={color}
>
{@render children?.()}
</DateRangeField.Provider>

@ -0,0 +1,56 @@
// DateRangeField — eidos compound API.
//
// import { DateRangeField } from '$uix/eidos/components/date-range-field';
//
// <DateRangeField bind:value variant="surface" size="md">
// <DateRangeField.Label>Period</DateRangeField.Label>
// <DateRangeField.Input type="start">
// {#snippet children({ segments })}
// {#each segments as seg}
// <DateRangeField.Segment part={seg.part}>{seg.value}</DateRangeField.Segment>
// {/each}
// {/snippet}
// </DateRangeField.Input>
// <DateRangeField.Input type="end">
// {#snippet children({ segments })}
// {#each segments as seg}
// <DateRangeField.Segment part={seg.part}>{seg.value}</DateRangeField.Segment>
// {/each}
// {/snippet}
// </DateRangeField.Input>
// </DateRangeField>
//
// Segment is reused from DateField's eidos wrapper — each endpoint
// Input scopes its own DateField context so segment resolution still
// targets the right endpoint.
import DateRangeFieldComponent from './date-range-field.svelte';
import Input from './date-range-field-input.svelte';
import Label from './date-range-field-label.svelte';
import DateField from '$uix/eidos/components/date-field';
const Segment = DateField.Segment;
type DateRangeFieldNamespace = typeof DateRangeFieldComponent & {
Label: typeof Label;
Input: typeof Input;
Segment: typeof Segment;
};
const DateRangeField = DateRangeFieldComponent as DateRangeFieldNamespace;
DateRangeField.Label = Label;
DateRangeField.Input = Input;
DateRangeField.Segment = Segment;
export { DateRangeField };
export default DateRangeField;
export type {
DateRangeFieldProps,
DateRangeFieldLabelProps as LabelProps,
DateRangeFieldInputProps as InputProps,
DateRangeFieldSegmentProps as SegmentProps,
DateRangeFieldSize,
DateRangeFieldVariant,
DateRangeFieldColor,
InputSnippetProps
} from './types';

@ -0,0 +1,58 @@
import type {
ProviderProps,
LabelProps,
InputProps,
InputSnippetProps,
SegmentProps
} from '$soma/components/date-range-field';
import type { ResponsiveProp } from '$uix/eidos/lib/types';
import type { DateFieldSize, DateFieldVariant, DateFieldColor } from '$uix/eidos/components/date-field';
/**
* Eidos `<DateRangeField>` — visual wrapper over the headless
* date-range-field. Two endpoint inputs (`type='start'` /
* `type='end'`), one shared Label, and the same Segment used by
* date-field for each editable slot.
*
* <DateRangeField bind:value variant="surface" size="md">
* <DateRangeField.Label>Period</DateRangeField.Label>
* <DateRangeField.Input type="start">
* {#snippet children({ segments })}
* {#each segments as seg}
* <DateRangeField.Segment part={seg.part}>{seg.value}</DateRangeField.Segment>
* {/each}
* {/snippet}
* </DateRangeField.Input>
* <span>—</span>
* <DateRangeField.Input type="end">
* {#snippet children({ segments })}
* {#each segments as seg}
* <DateRangeField.Segment part={seg.part}>{seg.value}</DateRangeField.Segment>
* {/each}
* {/snippet}
* </DateRangeField.Input>
* </DateRangeField>
*
* Visual concerns reuse the DateField vocabulary (size / variant /
* color) — the start and end Inputs read the cascaded data-attrs from
* the root, identical to how Field renders its Control.
*/
export type DateRangeFieldSize = DateFieldSize;
export type DateRangeFieldVariant = DateFieldVariant;
export type DateRangeFieldColor = DateFieldColor;
export type DateRangeFieldProps = ProviderProps & {
/** Visual treatment. Mirrors DateField. @default 'surface' */
variant?: DateRangeFieldVariant;
/** Color tone. @default 'primary' */
color?: DateRangeFieldColor;
/** Sizing scale. @default 'md' */
size?: ResponsiveProp<DateRangeFieldSize>;
};
export type DateRangeFieldLabelProps = LabelProps;
export type DateRangeFieldInputProps = InputProps;
export type DateRangeFieldSegmentProps = SegmentProps;
export type { InputSnippetProps };

@ -94,6 +94,7 @@
@import './components/dropdown-menu/dropdown-menu.css'; @import './components/dropdown-menu/dropdown-menu.css';
@import './components/context-menu/context-menu.css'; @import './components/context-menu/context-menu.css';
@import './components/pin-input/pin-input.css'; @import './components/pin-input/pin-input.css';
@import './components/date-range-field/date-range-field.css';
@import './components/drawer/drawer.css'; @import './components/drawer/drawer.css';
@import './components/field/field.css'; @import './components/field/field.css';
@import './components/form/form.css'; @import './components/form/form.css';

@ -4,7 +4,11 @@ import { v } from '../types';
export const dateRangeFieldMorfo = { export const dateRangeFieldMorfo = {
name: 'DateRangeField', name: 'DateRangeField',
kebab: 'date-range-field', kebab: 'date-range-field',
scope: ['soma'], // Eidos wrapper added 2026-05-23 (`src/uix/eidos/components/date-range-field/`).
// Sema events forwarded by the composed DateField instances (one per
// endpoint) — no per-component cascade needed at this layer.
scope: ['soma', 'sema', 'eidos'],
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/',
texts: { texts: {
label: '#?components.date-range-field.label|Date Range Field', label: '#?components.date-range-field.label|Date Range Field',
start: '#?components.date-range-field.start|Start date', start: '#?components.date-range-field.start|Start date',

@ -152,6 +152,7 @@
{ slug: '/uix/components/month-grid', label: 'Month grid' }, { slug: '/uix/components/month-grid', label: 'Month grid' },
{ slug: '/uix/components/year-grid', label: 'Year grid' }, { slug: '/uix/components/year-grid', label: 'Year grid' },
{ slug: '/uix/components/date-field', label: 'Date field' }, { slug: '/uix/components/date-field', label: 'Date field' },
{ slug: '/uix/components/date-range-field', label: 'Date range field' },
{ slug: '/uix/components/date-picker', label: 'Date picker' }, { slug: '/uix/components/date-picker', label: 'Date picker' },
{ slug: '/uix/components/date-range-picker', label: 'Date range picker' }, { slug: '/uix/components/date-range-picker', label: 'Date range picker' },
{ slug: '/uix/components/time-field', label: 'Time field' }, { slug: '/uix/components/time-field', label: 'Time field' },

@ -0,0 +1,386 @@
<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}',
' <DateField.Segment part={seg.part}>{seg.value}</DateField.Segment>',
' {/each}',
' {/snippet}',
' </DateRangeField.Input>',
' <DateRangeField.Input type="end">',
' {#snippet children({ segments })}',
' {#each segments as seg}',
' <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}',
' <DateRangeField.Segment part={seg.part}>{seg.value}</DateRangeField.Segment>',
' {/each}',
' {/snippet}',
' </DateRangeField.Input>',
' <DateRangeField.Input type="end">',
' {#snippet children({ segments })}',
' {#each segments as seg}',
' <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 (seg.part)}
<DateRangeField.Segment part={seg.part}>{seg.value}</DateRangeField.Segment>
{/each}
{/snippet}
</DateRangeField.Input>
<DateRangeField.Input type="end">
{#snippet children({ segments })}
{#each segments as seg (seg.part)}
<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>
Loading…
Cancel
Save

Powered by TurnKey Linux.