fix(eidos): picker triggers flush via field-trigger archetype + footer follows picker size

Two picker-coherence fixes:

1. Field-trigger flush treatment — popover.css excluded the picker's generic
   button envelope only for `[data-field-control-trigger]`. The time-picker /
   time-range-picker clock icons carry `data-{component}-trigger` (NOT the
   field-control marker), so they still got the popover button border/bg —
   looking like a bordered button instead of a flush field icon. The exclusion
   now keys off the `field-trigger` archetype, which the morfo/soma stamp on
   EVERY picker trigger; added the missing override to time-range-picker.
   All five pickers verified: date / time / time-range / date-range render a
   flush 28px icon; color renders its own field-box (its border is the field's,
   not the popover envelope's).

2. Footer follows picker size — picker-shell Clear/Cancel/Close were pinned to
   `size="xs"`, so md/lg pickers had a tiny incoherent footer. New shared
   `picker-shell/visual-context` lets each picker publish its resolved size;
   the footer buttons read it (fallback xs). Wired into all five pickers.
   Verified: at picker md the footer buttons are md (36px).

svelte-check clean. Excludes words/palabras/chronos.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
active-uix
dev 4 months ago
parent f822d6e4dd
commit f20efd4681

@ -8,6 +8,7 @@
import { ActiveEidos } from '$uix/eidos';
import * as ColorPicker from '$soma/components/color-picker';
import { setColorPickerVisualContext } from './context.svelte';
import { setPickerShellVisual } from '$uix/eidos/components/picker-shell/visual-context';
import type { ColorPickerProps } from './types';
let {
@ -30,6 +31,8 @@
getVariant: () => variant,
getColor: () => color
});
// Footer controls follow the picker size.
setPickerShellVisual({ getSize: () => resolvedSize });
</script>
<ColorPicker.Provider

@ -10,6 +10,7 @@
import { ActiveEidos } from '$uix/eidos';
import * as DatePicker from '$soma/components/date-picker';
import { setDatePickerVisualContext } from './context.svelte';
import { setPickerShellVisual } from '$uix/eidos/components/picker-shell/visual-context';
import type { DatePickerProps } from './types';
let {
@ -31,6 +32,8 @@
getVariant: () => variant,
getColor: () => color
});
// Footer controls (Clear / Cancel / Close) follow the picker size.
setPickerShellVisual({ getSize: () => resolvedSize });
</script>
<DatePicker.Provider

@ -9,6 +9,7 @@
import { ActiveEidos } from '$uix/eidos';
import * as DateRangePicker from '$soma/components/date-range-picker';
import { setDateRangePickerVisualContext } from './context.svelte';
import { setPickerShellVisual } from '$uix/eidos/components/picker-shell/visual-context';
import type { DateRangePickerProps } from './types';
let {
@ -30,6 +31,8 @@
getVariant: () => variant,
getColor: () => color
});
// Footer controls follow the picker size.
setPickerShellVisual({ getSize: () => resolvedSize });
</script>
<DateRangePicker.Provider

@ -2,16 +2,18 @@
import { getPickerShellHandle } from '$soma/components/picker-shell';
import { ActiveEidos } from '$uix/eidos';
import { Button } from '$uix/eidos/components/button';
import { getPickerShellVisual } from './visual-context';
import type { PickerShellCancelProps } from './types';
// Cancel action — reverts to the value snapshot taken on the OPEN edge
// and closes the popover. Visual: ghost neutral `<Button>` at
// size='xs' so the safe-escape action stays visually subordinate to
// the solid primary Close.
// and closes the popover. Visual: ghost neutral `<Button>` whose size
// follows the picker (via the shell visual context) so the footer stays
// coherent with the calendar/clock at any picker size.
const { children, onclick, ...rest }: PickerShellCancelProps = $props();
const eidos = ActiveEidos.require();
const shell = getPickerShellHandle();
const visual = getPickerShellVisual();
const label = eidos.langs.ts('#?components.picker-shell.cancel|Cancel');
function handleClick(event: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) {
@ -23,7 +25,7 @@
<Button
variant="ghost"
color="neutral"
size="xs"
size={visual?.getSize() ?? 'xs'}
data-picker-cancel=""
data-action="cancel"
aria-label={label}

@ -2,14 +2,17 @@
import { getPickerShellHandle } from '$soma/components/picker-shell';
import { ActiveEidos } from '$uix/eidos';
import { Button } from '$uix/eidos/components/button';
import { getPickerShellVisual } from './visual-context';
import type { PickerShellClearProps } from './types';
// Clear action — resets the host picker's value to undefined and keeps
// the popover open. Visual: ghost neutral `<Button>` at size='xs'.
// the popover open. Visual: ghost neutral `<Button>` whose size follows
// the picker (shell visual context).
const { children, onclick, ...rest }: PickerShellClearProps = $props();
const eidos = ActiveEidos.require();
const shell = getPickerShellHandle();
const visual = getPickerShellVisual();
const label = eidos.langs.ts('#?components.picker-shell.clear|Clear');
function handleClick(event: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) {
@ -21,7 +24,7 @@
<Button
variant="ghost"
color="neutral"
size="xs"
size={visual?.getSize() ?? 'xs'}
data-picker-clear=""
data-action="clear"
aria-label={label}

@ -2,22 +2,23 @@
import { getPickerShellHandle } from '$soma/components/picker-shell';
import { ActiveEidos } from '$uix/eidos';
import { Button } from '$uix/eidos/components/button';
import { getPickerShellVisual } from './visual-context';
import type { PickerShellCloseProps } from './types';
// Close / Save action — commits the current value and closes the
// popover. In `mode='modal'` the consumer MUST include this part
// because outside-click and Escape are blocked.
//
// Visual: solid primary `<Button>` at size='xs'. The xs default
// reflects the picker design constraint — footer actions must NOT
// visually compete with the picker body (calendar / clock / color
// area). The `close-commit` morfo event still fires with
// Visual: solid primary `<Button>` whose size follows the picker (via the
// shell visual context) so the footer stays coherent with the picker body
// at any size. The `close-commit` morfo event still fires with
// intent='fulfill' (hardcoded in the picker morfo) so the perceptual
// closure sounds like a save regardless of the visual palette.
const { children, onclick, ...rest }: PickerShellCloseProps = $props();
const eidos = ActiveEidos.require();
const shell = getPickerShellHandle();
const visual = getPickerShellVisual();
const label = eidos.langs.ts('#?components.picker-shell.close|Done');
function handleClick(event: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) {
@ -29,7 +30,7 @@
<Button
variant="solid"
color="primary"
size="xs"
size={visual?.getSize() ?? 'xs'}
data-picker-close=""
data-action="close"
aria-label={label}

@ -0,0 +1,26 @@
import { getContext, setContext } from 'svelte';
import type { ButtonSize } from '$uix/eidos/components/button';
/**
* Shared visual context for the picker-shell footer controls.
*
* The footer parts (`Clear` / `Cancel` / `Close` / `Commit`) are reused by every
* picker (date / time / color / *-range). Each picker's eidos root publishes its
* resolved size here so the footer buttons scale WITH the picker instead of being
* pinned to a fixed size — keeping the whole popover coherent at any size.
*/
const PICKER_SHELL_VISUAL = Symbol('picker-shell-visual');
export interface PickerShellVisual {
/** The active picker's resolved size. Footer controls follow it. */
getSize(): ButtonSize;
}
export function setPickerShellVisual(ctx: PickerShellVisual): PickerShellVisual {
setContext(PICKER_SHELL_VISUAL, ctx);
return ctx;
}
export function getPickerShellVisual(): PickerShellVisual | undefined {
return getContext<PickerShellVisual | undefined>(PICKER_SHELL_VISUAL);
}

@ -3,14 +3,16 @@
* baseline button envelope here. Consumes Capa 2 directly (same as Dialog
* trigger). Consumers can override per-instance via inline tokens.
*
* `:not([data-field-control-trigger])` — a field's icon affordance (date/time/
* color picker calendar/clock/swatch button) opens its overlay via a popover, so
* it ALSO carries `data-popover-trigger`, but it owns a flush, border-less
* treatment in `field-control-trigger.css`. This generic button envelope must
* NOT win over it (the two are equal-specificity, so it would otherwise depend
* on CSS load order).
* `:not([data-archetype='field-trigger'])` — a picker trigger (date/time/color
* calendar/clock/swatch) opens its overlay via a popover, so it ALSO carries
* `data-popover-trigger`, but it is NOT a generic button: the morfo/soma tag it
* with the `field-trigger` archetype and it owns its own field/icon treatment.
* Keying off the archetype (not the `data-field-control-trigger` marker) covers
* BOTH the inline field-icon triggers AND the standalone picker triggers (e.g.
* the time-picker clock button) that lack that marker. This generic envelope must
* NOT win over them (equal specificity → would otherwise depend on load order).
*/
[data-popover-trigger]:not([data-field-control-trigger]) {
[data-popover-trigger]:not([data-archetype='field-trigger']) {
display: inline-flex;
align-items: center;
justify-content: center;
@ -28,17 +30,17 @@
border-color var(--duration-fast) var(--ease-default);
}
[data-popover-trigger]:not([data-field-control-trigger]):hover:not([disabled]):not([data-disabled]) {
[data-popover-trigger]:not([data-archetype='field-trigger']):hover:not([disabled]):not([data-disabled]) {
background: var(--color-surface-overlay);
border-color: var(--color-border-strong);
}
[data-popover-trigger]:not([data-field-control-trigger])[data-state='open'] {
[data-popover-trigger]:not([data-archetype='field-trigger'])[data-state='open'] {
background: var(--color-surface-overlay);
border-color: var(--color-border-strong);
}
[data-popover-trigger]:not([data-field-control-trigger]):focus-visible {
[data-popover-trigger]:not([data-archetype='field-trigger']):focus-visible {
outline: var(--focus-ring-width) solid var(--focus-ring-color);
outline-offset: var(--focus-ring-offset);
}

@ -8,6 +8,7 @@
import { ActiveEidos } from '$uix/eidos';
import * as TimePicker from '$soma/components/time-picker';
import { setTimePickerVisualContext } from './context.svelte';
import { setPickerShellVisual } from '$uix/eidos/components/picker-shell/visual-context';
import type { TimePickerProps } from './types';
let {
@ -31,6 +32,8 @@
getColor: () => color,
getRuler: () => ruler
});
// Footer controls follow the picker size.
setPickerShellVisual({ getSize: () => resolvedSize });
</script>
<TimePicker.Provider

@ -9,6 +9,7 @@
import { ActiveEidos } from '$uix/eidos';
import * as TimeRangePicker from '$soma/components/time-range-picker';
import { setTimeRangePickerVisualContext } from './context.svelte';
import { setPickerShellVisual } from '$uix/eidos/components/picker-shell/visual-context';
import type { TimeRangePickerProps } from './types';
let {
@ -32,6 +33,8 @@
getColor: () => color,
getRuler: () => ruler
});
// Footer controls follow the picker size.
setPickerShellVisual({ getSize: () => resolvedSize });
</script>
<TimeRangePicker.Provider

@ -45,6 +45,8 @@
const mergedProps = $derived(
mergeProps(restProps, state.props, {
[timeRangePickerAttrs.trigger]: '',
// Field overlay trigger composed on a popover trigger — not a button.
'data-archetype': 'field-trigger',
'aria-label': resolvedLabel,
disabled: provider.opts.disabled.current || undefined
} as Record<string, unknown>)

Loading…
Cancel
Save

Powered by TurnKey Linux.