refactor(pickers): date/time/date-range pickers consume shared PickerShell

Follow-up to commit 714ee373 (PickerShell extraction). Norm N-8 now
applied to every composite picker — date-picker, time-picker and
date-range-picker stop carrying their own Footer/Clear/Cancel/Close
duplicates and read the shared <PickerShell.Footer/Clear/Cancel/Close>
through `pickerShellContext`.

Soma changes
------------
- date-picker, time-picker, date-range-picker providers each gain a
  `pickerShellHandle: PickerShellHandle` field exposing
  `getMode/commit/cancel/clear`, and call
  `pickerShellContext.set(this.pickerShellHandle)` in the constructor
  so descendant Eidos shell parts can drive the picker.

Morfo changes
-------------
- Footer/Clear/Cancel/Close parts removed from each picker morfo.
  The shared chrome lives outside the per-picker contract — its data
  attrs are `[data-picker-footer/clear/cancel/close]` (no per-picker
  prefix). Comment block in each morfo file documents the move.

Eidos changes
-------------
- Deleted 12 obsolete files:
    src/uix/eidos/components/date-picker/date-picker-{footer,clear,cancel,close}.svelte
    src/uix/eidos/components/time-picker/time-picker-{footer,clear,cancel,close}.svelte
    src/uix/eidos/components/date-range-picker/date-range-picker-{footer,clear,cancel,close}.svelte
- Each picker's `index.ts` re-imports the 4 parts from
  `../picker-shell/picker-shell-{footer,clear,cancel,close}.svelte`
  so the public namespace stays `X.Footer / X.Clear / X.Cancel /
  X.Close` — no breaking change for consumers.
- Each picker's `types.ts` re-exports `FooterProps/ClearProps/
  CancelProps/CloseProps` from `../picker-shell`, replacing the stale
  per-picker types that referenced soma popover Close props.

CSS changes
-----------
- Deleted ~190 lines of duplicated footer/clear/cancel/close rules
  from date-picker.css, time-picker.css, date-range-picker.css. The
  canonical `[data-picker-footer/clear/cancel/close]` rules in
  picker-shell.css now style every picker uniformly.
- prefers-reduced-motion blocks pruned of the same selectors.

Public API
----------
Unchanged from a consumer perspective: `<DatePicker.Footer>`,
`<TimePicker.Clear>`, `<DateRangePicker.Close>`, etc. still resolve
to working components. The inner DOM markers change from
`[data-{x}-picker-footer]` to `[data-picker-footer]` — anyone with
custom CSS targeting the old selectors must migrate.

Checks
------
- 67/67 component-audit PASS
- 40/40 morfo:check PASS
- svelte-check 0 errors / 0 warnings

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

@ -1,38 +0,0 @@
<script lang="ts">
import { DatePickerProvider } from '$soma/components/date-picker';
import { getActiveUix } from '$active-uix';
import type { Snippet } from 'svelte';
import type { HTMLButtonAttributes } from 'svelte/elements';
// Cancel action — reverts to the value snapshot taken on open and
// closes the popover. Renders unconditionally — visibility is the
// consumer's responsibility (include the part to show it).
interface CancelProps extends HTMLButtonAttributes {
children?: Snippet;
}
const { children, onclick, ...rest }: CancelProps = $props();
const uix = getActiveUix();
const provider = DatePickerProvider.require();
function handleClick(event: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) {
provider.cancel();
if (typeof onclick === 'function') onclick.call(event.currentTarget, event);
}
</script>
<button
type="button"
data-date-picker-cancel
data-action="cancel"
aria-label={uix.langs.ts('#?components.date-picker.cancel|Cancel')}
onclick={handleClick}
{...rest}
>
{#if children}
{@render children()}
{:else}
{uix.langs.ts('#?components.date-picker.cancel|Cancel')}
{/if}
</button>

@ -1,38 +0,0 @@
<script lang="ts">
import { DatePickerProvider } from '$soma/components/date-picker';
import { getActiveUix } from '$active-uix';
import type { Snippet } from 'svelte';
import type { HTMLButtonAttributes } from 'svelte/elements';
// Clear action — resets the picker's value (undefined) and keeps the
// popover open. Renders unconditionally — visibility is the
// consumer's responsibility (include the part to show it).
interface ClearProps extends HTMLButtonAttributes {
children?: Snippet;
}
const { children, onclick, ...rest }: ClearProps = $props();
const uix = getActiveUix();
const provider = DatePickerProvider.require();
function handleClick(event: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) {
provider.clear();
if (typeof onclick === 'function') onclick.call(event.currentTarget, event);
}
</script>
<button
type="button"
data-date-picker-clear
data-action="clear"
aria-label={uix.langs.ts('#?components.date-picker.clear|Clear date')}
onclick={handleClick}
{...rest}
>
{#if children}
{@render children()}
{:else}
{uix.langs.ts('#?components.date-picker.clear|Clear date')}
{/if}
</button>

@ -1,42 +0,0 @@
<script lang="ts">
import { DatePickerProvider } from '$soma/components/date-picker';
import { getActiveUix } from '$active-uix';
import type { Snippet } from 'svelte';
import type { HTMLButtonAttributes } from 'svelte/elements';
// Close / Save action — commits the current value and closes the
// popover. Renders unconditionally — visibility is the consumer's
// responsibility (include the part to show it).
//
// In `mode='modal'` the consumer MUST include this part (or another
// route out) because outside-click and Escape are intentionally
// blocked — otherwise the picker has no exit affordance.
interface CloseProps extends HTMLButtonAttributes {
children?: Snippet;
}
const { children, onclick, ...rest }: CloseProps = $props();
const uix = getActiveUix();
const provider = DatePickerProvider.require();
function handleClick(event: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) {
provider.commit();
if (typeof onclick === 'function') onclick.call(event.currentTarget, event);
}
</script>
<button
type="button"
data-date-picker-close
data-action="close"
aria-label={uix.langs.ts('#?components.date-picker.close|Close')}
onclick={handleClick}
{...rest}
>
{#if children}
{@render children()}
{:else}
{uix.langs.ts('#?components.date-picker.close|Close')}
{/if}
</button>

@ -1,26 +0,0 @@
<script lang="ts">
import { DatePickerProvider } from '$soma/components/date-picker';
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
// Footer container for the picker calendar. Pure composition: render
// whatever you want inside it (Clear / Cancel / Close — or arbitrary
// children). Visibility is the consumer's responsibility — include
// the part to show it, omit it to hide it.
//
// Carries `data-mode` so the CSS can react to the picker's mode
// without re-reading provider context (e.g. enforce a minimum width
// in modal, change the visual emphasis of the close button, etc.).
interface FooterProps extends HTMLAttributes<HTMLDivElement> {
children?: Snippet;
}
const { children, ...rest }: FooterProps = $props();
const provider = DatePickerProvider.require();
const mode = $derived(provider.opts.mode.current);
</script>
<div data-date-picker-footer data-mode={mode} {...rest}>
{@render children?.()}
</div>

@ -185,63 +185,6 @@
border-color: transparent;
}
/* ── Footer (Clear / Cancel / Close action row) ──────────────────────────── */
[data-date-picker-footer] {
display: flex;
align-items: center;
gap: var(--space-2);
padding-block-start: var(--space-3);
margin-block-start: var(--space-3);
border-block-start: var(--border-width) solid var(--color-border-subtle);
}
[data-date-picker-clear],
[data-date-picker-cancel] {
margin-inline-end: auto;
}
[data-date-picker-clear],
[data-date-picker-cancel],
[data-date-picker-close] {
inline-size: max-content;
min-block-size: var(--control-height-sm);
padding-inline: var(--space-3);
border-radius: var(--radius-md);
border: var(--border-width) solid transparent;
background: transparent;
color: var(--color-content-primary);
font: inherit;
font-size: var(--font-size-sm);
line-height: var(--leading-ui);
cursor: pointer;
transition:
background var(--duration-fast) var(--ease-default),
border-color var(--duration-fast) var(--ease-default),
color var(--duration-fast) var(--ease-default);
}
[data-date-picker-clear]:hover,
[data-date-picker-cancel]:hover {
background: var(--color-surface-overlay);
}
[data-date-picker-close] {
border-color: var(--color-primary-border);
background: var(--color-primary-solid);
color: var(--color-content-on-solid);
}
[data-date-picker-close]:hover {
background: var(--color-primary-solid-hover);
}
[data-date-picker-clear]:focus-visible,
[data-date-picker-cancel]:focus-visible,
[data-date-picker-close]:focus-visible {
outline: var(--focus-ring-width) solid var(--color-primary-border);
outline-offset: var(--focus-ring-offset);
}
/* ── Year-view + Month-view (kind='year' / 'month') ──────────────────────── */
@ -333,11 +276,6 @@
[data-date-picker-trigger] {
transition: none;
}
[data-date-picker-clear],
[data-date-picker-cancel],
[data-date-picker-close] {
transition: none;
}
[data-date-picker-year-view-cell],
[data-date-picker-month-view-cell] {
transition: none;

@ -2,10 +2,14 @@ import DatePickerComponent from './date-picker.svelte';
import Trigger from './date-picker-trigger.svelte';
import Content from './date-picker-content.svelte';
import Calendar from './date-picker-calendar.svelte';
import Footer from './date-picker-footer.svelte';
import Clear from './date-picker-clear.svelte';
import Cancel from './date-picker-cancel.svelte';
import Close from './date-picker-close.svelte';
// PickerShell parts — shared by every composite picker. Re-exported
// under the DatePicker namespace so `<DatePicker.Footer/Clear/Cancel/
// Close>` keeps working. The shell parts read `commit/cancel/clear`
// off the picker via `pickerShellContext`.
import Footer from '../picker-shell/picker-shell-footer.svelte';
import Clear from '../picker-shell/picker-shell-clear.svelte';
import Cancel from '../picker-shell/picker-shell-cancel.svelte';
import Close from '../picker-shell/picker-shell-close.svelte';
import YearView from './date-picker-year-view.svelte';
import MonthView from './date-picker-month-view.svelte';
import Label from '../date-field/date-field-label.svelte';

@ -2,7 +2,6 @@ import type {
ArrowProps as SomaDatePickerArrowProps,
CalendarProps as SomaDatePickerCalendarProps,
CalendarSnippetProps as DatePickerCalendarSnippetProps,
CloseProps as SomaDatePickerCloseProps,
InputProps as SomaDatePickerInputProps,
InputSnippetProps as DatePickerInputSnippetProps,
LabelProps as SomaDatePickerLabelProps,
@ -10,6 +9,12 @@ import type {
SegmentProps as SomaDatePickerSegmentProps,
TriggerProps as SomaDatePickerTriggerProps
} from '$soma/components/date-picker';
import type {
FooterProps as PickerShellFooterProps,
ClearProps as PickerShellClearProps,
CancelProps as PickerShellCancelProps,
CloseProps as PickerShellCloseProps
} from '../picker-shell';
import type {
PopoverAnchorProps,
PopoverContentProps,
@ -78,7 +83,12 @@ export type DatePickerHiddenInputProps = Record<string, never>;
export type DatePickerAnchorProps = PopoverAnchorProps;
export type DatePickerOverlayProps = PopoverOverlayProps;
export type DatePickerArrowProps = SomaDatePickerArrowProps;
export type DatePickerCloseProps = SomaDatePickerCloseProps;
// PickerShell action-row props.
export type DatePickerFooterProps = PickerShellFooterProps;
export type DatePickerClearProps = PickerShellClearProps;
export type DatePickerCancelProps = PickerShellCancelProps;
export type DatePickerCloseProps = PickerShellCloseProps;
export type DatePickerHeaderProps = EidosCalendarHeaderProps;
export type DatePickerHeadingProps = EidosCalendarHeadingProps;

@ -1,35 +0,0 @@
<script lang="ts">
import { DateRangePickerProvider } from '$soma/components/date-range-picker';
import { getActiveUix } from '$active-uix';
import type { Snippet } from 'svelte';
import type { HTMLButtonAttributes } from 'svelte/elements';
interface CancelProps extends HTMLButtonAttributes {
children?: Snippet;
}
const { children, onclick, ...rest }: CancelProps = $props();
const uix = getActiveUix();
const provider = DateRangePickerProvider.require();
function handleClick(event: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) {
provider.cancel();
if (typeof onclick === 'function') onclick.call(event.currentTarget, event);
}
</script>
<button
type="button"
data-date-range-picker-cancel
data-action="cancel"
aria-label={uix.langs.ts('#?components.date-range-picker.cancel|Cancel')}
onclick={handleClick}
{...rest}
>
{#if children}
{@render children()}
{:else}
{uix.langs.ts('#?components.date-range-picker.cancel|Cancel')}
{/if}
</button>

@ -1,35 +0,0 @@
<script lang="ts">
import { DateRangePickerProvider } from '$soma/components/date-range-picker';
import { getActiveUix } from '$active-uix';
import type { Snippet } from 'svelte';
import type { HTMLButtonAttributes } from 'svelte/elements';
interface ClearProps extends HTMLButtonAttributes {
children?: Snippet;
}
const { children, onclick, ...rest }: ClearProps = $props();
const uix = getActiveUix();
const provider = DateRangePickerProvider.require();
function handleClick(event: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) {
provider.clear();
if (typeof onclick === 'function') onclick.call(event.currentTarget, event);
}
</script>
<button
type="button"
data-date-range-picker-clear
data-action="clear"
aria-label={uix.langs.ts('#?components.date-range-picker.clear|Clear range')}
onclick={handleClick}
{...rest}
>
{#if children}
{@render children()}
{:else}
{uix.langs.ts('#?components.date-range-picker.clear|Clear range')}
{/if}
</button>

@ -1,35 +0,0 @@
<script lang="ts">
import { DateRangePickerProvider } from '$soma/components/date-range-picker';
import { getActiveUix } from '$active-uix';
import type { Snippet } from 'svelte';
import type { HTMLButtonAttributes } from 'svelte/elements';
interface CloseProps extends HTMLButtonAttributes {
children?: Snippet;
}
const { children, onclick, ...rest }: CloseProps = $props();
const uix = getActiveUix();
const provider = DateRangePickerProvider.require();
function handleClick(event: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) {
provider.commit();
if (typeof onclick === 'function') onclick.call(event.currentTarget, event);
}
</script>
<button
type="button"
data-date-range-picker-close
data-action="close"
aria-label={uix.langs.ts('#?components.date-range-picker.close|Close')}
onclick={handleClick}
{...rest}
>
{#if children}
{@render children()}
{:else}
{uix.langs.ts('#?components.date-range-picker.close|Close')}
{/if}
</button>

@ -1,21 +0,0 @@
<script lang="ts">
import { DateRangePickerProvider } from '$soma/components/date-range-picker';
import type { Snippet } from 'svelte';
import type { HTMLAttributes } from 'svelte/elements';
// Pure composition: render whatever you want inside it (Clear /
// Cancel / Close — or arbitrary children). Visibility is the
// consumer's responsibility — include the part to show it.
interface FooterProps extends HTMLAttributes<HTMLDivElement> {
children?: Snippet;
}
const { children, ...rest }: FooterProps = $props();
const provider = DateRangePickerProvider.require();
const mode = $derived(provider.opts.mode.current);
</script>
<div data-date-range-picker-footer data-mode={mode} {...rest}>
{@render children?.()}
</div>

@ -933,63 +933,6 @@
outline-offset: var(--focus-ring-offset);
}
/* ── Footer (Clear / Cancel / Close action row) ──────────────────────────── */
[data-date-range-picker-footer] {
display: flex;
align-items: center;
gap: var(--space-2);
padding-block-start: var(--space-3);
margin-block-start: var(--space-3);
border-block-start: var(--border-width) solid var(--color-border-subtle);
}
[data-date-range-picker-clear],
[data-date-range-picker-cancel] {
margin-inline-end: auto;
}
[data-date-range-picker-clear],
[data-date-range-picker-cancel],
[data-date-range-picker-close] {
inline-size: max-content;
min-block-size: var(--control-height-sm);
padding-inline: var(--space-3);
border-radius: var(--radius-md);
border: var(--border-width) solid transparent;
background: transparent;
color: var(--color-content-primary);
font: inherit;
font-size: var(--font-size-sm);
line-height: var(--leading-ui);
cursor: pointer;
transition:
background var(--duration-fast) var(--ease-default),
border-color var(--duration-fast) var(--ease-default),
color var(--duration-fast) var(--ease-default);
}
[data-date-range-picker-clear]:hover,
[data-date-range-picker-cancel]:hover {
background: var(--color-surface-overlay);
}
[data-date-range-picker-close] {
border-color: var(--color-primary-border);
background: var(--color-primary-solid);
color: var(--color-content-on-solid);
}
[data-date-range-picker-close]:hover {
background: var(--color-primary-solid-hover);
}
[data-date-range-picker-clear]:focus-visible,
[data-date-range-picker-cancel]:focus-visible,
[data-date-range-picker-close]:focus-visible {
outline: var(--focus-ring-width) solid var(--color-primary-border);
outline-offset: var(--focus-ring-offset);
}
@media (prefers-reduced-motion: reduce) {
[data-date-range-picker-trigger],
@ -999,10 +942,7 @@
[data-range-calendar-next-button],
[data-range-calendar-month-select],
[data-range-calendar-year-select],
[data-range-calendar-day],
[data-date-range-picker-clear],
[data-date-range-picker-cancel],
[data-date-range-picker-close] {
[data-range-calendar-day] {
transition: none;
}
}

@ -8,12 +8,13 @@ import Segment from './date-range-picker-segment.svelte';
import Anchor from '../popover/popover-anchor.svelte';
import Overlay from '../popover/popover-overlay.svelte';
import Arrow from '../popover/popover-arrow.svelte';
import Close from './date-range-picker-close.svelte';
import Footer from './date-range-picker-footer.svelte';
// PickerShell parts — shared by every composite picker.
import Footer from '../picker-shell/picker-shell-footer.svelte';
import Clear from '../picker-shell/picker-shell-clear.svelte';
import Cancel from '../picker-shell/picker-shell-cancel.svelte';
import Close from '../picker-shell/picker-shell-close.svelte';
import YearView from './date-range-picker-year-view.svelte';
import MonthView from './date-range-picker-month-view.svelte';
import Clear from './date-range-picker-clear.svelte';
import Cancel from './date-range-picker-cancel.svelte';
import Header from './date-range-picker-header.svelte';
import Heading from './date-range-picker-heading.svelte';
import PrevButton from './date-range-picker-prev-button.svelte';

@ -2,7 +2,6 @@ import type {
ArrowProps as SomaDateRangePickerArrowProps,
CalendarProps as SomaDateRangePickerCalendarProps,
CalendarSnippetProps as SomaDateRangePickerCalendarSnippetProps,
CloseProps as SomaDateRangePickerCloseProps,
GridBodyProps as SomaDateRangePickerGridBodyProps,
GridHeadProps as SomaDateRangePickerGridHeadProps,
GridProps as SomaDateRangePickerGridProps,
@ -76,7 +75,14 @@ export type DateRangePickerSegmentProps = SomaDateRangePickerSegmentProps;
export type DateRangePickerAnchorProps = PopoverAnchorProps;
export type DateRangePickerOverlayProps = PopoverOverlayProps;
export type DateRangePickerArrowProps = SomaDateRangePickerArrowProps;
export type DateRangePickerCloseProps = SomaDateRangePickerCloseProps;
// PickerShell action-row props — Footer/Clear/Cancel/Close are shared.
export type {
FooterProps as DateRangePickerFooterProps,
ClearProps as DateRangePickerClearProps,
CancelProps as DateRangePickerCancelProps,
CloseProps as DateRangePickerCloseProps
} from '../picker-shell';
export type DateRangePickerHeaderProps = SomaDateRangePickerHeaderProps;
export type DateRangePickerHeadingProps = SomaDateRangePickerHeadingProps;

@ -2,10 +2,11 @@ import TimePickerComponent from './time-picker.svelte'
import Trigger from './time-picker-trigger.svelte'
import Content from './time-picker-content.svelte'
import Clock from './time-picker-clock.svelte'
import Footer from './time-picker-footer.svelte'
import Clear from './time-picker-clear.svelte'
import Cancel from './time-picker-cancel.svelte'
import Close from './time-picker-close.svelte'
// PickerShell parts — shared by every composite picker.
import Footer from '../picker-shell/picker-shell-footer.svelte'
import Clear from '../picker-shell/picker-shell-clear.svelte'
import Cancel from '../picker-shell/picker-shell-cancel.svelte'
import Close from '../picker-shell/picker-shell-close.svelte'
import HourSlider from './time-picker-hour-slider.svelte'
import MinuteSlider from './time-picker-minute-slider.svelte'
import SecondSlider from './time-picker-second-slider.svelte'

@ -1,33 +0,0 @@
<script lang="ts">
import { TimePickerProvider } from '$soma/components/time-picker';
import { getActiveUix } from '$active-uix';
import type { TimePickerCancelProps } from './types';
// Cancel action — reverts to the value snapshot taken on open and
// closes the popover. Renders unconditionally — visibility is the
// consumer's responsibility (include the part to show it).
const { children, onclick, ...rest }: TimePickerCancelProps = $props();
const uix = getActiveUix();
const provider = TimePickerProvider.require();
function handleClick(event: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) {
provider.cancel();
if (typeof onclick === 'function') onclick.call(event.currentTarget, event);
}
</script>
<button
type="button"
data-time-picker-cancel
data-action="cancel"
aria-label={uix.langs.ts('#?components.time-picker.cancel|Cancel')}
onclick={handleClick}
{...rest}
>
{#if children}
{@render children()}
{:else}
{uix.langs.ts('#?components.time-picker.cancel|Cancel')}
{/if}
</button>

@ -1,33 +0,0 @@
<script lang="ts">
import { TimePickerProvider } from '$soma/components/time-picker';
import { getActiveUix } from '$active-uix';
import type { TimePickerClearProps } from './types';
// Clear action — resets the picker's value (undefined) and keeps the
// popover open. Renders unconditionally — visibility is the
// consumer's responsibility (include the part to show it).
const { children, onclick, ...rest }: TimePickerClearProps = $props();
const uix = getActiveUix();
const provider = TimePickerProvider.require();
function handleClick(event: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) {
provider.clear();
if (typeof onclick === 'function') onclick.call(event.currentTarget, event);
}
</script>
<button
type="button"
data-time-picker-clear
data-action="clear"
aria-label={uix.langs.ts('#?components.time-picker.clear|Clear time')}
onclick={handleClick}
{...rest}
>
{#if children}
{@render children()}
{:else}
{uix.langs.ts('#?components.time-picker.clear|Clear time')}
{/if}
</button>

@ -1,37 +0,0 @@
<script lang="ts">
import { TimePickerProvider } from '$soma/components/time-picker';
import { getActiveUix } from '$active-uix';
import type { TimePickerCloseProps } from './types';
// Close / Save action — commits the current value and closes the
// popover. Renders unconditionally — visibility is the consumer's
// responsibility (include the part to show it).
//
// In `mode='modal'` the consumer MUST include this part (or another
// route out) because outside-click and Escape are intentionally
// blocked — otherwise the picker has no exit affordance.
const { children, onclick, ...rest }: TimePickerCloseProps = $props();
const uix = getActiveUix();
const provider = TimePickerProvider.require();
function handleClick(event: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) {
provider.commit();
if (typeof onclick === 'function') onclick.call(event.currentTarget, event);
}
</script>
<button
type="button"
data-time-picker-close
data-action="close"
aria-label={uix.langs.ts('#?components.time-picker.close|Close')}
onclick={handleClick}
{...rest}
>
{#if children}
{@render children()}
{:else}
{uix.langs.ts('#?components.time-picker.close|Close')}
{/if}
</button>

@ -1,20 +0,0 @@
<script lang="ts">
import { TimePickerProvider } from '$soma/components/time-picker';
import type { TimePickerFooterProps } from './types';
// Footer container for the picker clock. Pure composition: render
// whatever you want inside it (Clear / Cancel / Close — or arbitrary
// children). Visibility is the consumer's responsibility — include
// the part to show it, omit it to hide it.
//
// Carries `data-mode` so the CSS can react to the picker's mode
// without re-reading provider context.
const { children, ...rest }: TimePickerFooterProps = $props();
const provider = TimePickerProvider.require();
const mode = $derived(provider.opts.mode.current);
</script>
<div data-time-picker-footer data-mode={mode} {...rest}>
{@render children?.()}
</div>

@ -246,69 +246,9 @@
background: var(--_time-field-accent-border);
}
/* ── Footer (Clear / Cancel / Close action row) ──────────────────────────── */
[data-time-picker-footer] {
display: flex;
align-items: center;
gap: var(--space-2);
padding-block-start: var(--space-3);
margin-block-start: var(--space-3);
border-block-start: var(--border-width) solid var(--color-border-subtle);
}
[data-time-picker-clear],
[data-time-picker-cancel] {
margin-inline-end: auto;
}
[data-time-picker-clear],
[data-time-picker-cancel],
[data-time-picker-close] {
inline-size: max-content;
min-block-size: var(--control-height-sm);
padding-inline: var(--space-3);
border-radius: var(--radius-md);
border: var(--border-width) solid transparent;
background: transparent;
color: var(--color-content-primary);
font: inherit;
font-size: var(--font-size-sm);
line-height: var(--leading-ui);
cursor: pointer;
transition:
background var(--duration-fast) var(--ease-default),
border-color var(--duration-fast) var(--ease-default),
color var(--duration-fast) var(--ease-default);
}
[data-time-picker-clear]:hover,
[data-time-picker-cancel]:hover {
background: var(--color-surface-overlay);
}
[data-time-picker-close] {
border-color: var(--color-primary-border);
background: var(--color-primary-solid);
color: var(--color-content-on-solid);
}
[data-time-picker-close]:hover {
background: var(--color-primary-solid-hover);
}
[data-time-picker-clear]:focus-visible,
[data-time-picker-cancel]:focus-visible,
[data-time-picker-close]:focus-visible {
outline: var(--focus-ring-width) solid var(--color-primary-border);
outline-offset: var(--focus-ring-offset);
}
@media (prefers-reduced-motion: reduce) {
[data-time-picker-trigger],
[data-time-picker-clear],
[data-time-picker-cancel],
[data-time-picker-close] {
[data-time-picker-trigger] {
transition: none;
}
}

@ -20,7 +20,6 @@ import type {
} from '../popover/types'
import type { ColorRole, ControlVariant, ResponsiveProp, Size } from '$uix/eidos/lib/types'
import type { Snippet } from 'svelte'
import type { HTMLButtonAttributes } from 'svelte/elements'
export type TimePickerSize = Extract<Size, 'sm' | 'md' | 'lg'>
export type TimePickerVariant = ControlVariant
@ -60,24 +59,13 @@ export type TimePickerClockProps = {
children?: Snippet
}
export type TimePickerFooterProps = {
id?: string
class?: string
style?: string
children?: Snippet
}
export type TimePickerClearProps = HTMLButtonAttributes & {
children?: Snippet
}
export type TimePickerCancelProps = HTMLButtonAttributes & {
children?: Snippet
}
export type TimePickerCloseProps = HTMLButtonAttributes & {
children?: Snippet
}
// PickerShell action-row props — Footer/Clear/Cancel/Close are shared.
export type {
FooterProps as TimePickerFooterProps,
ClearProps as TimePickerClearProps,
CancelProps as TimePickerCancelProps,
CloseProps as TimePickerCloseProps
} from '../picker-shell'
export type TimePickerLabelProps = SomaTimePickerLabelProps
export type TimePickerInputProps = SomaTimePickerInputProps

@ -231,78 +231,11 @@ export const datePickerMorfo = {
{ key: 'Tab', action: 'focus-next' },
{ key: 'Shift+Tab', action: 'focus-prev' }
]
},
{
// Optional footer rendered inside the Calendar. Visibility is owned
// by the consumer (`clearButton`/`cancelButton`/`closeButton` props
// on the root). In `mode='modal'` the Close affordance is forced on.
name: 'Footer',
kebab: 'footer',
archetype: 'footer',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [{ attr: 'data-mode', values: ['inline', 'modal'], severity: 'optional' }],
aria: []
},
{
// Clear action — resets value to undefined, keeps the popover open.
name: 'Clear',
kebab: 'clear',
archetype: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-action', value: v.literal('clear') }],
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.date-picker.clear|Clear date'),
severity: 'recommended'
}
]
},
{
// Cancel action — reverts to the value snapshot taken on open, closes.
name: 'Cancel',
kebab: 'cancel',
archetype: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-action', value: v.literal('cancel') }],
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.date-picker.cancel|Cancel'),
severity: 'recommended'
}
]
},
{
// Close / Save action — commits current value and closes. In modal
// mode this is the only affordance to exit (outside-click + Escape
// are intentionally blocked).
name: 'Close',
kebab: 'close',
archetype: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-action', value: v.literal('close') }],
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.date-picker.close|Close'),
severity: 'recommended'
}
]
}
// Footer / Clear / Cancel / Close — declared by the shared
// `picker-shell` (Eidos `<PickerShell.Footer/Clear/Cancel/Close>`).
// Their data-attrs are `[data-picker-footer/clear/cancel/close]`
// (no per-picker prefix). The audit / runtime won't find them in
// this morfo because they're chrome shared across pickers.
]
} as const satisfies Morfo;

@ -237,78 +237,9 @@ export const dateRangePickerMorfo = {
{ key: 'Tab', action: 'focus-next' },
{ key: 'Shift+Tab', action: 'focus-prev' }
]
},
{
// Optional footer rendered inside the Calendar. Visibility is owned
// by the consumer (`clearButton`/`cancelButton`/`closeButton` props
// on the root). In `mode='modal'` the Close affordance is forced on.
name: 'Footer',
kebab: 'footer',
archetype: 'footer',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [{ attr: 'data-mode', values: ['inline', 'modal'], severity: 'optional' }],
aria: []
},
{
// Clear action — resets value to an empty range, keeps the popover open.
name: 'Clear',
kebab: 'clear',
archetype: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-action', value: v.literal('clear') }],
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.date-range-picker.clear|Clear range'),
severity: 'recommended'
}
]
},
{
// Cancel action — reverts to the value snapshot taken on open, closes.
name: 'Cancel',
kebab: 'cancel',
archetype: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-action', value: v.literal('cancel') }],
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.date-range-picker.cancel|Cancel'),
severity: 'recommended'
}
]
},
{
// Close / Save action — commits current value and closes. In modal
// mode this is the only affordance to exit (outside-click + Escape
// are intentionally blocked).
name: 'Close',
kebab: 'close',
archetype: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-action', value: v.literal('close') }],
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.date-range-picker.close|Close'),
severity: 'recommended'
}
]
}
// Footer / Clear / Cancel / Close — chrome shared via picker-shell;
// not declared here. Eidos `<DateRangePicker.Footer/Clear/Cancel/
// Close>` re-exports the `<PickerShell.…>` parts.
]
} as const satisfies Morfo;

@ -308,70 +308,9 @@ export const timePickerMorfo = {
optional: true,
data: [{ attr: 'data-checked', severity: 'optional' }],
aria: []
},
{
name: 'Footer',
kebab: 'footer',
archetype: 'footer',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [{ attr: 'data-mode', values: ['inline', 'modal'], severity: 'optional' }],
aria: []
},
{
name: 'Clear',
kebab: 'clear',
archetype: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-action', value: v.literal('clear') }],
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.time-picker.clear|Clear time'),
severity: 'recommended'
}
]
},
{
name: 'Cancel',
kebab: 'cancel',
archetype: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-action', value: v.literal('cancel') }],
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.time-picker.cancel|Cancel'),
severity: 'recommended'
}
]
},
{
name: 'Close',
kebab: 'close',
archetype: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-action', value: v.literal('close') }],
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.time-picker.close|Close'),
severity: 'recommended'
}
]
}
// Footer / Clear / Cancel / Close — chrome shared via picker-shell;
// not declared here. Eidos `<TimePicker.Footer/Clear/Cancel/Close>`
// re-exports the `<PickerShell.…>` parts.
]
} as const satisfies Morfo;

@ -7,6 +7,10 @@ import { type ActiveProps, type StateProps } from '$libs/reactive';
import type { OnChangeFn, Direction } from '../../types';
import { Soma } from '../../core/soma.svelte';
import { isBefore } from '$libs/days';
import {
pickerShellContext,
type PickerShellHandle
} from '../picker-shell';
import type {
DateValue,
DateMatcher,
@ -160,8 +164,19 @@ export class DatePickerProvider {
}
}
);
// Expose our commit/cancel/clear/mode handle to descendant Eidos
// PickerShell parts (`<DatePicker.Footer>`, `<DatePicker.Clear>`, etc.).
pickerShellContext.set(this.pickerShellHandle);
}
readonly pickerShellHandle: PickerShellHandle = {
getMode: () => this.opts.mode.current,
commit: () => this.commit(),
cancel: () => this.cancel(),
clear: () => this.clear()
};
readonly validationStatus = $derived.by(() => {
const value = this.opts.value.current;
if (!value) return false as const;

@ -7,6 +7,10 @@ import { type ActiveProps, type StateProps } from '$libs/reactive';
import type { OnChangeFn, Direction } from '../../types';
import { Soma } from '../../core/soma.svelte';
import { getLocalTimeZone, isBefore } from '$libs/days';
import {
pickerShellContext,
type PickerShellHandle
} from '../picker-shell';
/**
* Picker interaction mode. `inline` (default) auto-closes the popover when
@ -150,8 +154,19 @@ export class DateRangePickerProvider {
}
}
);
// Expose our commit/cancel/clear/mode handle to descendant Eidos
// PickerShell parts (`<DateRangePicker.Footer>`, etc.).
pickerShellContext.set(this.pickerShellHandle);
}
readonly pickerShellHandle: PickerShellHandle = {
getMode: () => this.opts.mode.current,
commit: () => this.commit(),
cancel: () => this.cancel(),
clear: () => this.clear()
};
readonly validationStatus = $derived.by(() => {
const value = this.opts.value.current;
const start = value.start;

@ -17,6 +17,10 @@ import type {
import { timePickerMorfo } from '../../../morfo/components/time-picker';
import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte';
import {
pickerShellContext,
type PickerShellHandle
} from '../picker-shell';
const attrs = createAttrs(timePickerMorfo);
@ -109,8 +113,19 @@ export class TimePickerProvider {
}
}
);
// Expose our commit/cancel/clear/mode handle to descendant Eidos
// PickerShell parts (`<TimePicker.Footer>`, `<TimePicker.Clear>`, etc.).
pickerShellContext.set(this.pickerShellHandle);
}
readonly pickerShellHandle: PickerShellHandle = {
getMode: () => this.opts.mode.current,
commit: () => this.commit(),
cancel: () => this.cancel(),
clear: () => this.clear()
};
/** Resolved hour cycle — same rule as TimeField: explicit → service → locale. */
readonly resolvedHourCycle: HourCycle = $derived.by(() => {
const explicit = this.opts.hourCycle.current;

Loading…
Cancel
Save

Powered by TurnKey Linux.