From 28bf7f8b497b23270fa7ec3b8c4dc3b11cb316dd Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 26 May 2026 22:44:11 +0200 Subject: [PATCH] feat(pickers): wire runtime.trigger('close') in 5 picker providers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the 2026-05-27 picker family polymorphic refactor, the morfos declared the polymorphic `close` event but the providers were inert — they just toggled `opts.open = false` without firing the event. This commit cables them, mirroring the Dialog/Drawer/Popover dismissWith pattern (book §5.3). **Affected providers** (5): color-picker, date-picker, date-range-picker, time-picker, time-range-picker. **Pattern** (consistent across all 5): - Private `triggerClose(cause)` helper maps: 'committed' / 'range-committed' → commit.save + fulfill 'cancelled' → emerge.close 'dismissed' → emerge.dismiss 'dismissed-outside' → emerge.dismiss - Writes `data-last-action` on the morfo's target part (calendar / clock / content) imperatively before the trigger fires so eidos can tint the exit animation per cause. - Called from commit() / cancel() / handleSelect / handleDateSelect / handleRangeSelect — the close paths the provider already had. The morfo target varies per picker: - color-picker → content - date-picker → calendar - date-range-picker → calendar - time-picker → clock - time-range-picker → clock Internal state (`opts.open = false`) sequence preserved — the trigger fires BEFORE the open flip so the perceptual signal stamps `data-event-*` on a node that is still rendered. **dismissed / dismissed-outside causes** are declared but not yet wired to specific gestures — the picker shell delegates escape / outside-click handling to its wrapping Popover, whose own polymorphic close fires independently. The cause strings remain in the helper for future use (e.g. if a picker grows its own escape handling). **Result**: - Pickers now emit perceptual signal on commit/cancel via sema engine. - Eidos `[data-last-action='X']` selectors get exit-tint values to act on (mirror of Dialog/Drawer/Popover). - Picker tests: 18/18 still pass. `morfo:check`: 0 issues. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../color-picker-provider.svelte.ts | 27 +++++++++++++++ .../date-picker-provider.svelte.ts | 33 +++++++++++++++++++ .../date-range-picker-provider.svelte.ts | 28 ++++++++++++++++ .../time-picker-provider.svelte.ts | 26 +++++++++++++++ .../time-range-picker-provider.svelte.ts | 27 +++++++++++++++ 5 files changed, 141 insertions(+) diff --git a/src/uix/soma/components/color-picker/color-picker-provider.svelte.ts b/src/uix/soma/components/color-picker/color-picker-provider.svelte.ts index f686fbfb0..ee3db9eff 100644 --- a/src/uix/soma/components/color-picker/color-picker-provider.svelte.ts +++ b/src/uix/soma/components/color-picker/color-picker-provider.svelte.ts @@ -300,6 +300,7 @@ export class ColorPickerProvider { /** Close the popover when `closeOnSelect` is enabled. */ handleSelect(): void { if (this.opts.closeOnSelect.current && !this.opts.inline.current) { + this.triggerClose('committed'); this.opts.open.current = false; } } @@ -317,6 +318,7 @@ export class ColorPickerProvider { * the canonical picker footer. */ commit(): void { + this.triggerClose('committed'); this.opts.open.current = false; this.commitChange(); } @@ -327,6 +329,7 @@ export class ColorPickerProvider { */ cancel(): void { this.opts.value.current = this.valueOnOpen; + this.triggerClose('cancelled'); this.opts.open.current = false; } @@ -338,6 +341,30 @@ export class ColorPickerProvider { this.opts.value.current = undefined; } + /** + * Polymorphic close (book §5.3) — fire the morfo `close` event with + * the per-cause semantic + write `data-last-action` on Content so + * eidos can tint the exit animation. Mirrors the date-picker pattern. + */ + private triggerClose( + cause: 'committed' | 'cancelled' | 'dismissed' | 'dismissed-outside' + ): void { + const CAUSES = { + committed: { semantic: { family: 'commit' as const, verb: 'save', intent: 'fulfill' as const } }, + cancelled: { semantic: { family: 'emerge' as const, verb: 'close' } }, + dismissed: { semantic: { family: 'emerge' as const, verb: 'dismiss' } }, + 'dismissed-outside': { semantic: { family: 'emerge' as const, verb: 'dismiss' } } + }; + const target = this.runtime.partRef('content') ?? undefined; + if (target) { + this.soma.dom.apply({ target, attrs: { 'data-last-action': cause } }); + } + void this.runtime.trigger('close', { + ...(target ? { fallbackTarget: target } : {}), + semantic: CAUSES[cause].semantic + }); + } + readonly pickerShellHandle: PickerShellHandle = { getMode: () => this.opts.mode.current, commit: () => this.commit(), diff --git a/src/uix/soma/components/date-picker/date-picker-provider.svelte.ts b/src/uix/soma/components/date-picker/date-picker-provider.svelte.ts index 63c3b6da9..b6e24fe3d 100644 --- a/src/uix/soma/components/date-picker/date-picker-provider.svelte.ts +++ b/src/uix/soma/components/date-picker/date-picker-provider.svelte.ts @@ -208,6 +208,7 @@ export class DatePickerProvider { handleDateSelect(): void { if (this.opts.mode.current === 'modal') return; if (this.opts.closeOnDateSelect.current) { + this.triggerClose('committed'); this.opts.open.current = false; } } @@ -219,6 +220,7 @@ export class DatePickerProvider { * for a "Save" or "Done" button in the footer, especially in modal mode. */ commit(): void { + this.triggerClose('committed'); this.opts.open.current = false; } @@ -228,6 +230,7 @@ export class DatePickerProvider { */ cancel(): void { this.opts.value.current = this.valueOnOpen; + this.triggerClose('cancelled'); this.opts.open.current = false; } @@ -239,6 +242,36 @@ export class DatePickerProvider { this.opts.value.current = undefined; } + /** + * Fire the polymorphic `close` event on the morfo runtime with the + * right semantic concretion per cause + write `data-last-action` on + * Calendar so eidos can tint the exit animation. Mirrors the + * Dialog/Drawer/Popover dismissWith pattern (book §5.3). + * + * 'committed' → commit.save + fulfill, data-last-action='committed' + * 'cancelled' → emerge.close, data-last-action='cancelled' + * 'dismissed' → emerge.dismiss, data-last-action='dismissed' + * 'dismissed-outside'→ emerge.dismiss, data-last-action='dismissed-outside' + */ + private triggerClose( + cause: 'committed' | 'cancelled' | 'dismissed' | 'dismissed-outside' + ): void { + const CAUSES = { + committed: { semantic: { family: 'commit' as const, verb: 'save', intent: 'fulfill' as const } }, + cancelled: { semantic: { family: 'emerge' as const, verb: 'close' } }, + dismissed: { semantic: { family: 'emerge' as const, verb: 'dismiss' } }, + 'dismissed-outside': { semantic: { family: 'emerge' as const, verb: 'dismiss' } } + }; + const target = this.runtime.partRef('calendar') ?? undefined; + if (target) { + this.soma.dom.apply({ target, attrs: { 'data-last-action': cause } }); + } + void this.runtime.trigger('close', { + ...(target ? { fallbackTarget: target } : {}), + semantic: CAUSES[cause].semantic + }); + } + readonly props = $derived.by(() => this.runtimePart.assert({ ...this.runtimePart.props, diff --git a/src/uix/soma/components/date-range-picker/date-range-picker-provider.svelte.ts b/src/uix/soma/components/date-range-picker/date-range-picker-provider.svelte.ts index b02027fd1..98081ab3b 100644 --- a/src/uix/soma/components/date-range-picker/date-range-picker-provider.svelte.ts +++ b/src/uix/soma/components/date-range-picker/date-range-picker-provider.svelte.ts @@ -212,6 +212,7 @@ export class DateRangePickerProvider { handleRangeSelect(): void { if (this.opts.mode.current === 'modal') return; if (this.opts.closeOnRangeSelect.current) { + this.triggerClose('range-committed'); this.opts.open.current = false; } } @@ -223,6 +224,7 @@ export class DateRangePickerProvider { * for a "Save" or "Done" button in the footer, especially in modal mode. */ commit(): void { + this.triggerClose('range-committed'); this.opts.open.current = false; } @@ -235,6 +237,7 @@ export class DateRangePickerProvider { if (snapshot) { this.opts.value.current = { ...snapshot }; } + this.triggerClose('cancelled'); this.opts.open.current = false; } @@ -246,6 +249,31 @@ export class DateRangePickerProvider { this.opts.value.current = { start: undefined, end: undefined }; } + /** + * Polymorphic close (book §5.3) — fire the morfo `close` event with + * the per-cause semantic + write `data-last-action` on Calendar so + * eidos can tint the exit animation. Mirrors the date-picker / + * Dialog / Drawer / Popover pattern. + */ + private triggerClose( + cause: 'range-committed' | 'cancelled' | 'dismissed' | 'dismissed-outside' + ): void { + const CAUSES = { + 'range-committed': { semantic: { family: 'commit' as const, verb: 'save', intent: 'fulfill' as const } }, + cancelled: { semantic: { family: 'emerge' as const, verb: 'close' } }, + dismissed: { semantic: { family: 'emerge' as const, verb: 'dismiss' } }, + 'dismissed-outside': { semantic: { family: 'emerge' as const, verb: 'dismiss' } } + }; + const target = this.runtime.partRef('calendar') ?? undefined; + if (target) { + this.soma.dom.apply({ target, attrs: { 'data-last-action': cause } }); + } + void this.runtime.trigger('close', { + ...(target ? { fallbackTarget: target } : {}), + semantic: CAUSES[cause].semantic + }); + } + readonly props = $derived.by(() => this.runtimePart.assert({ ...this.runtimePart.props, diff --git a/src/uix/soma/components/time-picker/time-picker-provider.svelte.ts b/src/uix/soma/components/time-picker/time-picker-provider.svelte.ts index 23f8f1132..6ea497504 100644 --- a/src/uix/soma/components/time-picker/time-picker-provider.svelte.ts +++ b/src/uix/soma/components/time-picker/time-picker-provider.svelte.ts @@ -204,12 +204,14 @@ export class TimePickerProvider { /** Commit current value and close. Default for Save/Done in the footer. */ commit(): void { + this.triggerClose('committed'); this.opts.open.current = false; } /** Discard mid-session edits (revert to OPEN-edge snapshot) and close. */ cancel(): void { this.opts.value.current = this.valueOnOpen; + this.triggerClose('cancelled'); this.opts.open.current = false; } @@ -218,6 +220,30 @@ export class TimePickerProvider { this.opts.value.current = undefined; } + /** + * Polymorphic close (book §5.3) — fire the morfo `close` event with + * the per-cause semantic + write `data-last-action` on Clock so eidos + * can tint the exit animation. Mirrors the date-picker pattern. + */ + private triggerClose( + cause: 'committed' | 'cancelled' | 'dismissed' | 'dismissed-outside' + ): void { + const CAUSES = { + committed: { semantic: { family: 'commit' as const, verb: 'save', intent: 'fulfill' as const } }, + cancelled: { semantic: { family: 'emerge' as const, verb: 'close' } }, + dismissed: { semantic: { family: 'emerge' as const, verb: 'dismiss' } }, + 'dismissed-outside': { semantic: { family: 'emerge' as const, verb: 'dismiss' } } + }; + const target = this.runtime.partRef('clock') ?? undefined; + if (target) { + this.soma.dom.apply({ target, attrs: { 'data-last-action': cause } }); + } + void this.runtime.trigger('close', { + ...(target ? { fallbackTarget: target } : {}), + semantic: CAUSES[cause].semantic + }); + } + readonly props = $derived.by(() => this.runtimePart.assert({ ...this.runtimePart.props, diff --git a/src/uix/soma/components/time-range-picker/time-range-picker-provider.svelte.ts b/src/uix/soma/components/time-range-picker/time-range-picker-provider.svelte.ts index 344a732e9..303e5cc0a 100644 --- a/src/uix/soma/components/time-range-picker/time-range-picker-provider.svelte.ts +++ b/src/uix/soma/components/time-range-picker/time-range-picker-provider.svelte.ts @@ -233,6 +233,7 @@ export class TimeRangePickerProvider { const s = this.opts.startValue.current; const e = this.opts.endValue.current; if (s !== undefined && e !== undefined) { + this.triggerClose('committed'); this.opts.open.current = false; } } @@ -241,6 +242,7 @@ export class TimeRangePickerProvider { /** Save current range and close the popover. */ commit(): void { + this.triggerClose('committed'); this.opts.open.current = false; } @@ -251,6 +253,7 @@ export class TimeRangePickerProvider { this.opts.startValue.current = snapshot.start; this.opts.endValue.current = snapshot.end; } + this.triggerClose('cancelled'); this.opts.open.current = false; } @@ -260,6 +263,30 @@ export class TimeRangePickerProvider { this.opts.endValue.current = undefined; } + /** + * Polymorphic close (book §5.3) — fire the morfo `close` event with + * the per-cause semantic + write `data-last-action` on Clock so eidos + * can tint the exit animation. Mirrors the date-picker pattern. + */ + private triggerClose( + cause: 'committed' | 'cancelled' | 'dismissed' | 'dismissed-outside' + ): void { + const CAUSES = { + committed: { semantic: { family: 'commit' as const, verb: 'save', intent: 'fulfill' as const } }, + cancelled: { semantic: { family: 'emerge' as const, verb: 'close' } }, + dismissed: { semantic: { family: 'emerge' as const, verb: 'dismiss' } }, + 'dismissed-outside': { semantic: { family: 'emerge' as const, verb: 'dismiss' } } + }; + const target = this.runtime.partRef('clock') ?? undefined; + if (target) { + this.soma.dom.apply({ target, attrs: { 'data-last-action': cause } }); + } + void this.runtime.trigger('close', { + ...(target ? { fallbackTarget: target } : {}), + semantic: CAUSES[cause].semantic + }); + } + // ── Root props (emitted onto `[data-time-range-picker]`) ─────────────── readonly props = $derived.by(() =>