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; 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') ──────────────────────── */ /* ── Year-view + Month-view (kind='year' / 'month') ──────────────────────── */
@ -333,11 +276,6 @@
[data-date-picker-trigger] { [data-date-picker-trigger] {
transition: none; 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-year-view-cell],
[data-date-picker-month-view-cell] { [data-date-picker-month-view-cell] {
transition: none; transition: none;

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

@ -2,7 +2,6 @@ import type {
ArrowProps as SomaDatePickerArrowProps, ArrowProps as SomaDatePickerArrowProps,
CalendarProps as SomaDatePickerCalendarProps, CalendarProps as SomaDatePickerCalendarProps,
CalendarSnippetProps as DatePickerCalendarSnippetProps, CalendarSnippetProps as DatePickerCalendarSnippetProps,
CloseProps as SomaDatePickerCloseProps,
InputProps as SomaDatePickerInputProps, InputProps as SomaDatePickerInputProps,
InputSnippetProps as DatePickerInputSnippetProps, InputSnippetProps as DatePickerInputSnippetProps,
LabelProps as SomaDatePickerLabelProps, LabelProps as SomaDatePickerLabelProps,
@ -10,6 +9,12 @@ import type {
SegmentProps as SomaDatePickerSegmentProps, SegmentProps as SomaDatePickerSegmentProps,
TriggerProps as SomaDatePickerTriggerProps TriggerProps as SomaDatePickerTriggerProps
} from '$soma/components/date-picker'; } from '$soma/components/date-picker';
import type {
FooterProps as PickerShellFooterProps,
ClearProps as PickerShellClearProps,
CancelProps as PickerShellCancelProps,
CloseProps as PickerShellCloseProps
} from '../picker-shell';
import type { import type {
PopoverAnchorProps, PopoverAnchorProps,
PopoverContentProps, PopoverContentProps,
@ -78,7 +83,12 @@ export type DatePickerHiddenInputProps = Record<string, never>;
export type DatePickerAnchorProps = PopoverAnchorProps; export type DatePickerAnchorProps = PopoverAnchorProps;
export type DatePickerOverlayProps = PopoverOverlayProps; export type DatePickerOverlayProps = PopoverOverlayProps;
export type DatePickerArrowProps = SomaDatePickerArrowProps; 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 DatePickerHeaderProps = EidosCalendarHeaderProps;
export type DatePickerHeadingProps = EidosCalendarHeadingProps; 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); 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) { @media (prefers-reduced-motion: reduce) {
[data-date-range-picker-trigger], [data-date-range-picker-trigger],
@ -999,10 +942,7 @@
[data-range-calendar-next-button], [data-range-calendar-next-button],
[data-range-calendar-month-select], [data-range-calendar-month-select],
[data-range-calendar-year-select], [data-range-calendar-year-select],
[data-range-calendar-day], [data-range-calendar-day] {
[data-date-range-picker-clear],
[data-date-range-picker-cancel],
[data-date-range-picker-close] {
transition: none; transition: none;
} }
} }

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

@ -2,7 +2,6 @@ import type {
ArrowProps as SomaDateRangePickerArrowProps, ArrowProps as SomaDateRangePickerArrowProps,
CalendarProps as SomaDateRangePickerCalendarProps, CalendarProps as SomaDateRangePickerCalendarProps,
CalendarSnippetProps as SomaDateRangePickerCalendarSnippetProps, CalendarSnippetProps as SomaDateRangePickerCalendarSnippetProps,
CloseProps as SomaDateRangePickerCloseProps,
GridBodyProps as SomaDateRangePickerGridBodyProps, GridBodyProps as SomaDateRangePickerGridBodyProps,
GridHeadProps as SomaDateRangePickerGridHeadProps, GridHeadProps as SomaDateRangePickerGridHeadProps,
GridProps as SomaDateRangePickerGridProps, GridProps as SomaDateRangePickerGridProps,
@ -76,7 +75,14 @@ export type DateRangePickerSegmentProps = SomaDateRangePickerSegmentProps;
export type DateRangePickerAnchorProps = PopoverAnchorProps; export type DateRangePickerAnchorProps = PopoverAnchorProps;
export type DateRangePickerOverlayProps = PopoverOverlayProps; export type DateRangePickerOverlayProps = PopoverOverlayProps;
export type DateRangePickerArrowProps = SomaDateRangePickerArrowProps; 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 DateRangePickerHeaderProps = SomaDateRangePickerHeaderProps;
export type DateRangePickerHeadingProps = SomaDateRangePickerHeadingProps; export type DateRangePickerHeadingProps = SomaDateRangePickerHeadingProps;

@ -2,10 +2,11 @@ import TimePickerComponent from './time-picker.svelte'
import Trigger from './time-picker-trigger.svelte' import Trigger from './time-picker-trigger.svelte'
import Content from './time-picker-content.svelte' import Content from './time-picker-content.svelte'
import Clock from './time-picker-clock.svelte' import Clock from './time-picker-clock.svelte'
import Footer from './time-picker-footer.svelte' // PickerShell parts — shared by every composite picker.
import Clear from './time-picker-clear.svelte' import Footer from '../picker-shell/picker-shell-footer.svelte'
import Cancel from './time-picker-cancel.svelte' import Clear from '../picker-shell/picker-shell-clear.svelte'
import Close from './time-picker-close.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 HourSlider from './time-picker-hour-slider.svelte'
import MinuteSlider from './time-picker-minute-slider.svelte' import MinuteSlider from './time-picker-minute-slider.svelte'
import SecondSlider from './time-picker-second-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); 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) { @media (prefers-reduced-motion: reduce) {
[data-time-picker-trigger], [data-time-picker-trigger] {
[data-time-picker-clear],
[data-time-picker-cancel],
[data-time-picker-close] {
transition: none; transition: none;
} }
} }

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

@ -231,78 +231,11 @@ export const datePickerMorfo = {
{ key: 'Tab', action: 'focus-next' }, { key: 'Tab', action: 'focus-next' },
{ key: 'Shift+Tab', action: 'focus-prev' } { 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; } as const satisfies Morfo;

@ -237,78 +237,9 @@ export const dateRangePickerMorfo = {
{ key: 'Tab', action: 'focus-next' }, { key: 'Tab', action: 'focus-next' },
{ key: 'Shift+Tab', action: 'focus-prev' } { 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; } as const satisfies Morfo;

@ -308,70 +308,9 @@ export const timePickerMorfo = {
optional: true, optional: true,
data: [{ attr: 'data-checked', severity: 'optional' }], data: [{ attr: 'data-checked', severity: 'optional' }],
aria: [] 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; } as const satisfies Morfo;

@ -7,6 +7,10 @@ import { type ActiveProps, type StateProps } from '$libs/reactive';
import type { OnChangeFn, Direction } from '../../types'; import type { OnChangeFn, Direction } from '../../types';
import { Soma } from '../../core/soma.svelte'; import { Soma } from '../../core/soma.svelte';
import { isBefore } from '$libs/days'; import { isBefore } from '$libs/days';
import {
pickerShellContext,
type PickerShellHandle
} from '../picker-shell';
import type { import type {
DateValue, DateValue,
DateMatcher, 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(() => { readonly validationStatus = $derived.by(() => {
const value = this.opts.value.current; const value = this.opts.value.current;
if (!value) return false as const; 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 type { OnChangeFn, Direction } from '../../types';
import { Soma } from '../../core/soma.svelte'; import { Soma } from '../../core/soma.svelte';
import { getLocalTimeZone, isBefore } from '$libs/days'; import { getLocalTimeZone, isBefore } from '$libs/days';
import {
pickerShellContext,
type PickerShellHandle
} from '../picker-shell';
/** /**
* Picker interaction mode. `inline` (default) auto-closes the popover when * 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(() => { readonly validationStatus = $derived.by(() => {
const value = this.opts.value.current; const value = this.opts.value.current;
const start = value.start; const start = value.start;

@ -17,6 +17,10 @@ import type {
import { timePickerMorfo } from '../../../morfo/components/time-picker'; import { timePickerMorfo } from '../../../morfo/components/time-picker';
import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte'; import type { SomaRuntime, SomaRuntimePart } from '../../runtime.svelte';
import {
pickerShellContext,
type PickerShellHandle
} from '../picker-shell';
const attrs = createAttrs(timePickerMorfo); 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. */ /** Resolved hour cycle — same rule as TimeField: explicit → service → locale. */
readonly resolvedHourCycle: HourCycle = $derived.by(() => { readonly resolvedHourCycle: HourCycle = $derived.by(() => {
const explicit = this.opts.hourCycle.current; const explicit = this.opts.hourCycle.current;

Loading…
Cancel
Save

Powered by TurnKey Linux.