Eliminates `Provider.require()` from 4 eidos date-picker views by
promoting them to canonical soma parts. Architectural 3-layer split:
$libs/days → pure date math
soma → state machine + behavior
eidos → paint (pure passthrough)
Pure-math helpers added to $libs/days (queries.ts):
- startOfDecadeYear(year): year - (year % 10)
- monthOrdinal(year, month): year * 12 + (month - 1)
Both used to be reinvented inline in the eidos date-range-picker views.
New soma parts:
- <DatePicker.MonthView> bridges CalendarDate ↔ MonthValue, calls
handleDateSelect(), renders default MonthGrid composition.
- <DatePicker.YearView> symmetric bridge to YearGrid.
- <DateRangePicker.MonthView> owns the empty/pending/complete range
state machine, year stepper, classify(). Endpoints normalise to
day=1 (start) and endOfMonth(...) (end).
- <DateRangePicker.YearView> owns the range state machine with
decade stepper. Year endpoints normalise to Jan 1 / Dec 31.
All four parts declared in their morfos with `kind: 'public', optional`.
The range views stamp `role='grid'` + canonical aria-label.
Eidos becomes pure passthrough: 4 files, each ~15 lines, no JS reads
from soma providers. Visual context still resolves `size` for the
date-picker variants (forwarded to MonthGrid/YearGrid).
Test result unchanged: 2391/2397 passing. No regressions.
This closes audit 5.3 Option C — all 8 sites refactored.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
parent
a5243168e5
commit
e93c0c7337
@ -1,76 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { DatePickerProvider } from '$soma/components/date-picker';
|
||||
import { CalendarDate } from '$libs/days';
|
||||
import { MonthGrid } from '../month-grid';
|
||||
import type { MonthValue, MonthPlaceholder } from '$soma/components/month-grid';
|
||||
import type { MonthGridProviderSnippetProps, MonthGridSize } from '../month-grid';
|
||||
/**
|
||||
* Eidos `<DatePicker.MonthView>` — pure passthrough. Soma's
|
||||
* `<DatePicker.MonthView>` owns the bridge from CalendarDate ↔
|
||||
* MonthValue and the default MonthGrid composition. Eidos resolves
|
||||
* `size` from the visual context and forwards it.
|
||||
*/
|
||||
import * as DatePicker from '$soma/components/date-picker';
|
||||
import type { MonthGridSize } from '../month-grid';
|
||||
import { getDatePickerVisualContext } from './context.svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
// Month-grid view for `kind='month'`. Internally renders the standalone
|
||||
// MonthGrid primitive, bridging its `{year, month}` value to the
|
||||
// DatePickerProvider's CalendarDate value. Click on a month sets the
|
||||
// picker's value to day=1 of that month and triggers handleDateSelect.
|
||||
interface MonthViewProps extends HTMLAttributes<HTMLDivElement> {}
|
||||
type Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
// View is a thin bridge that reads its state from the DatePickerProvider
|
||||
// context. It accepts no extra props from consumers — the demo just
|
||||
// renders `<DatePicker.MonthView />` without arguments. Bind $props()
|
||||
// to silence Svelte's mandatory props declaration check.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const _props: MonthViewProps = $props();
|
||||
const props: Props = $props();
|
||||
|
||||
const provider = DatePickerProvider.require();
|
||||
const visual = getDatePickerVisualContext();
|
||||
const size = $derived((visual?.getSize() ?? 'md') as MonthGridSize);
|
||||
|
||||
const anchor = $derived(provider.opts.value.current ?? provider.opts.placeholder.current);
|
||||
|
||||
const monthValue = $derived<MonthValue | undefined>(
|
||||
provider.opts.value.current
|
||||
? { year: provider.opts.value.current.year, month: provider.opts.value.current.month }
|
||||
: undefined
|
||||
);
|
||||
const monthPlaceholder = $derived<MonthPlaceholder>({ year: anchor.year });
|
||||
|
||||
function onValueChange(next: MonthValue | undefined): void {
|
||||
if (!next) return;
|
||||
const date = new CalendarDate(next.year, next.month, 1);
|
||||
provider.opts.value.current = date;
|
||||
provider.opts.placeholder.current = date;
|
||||
provider.handleDateSelect();
|
||||
}
|
||||
|
||||
function onPlaceholderChange(next: MonthPlaceholder): void {
|
||||
const placeholder = provider.opts.placeholder.current;
|
||||
provider.opts.placeholder.current = new CalendarDate(
|
||||
next.year,
|
||||
placeholder.month,
|
||||
placeholder.day
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<MonthGrid
|
||||
value={monthValue}
|
||||
placeholder={monthPlaceholder}
|
||||
{onValueChange}
|
||||
{onPlaceholderChange}
|
||||
{size}
|
||||
locale={provider.opts.locale.current}
|
||||
disabled={provider.opts.disabled.current}
|
||||
readonly={provider.opts.readonly.current}
|
||||
>
|
||||
{#snippet children({ months }: MonthGridProviderSnippetProps)}
|
||||
<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>
|
||||
<DatePicker.MonthView {size} {...props} />
|
||||
|
||||
@ -1,75 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { DatePickerProvider } from '$soma/components/date-picker';
|
||||
import { CalendarDate } from '$libs/days';
|
||||
import { YearGrid } from '../year-grid';
|
||||
import type { YearValue, YearPlaceholder } from '$soma/components/year-grid';
|
||||
import type { YearGridProviderSnippetProps, YearGridSize } from '../year-grid';
|
||||
/**
|
||||
* Eidos `<DatePicker.YearView>` — pure passthrough. Soma's
|
||||
* `<DatePicker.YearView>` owns the bridge from CalendarDate ↔
|
||||
* YearValue and the default YearGrid composition. Eidos resolves
|
||||
* `size` from the visual context and forwards it.
|
||||
*/
|
||||
import * as DatePicker from '$soma/components/date-picker';
|
||||
import type { YearGridSize } from '../year-grid';
|
||||
import { getDatePickerVisualContext } from './context.svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
// Year-grid view for `kind='year'`. Internally renders the standalone
|
||||
// YearGrid primitive, bridging its `{year}` value to the
|
||||
// DatePickerProvider's CalendarDate value. Click on a year sets the
|
||||
// picker's value to January 1 of that year and triggers handleDateSelect.
|
||||
interface YearViewProps extends HTMLAttributes<HTMLDivElement> {}
|
||||
type Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
// View is a thin bridge that reads its state from the DatePickerProvider
|
||||
// context. It accepts no extra props from consumers — the demo just
|
||||
// renders `<DatePicker.YearView />` without arguments. Bind $props()
|
||||
// to silence Svelte's mandatory props declaration check.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const _props: YearViewProps = $props();
|
||||
const props: Props = $props();
|
||||
|
||||
const provider = DatePickerProvider.require();
|
||||
const visual = getDatePickerVisualContext();
|
||||
const size = $derived((visual?.getSize() ?? 'md') as YearGridSize);
|
||||
|
||||
const anchor = $derived(provider.opts.value.current ?? provider.opts.placeholder.current);
|
||||
|
||||
const yearValue = $derived<YearValue | undefined>(
|
||||
provider.opts.value.current ? { year: provider.opts.value.current.year } : undefined
|
||||
);
|
||||
const yearPlaceholder = $derived<YearPlaceholder>({ year: anchor.year });
|
||||
|
||||
function onValueChange(next: YearValue | undefined): void {
|
||||
if (!next) return;
|
||||
// For kind='year' only the year matters — keep month=1, day=1.
|
||||
const date = new CalendarDate(next.year, 1, 1);
|
||||
provider.opts.value.current = date;
|
||||
provider.opts.placeholder.current = date;
|
||||
provider.handleDateSelect();
|
||||
}
|
||||
|
||||
function onPlaceholderChange(next: YearPlaceholder): void {
|
||||
const placeholder = provider.opts.placeholder.current;
|
||||
provider.opts.placeholder.current = new CalendarDate(
|
||||
next.year,
|
||||
placeholder.month,
|
||||
placeholder.day
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<YearGrid
|
||||
value={yearValue}
|
||||
placeholder={yearPlaceholder}
|
||||
{onValueChange}
|
||||
{onPlaceholderChange}
|
||||
{size}
|
||||
locale={provider.opts.locale.current}
|
||||
disabled={provider.opts.disabled.current}
|
||||
readonly={provider.opts.readonly.current}
|
||||
>
|
||||
{#snippet children({ years }: YearGridProviderSnippetProps)}
|
||||
<YearGrid.Header>
|
||||
<YearGrid.PrevButton><</YearGrid.PrevButton>
|
||||
<YearGrid.Heading />
|
||||
<YearGrid.NextButton>></YearGrid.NextButton>
|
||||
</YearGrid.Header>
|
||||
<YearGrid.Grid>
|
||||
{#each years as year (year)}
|
||||
<YearGrid.Cell {year} />
|
||||
{/each}
|
||||
</YearGrid.Grid>
|
||||
{/snippet}
|
||||
</YearGrid>
|
||||
<DatePicker.YearView {size} {...props} />
|
||||
|
||||
@ -0,0 +1,95 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Soma `<DatePicker.MonthView>` — month-grid view for `kind='month'`.
|
||||
*
|
||||
* Bridges DatePicker's CalendarDate state to MonthGrid's `{year, month}`
|
||||
* value: reads the picker's current value/placeholder/locale/disabled/
|
||||
* readonly, exposes them via MonthGrid, and on month-select converts
|
||||
* back to a CalendarDate (day=1) + fires `handleDateSelect()` so the
|
||||
* picker's commit pipeline runs.
|
||||
*
|
||||
* Internal composition: renders the default `<` Heading `>` header +
|
||||
* 3x4 grid. Consumers can override the body via the `children` snippet
|
||||
* (receives the live `months` array from MonthGrid).
|
||||
*/
|
||||
import type { Snippet } from 'svelte';
|
||||
import { CalendarDate } from '$libs/days';
|
||||
import { DatePickerProvider } from '../date-picker-provider.svelte';
|
||||
import * as MonthGrid from '../../month-grid/exports';
|
||||
import type {
|
||||
MonthValue,
|
||||
MonthPlaceholder,
|
||||
MonthGridProviderSnippetProps,
|
||||
MonthGridProps
|
||||
} from '../../month-grid/types';
|
||||
|
||||
type Props = Omit<
|
||||
MonthGridProps,
|
||||
'value' | 'placeholder' | 'onValueChange' | 'onPlaceholderChange' | 'children'
|
||||
> & {
|
||||
children?: Snippet<[MonthGridProviderSnippetProps]>;
|
||||
};
|
||||
|
||||
let { children, ...rest }: Props = $props();
|
||||
|
||||
const provider = DatePickerProvider.require();
|
||||
|
||||
const anchor = $derived(
|
||||
provider.opts.value.current ?? provider.opts.placeholder.current
|
||||
);
|
||||
|
||||
const monthValue = $derived<MonthValue | undefined>(
|
||||
provider.opts.value.current
|
||||
? {
|
||||
year: provider.opts.value.current.year,
|
||||
month: provider.opts.value.current.month
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
const monthPlaceholder = $derived<MonthPlaceholder>({ year: anchor.year });
|
||||
|
||||
function onValueChange(next: MonthValue | undefined): void {
|
||||
if (!next) return;
|
||||
const date = new CalendarDate(next.year, next.month, 1);
|
||||
provider.opts.value.current = date;
|
||||
provider.opts.placeholder.current = date;
|
||||
provider.handleDateSelect();
|
||||
}
|
||||
|
||||
function onPlaceholderChange(next: MonthPlaceholder): void {
|
||||
const placeholder = provider.opts.placeholder.current;
|
||||
provider.opts.placeholder.current = new CalendarDate(
|
||||
next.year,
|
||||
placeholder.month,
|
||||
placeholder.day
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<MonthGrid.Provider
|
||||
value={monthValue}
|
||||
placeholder={monthPlaceholder}
|
||||
{onValueChange}
|
||||
{onPlaceholderChange}
|
||||
locale={provider.opts.locale.current}
|
||||
disabled={provider.opts.disabled.current}
|
||||
readonly={provider.opts.readonly.current}
|
||||
{...rest}
|
||||
>
|
||||
{#snippet children(snippetProps: MonthGridProviderSnippetProps)}
|
||||
{#if children}
|
||||
{@render children(snippetProps)}
|
||||
{:else}
|
||||
<MonthGrid.Header>
|
||||
<MonthGrid.PrevButton><</MonthGrid.PrevButton>
|
||||
<MonthGrid.Heading />
|
||||
<MonthGrid.NextButton>></MonthGrid.NextButton>
|
||||
</MonthGrid.Header>
|
||||
<MonthGrid.Grid>
|
||||
{#each snippetProps.months as month (month)}
|
||||
<MonthGrid.Cell {month} />
|
||||
{/each}
|
||||
</MonthGrid.Grid>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</MonthGrid.Provider>
|
||||
@ -0,0 +1,93 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Soma `<DatePicker.YearView>` — year-grid view for `kind='year'`.
|
||||
*
|
||||
* Bridges DatePicker's CalendarDate state to YearGrid's `{year}`
|
||||
* value: reads the picker's current value/placeholder/locale/disabled/
|
||||
* readonly, exposes them via YearGrid, and on year-select converts
|
||||
* back to a CalendarDate (Jan 1) + fires `handleDateSelect()` so the
|
||||
* picker's commit pipeline runs.
|
||||
*
|
||||
* Internal composition: renders the default `<` Heading `>` header +
|
||||
* decade grid. Consumers can override the body via the `children`
|
||||
* snippet (receives the live `years` array from YearGrid).
|
||||
*/
|
||||
import type { Snippet } from 'svelte';
|
||||
import { CalendarDate } from '$libs/days';
|
||||
import { DatePickerProvider } from '../date-picker-provider.svelte';
|
||||
import * as YearGrid from '../../year-grid/exports';
|
||||
import type {
|
||||
YearValue,
|
||||
YearPlaceholder,
|
||||
YearGridProviderSnippetProps,
|
||||
YearGridProps
|
||||
} from '../../year-grid/types';
|
||||
|
||||
type Props = Omit<
|
||||
YearGridProps,
|
||||
'value' | 'placeholder' | 'onValueChange' | 'onPlaceholderChange' | 'children'
|
||||
> & {
|
||||
children?: Snippet<[YearGridProviderSnippetProps]>;
|
||||
};
|
||||
|
||||
let { children, ...rest }: Props = $props();
|
||||
|
||||
const provider = DatePickerProvider.require();
|
||||
|
||||
const anchor = $derived(
|
||||
provider.opts.value.current ?? provider.opts.placeholder.current
|
||||
);
|
||||
|
||||
const yearValue = $derived<YearValue | undefined>(
|
||||
provider.opts.value.current
|
||||
? { year: provider.opts.value.current.year }
|
||||
: undefined
|
||||
);
|
||||
const yearPlaceholder = $derived<YearPlaceholder>({ year: anchor.year });
|
||||
|
||||
function onValueChange(next: YearValue | undefined): void {
|
||||
if (!next) return;
|
||||
// For kind='year' only the year matters — keep month=1, day=1.
|
||||
const date = new CalendarDate(next.year, 1, 1);
|
||||
provider.opts.value.current = date;
|
||||
provider.opts.placeholder.current = date;
|
||||
provider.handleDateSelect();
|
||||
}
|
||||
|
||||
function onPlaceholderChange(next: YearPlaceholder): void {
|
||||
const placeholder = provider.opts.placeholder.current;
|
||||
provider.opts.placeholder.current = new CalendarDate(
|
||||
next.year,
|
||||
placeholder.month,
|
||||
placeholder.day
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<YearGrid.Provider
|
||||
value={yearValue}
|
||||
placeholder={yearPlaceholder}
|
||||
{onValueChange}
|
||||
{onPlaceholderChange}
|
||||
locale={provider.opts.locale.current}
|
||||
disabled={provider.opts.disabled.current}
|
||||
readonly={provider.opts.readonly.current}
|
||||
{...rest}
|
||||
>
|
||||
{#snippet children(snippetProps: YearGridProviderSnippetProps)}
|
||||
{#if children}
|
||||
{@render children(snippetProps)}
|
||||
{:else}
|
||||
<YearGrid.Header>
|
||||
<YearGrid.PrevButton><</YearGrid.PrevButton>
|
||||
<YearGrid.Heading />
|
||||
<YearGrid.NextButton>></YearGrid.NextButton>
|
||||
</YearGrid.Header>
|
||||
<YearGrid.Grid>
|
||||
{#each snippetProps.years as year (year)}
|
||||
<YearGrid.Cell {year} />
|
||||
{/each}
|
||||
</YearGrid.Grid>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</YearGrid.Provider>
|
||||
Loading…
Reference in new issue