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(() =>