From a2434ea79ef649d846ae76ed2b683c4554c98056 Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 30 Jun 2026 23:00:17 +0200 Subject: [PATCH] feat(natural-time-picker): daylight-band time picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 24h picker driven by a horizontal daylight gradient — drag the sun/moon thumb across the night→day→night sky, jump with moment chips, or nudge with hour/minute/second steppers. Inline panel + trigger/popover variants, built on the generic Picker (deferValue) composing Slider, Button, PickerShell, Popover and Tooltip. - granularity (hour|minute|second) + optional band props; steppers keep a fixed 3-column grid; shape/size propagate to panel, trigger and chips. - fixed-tone sky palette as named recipe tokens (theme-independent). - fix soma Tooltip.Arrow (empty children slot hid the default SVG) and make PickerShell footer labels locale-reactive. --- .../natural-time-picker/context.svelte.ts | 25 + .../components/natural-time-picker/index.ts | 25 + .../natural-time-picker-content.svelte | 21 + .../natural-time-picker-panel.svelte | 313 ++++++++++ .../natural-time-picker-trigger.svelte | 35 ++ .../natural-time-picker.css | 327 +++++++++++ .../natural-time-picker.svelte | 36 ++ .../components/natural-time-picker/types.ts | 34 ++ .../picker-shell/picker-shell-cancel.svelte | 2 +- .../picker-shell/picker-shell-clear.svelte | 2 +- .../picker-shell/picker-shell-close.svelte | 2 +- src/uix/eidos/components/tooltip/tooltip.css | 48 +- src/uix/langs/common.ts | 12 +- src/uix/langs/components/index.ts | 2 + .../langs/components/natural-time-picker.ts | 22 + .../morfo/components/natural-time-picker.ts | 116 ++++ src/uix/soma/components/index.ts | 2 + .../components/natural-time-picker.svelte | 122 ++++ .../components/natural-time-picker/exports.ts | 16 + .../components/natural-time-picker/index.ts | 1 + .../natural-time-picker/internals.ts | 8 + .../natural-time-picker-provider.svelte.ts | 242 ++++++++ .../components/natural-time-picker/types.ts | 72 +++ .../tooltip/components/tooltip-arrow.svelte | 6 +- .../natural-time-picker/+page.svelte | 543 ++++++++++++++++++ 25 files changed, 2007 insertions(+), 27 deletions(-) create mode 100644 src/uix/eidos/components/natural-time-picker/context.svelte.ts create mode 100644 src/uix/eidos/components/natural-time-picker/index.ts create mode 100644 src/uix/eidos/components/natural-time-picker/natural-time-picker-content.svelte create mode 100644 src/uix/eidos/components/natural-time-picker/natural-time-picker-panel.svelte create mode 100644 src/uix/eidos/components/natural-time-picker/natural-time-picker-trigger.svelte create mode 100644 src/uix/eidos/components/natural-time-picker/natural-time-picker.css create mode 100644 src/uix/eidos/components/natural-time-picker/natural-time-picker.svelte create mode 100644 src/uix/eidos/components/natural-time-picker/types.ts create mode 100644 src/uix/langs/components/natural-time-picker.ts create mode 100644 src/uix/morfo/components/natural-time-picker.ts create mode 100644 src/uix/soma/components/natural-time-picker/components/natural-time-picker.svelte create mode 100644 src/uix/soma/components/natural-time-picker/exports.ts create mode 100644 src/uix/soma/components/natural-time-picker/index.ts create mode 100644 src/uix/soma/components/natural-time-picker/internals.ts create mode 100644 src/uix/soma/components/natural-time-picker/natural-time-picker-provider.svelte.ts create mode 100644 src/uix/soma/components/natural-time-picker/types.ts create mode 100644 web/routes/uix/components/natural-time-picker/+page.svelte diff --git a/src/uix/eidos/components/natural-time-picker/context.svelte.ts b/src/uix/eidos/components/natural-time-picker/context.svelte.ts new file mode 100644 index 000000000..33bd07206 --- /dev/null +++ b/src/uix/eidos/components/natural-time-picker/context.svelte.ts @@ -0,0 +1,25 @@ +import { getContext, setContext } from 'svelte'; +import type { ShapeFamily, Size } from '$uix/eidos/lib/types'; + +/** The size subset NaturalTimePicker exposes. */ +export type NaturalTimePickerSize = Extract; + +/** + * Visual context — size + shape propagated from the eidos root to the panel + * (and its composed Buttons). Lives in eidos (the soma layer must not depend on + * eidos visual types). Children adopt the parent's shape by default. + */ +export interface NaturalTimePickerVisual { + getSize: () => NaturalTimePickerSize; + getShape: () => ShapeFamily; +} + +const KEY = Symbol('natural-time-picker-visual'); + +export function setNaturalTimePickerVisual(visual: NaturalTimePickerVisual): void { + setContext(KEY, visual); +} + +export function getNaturalTimePickerVisual(): NaturalTimePickerVisual | undefined { + return getContext(KEY) as NaturalTimePickerVisual | undefined; +} diff --git a/src/uix/eidos/components/natural-time-picker/index.ts b/src/uix/eidos/components/natural-time-picker/index.ts new file mode 100644 index 000000000..d3c0df5df --- /dev/null +++ b/src/uix/eidos/components/natural-time-picker/index.ts @@ -0,0 +1,25 @@ +import NaturalTimePickerComponent from './natural-time-picker.svelte'; +import Panel from './natural-time-picker-panel.svelte'; +import Trigger from './natural-time-picker-trigger.svelte'; +import Content from './natural-time-picker-content.svelte'; + +type NaturalTimePickerNamespace = typeof NaturalTimePickerComponent & { + Panel: typeof Panel; + Trigger: typeof Trigger; + Content: typeof Content; +}; + +const NaturalTimePicker = NaturalTimePickerComponent as NaturalTimePickerNamespace; +NaturalTimePicker.Panel = Panel; +NaturalTimePicker.Trigger = Trigger; +NaturalTimePicker.Content = Content; + +export { NaturalTimePicker }; +export default NaturalTimePicker; + +export type { + NaturalTimePickerProps, + NaturalTimePickerPanelProps, + NaturalTimePickerTriggerProps, + NaturalTimePickerContentProps +} from './types'; diff --git a/src/uix/eidos/components/natural-time-picker/natural-time-picker-content.svelte b/src/uix/eidos/components/natural-time-picker/natural-time-picker-content.svelte new file mode 100644 index 000000000..f353bc5f2 --- /dev/null +++ b/src/uix/eidos/components/natural-time-picker/natural-time-picker-content.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/src/uix/eidos/components/natural-time-picker/natural-time-picker-panel.svelte b/src/uix/eidos/components/natural-time-picker/natural-time-picker-panel.svelte new file mode 100644 index 000000000..f504eec7f --- /dev/null +++ b/src/uix/eidos/components/natural-time-picker/natural-time-picker-panel.svelte @@ -0,0 +1,313 @@ + + +
+ +
+ + {natTP.displayTime}{#if natTP.meridiem}{natTP.meridiem}{/if} + + {#if natTP.opts.showPeriod.current} + + + {currentPeriodLabel} + + {/if} +
+ + + {#if natTP.opts.band.current} +
+ natTP.setMinutes(v[0] ?? 0)} + min={0} + max={1439} + step={1} + {disabled} + aria-label={bandLabel} + > + + + + + + + +
+ {#each ticks as tick} + {tick.label} + {/each} +
+
+ {/if} + + +
+
+ + {eidos.langs.ts('#?common.time.hour|Hora')} + +
+ {#if natTP.showMinutes} +
+ + {eidos.langs.ts('#?common.time.minute|Minuto')} + +
+ {/if} + {#if natTP.showSeconds} +
+ + {eidos.langs.ts('#?common.time.second|Segundo')} + +
+ {/if} +
+ + + {#if natTP.opts.showMoments.current} +
+ {#if chipsIconOnly} + + {#each PERIODS as period} + + + {#snippet child({ props }: { props: Record })} + + {/snippet} + + + + + {eidos.langs.ts(period.lang)} · {startHourLabel(period.jump)} + + + + {/each} + + {:else} + {#each PERIODS as period} + + {/each} + {/if} +
+ {/if} + + + + + + + +
diff --git a/src/uix/eidos/components/natural-time-picker/natural-time-picker-trigger.svelte b/src/uix/eidos/components/natural-time-picker/natural-time-picker-trigger.svelte new file mode 100644 index 000000000..e882c935d --- /dev/null +++ b/src/uix/eidos/components/natural-time-picker/natural-time-picker-trigger.svelte @@ -0,0 +1,35 @@ + + + + + {display || resolvedPlaceholder} + + + diff --git a/src/uix/eidos/components/natural-time-picker/natural-time-picker.css b/src/uix/eidos/components/natural-time-picker/natural-time-picker.css new file mode 100644 index 000000000..49ac12813 --- /dev/null +++ b/src/uix/eidos/components/natural-time-picker/natural-time-picker.css @@ -0,0 +1,327 @@ +/* NaturalTimePicker — visual layer. + * + * Everything uses system tokens (surface / content / border / space / + * control-height / radius / shadow; composed + + + + + + + + + + + {#if tab === 'live'} +
+

Controls

+

+ The component API. Foundation axes (density, scaling, mode, dir, border) live in the + tab. +

+ +
+ soma value + transaction +
+
+ + + + + + + {#if granularity === 'second'} + + {/if} + + + + + +
+ +
+ eidos visual · size + shape +
+
+ + +
+ +
+
+ eidos + visual · root + trigger + content + svelte +
+
{eidosSnippet}
+
+
+ {/if} + + {#if tab === 'system'} +
+

eidos System axes

+

+ Foundation knobs that are NOT component props, applied live to the stage. Try + dark mode — the daylight band keeps its fixed-tone sky (it depicts a + physical thing), while the panel chrome retints with the theme. +

+ +
+ {/if} + + {#if tab === 'motion'} +
+

Motion catalog

+

+ The motion system. NaturalTimePicker has no motion prop — the popover + open/close animation comes from the composed Popover, and the thumb tracks the pointer + directly. Below is the catalog the system ships. +

+ +
+ {/if} + + {#if tab === 'sema'} +
+

sema firma + sound

+

+ NaturalTimePicker declares 0 events of its own — open / close are + delegated to the composed Popover (which IS the dialog) and the + accept / cancel / clear perception flows through the generic Picker + + PickerShell. So the firma table is intentionally empty here; the + perceptual events live on those composed components' demos. +

+ stageRef?.querySelector('[data-natural-time-picker]') ?? stageRef} /> +
+ {/if} + + {#if tab === 'services'} +
+

Services

+

+ Consumes langs (i18n): the morfo declares translatable + texts, and the panel labels (Mañana / Mediodía / día / noche / Borrar / + Cancelar / Listo) resolve through uix.langs with literal fallbacks. The + hour cycle follows soma.dates when hourCycle is unset. +

+
+ + + + + + + +
Text keyDefaultService
label{naturalTimePickerMorfo.texts?.label ?? '—'}langs
trigger{naturalTimePickerMorfo.texts?.trigger ?? '—'}langs
moment.* / day / night#?components.natural-time-picker.* (fallbacks inline)langs
+
+
+ {/if} + + {#if tab === 'api'} +
+

API reference

+
+ + + + + + + + + + + + + + + +
PropTypeDefaultDescription
value bindableTimeValue | undefined—Current time.
onValueChange(v) => void—Fires on commit (deferred) or every edit (live).
placeholderTimeValue12:00Band/display anchor when value is unset.
open bindablebooleanfalsePopover state.
deferValuebooleantrueBuffer edits until Listo (commit); else write the bound value live.
mode'inline' | 'modal''inline'Popover dismissal — modal blocks outside-click + Escape.
hourCycle12 | 24soma dates → 24Display clock; 12 adds AM/PM.
minuteStepnumber1Minutes per minute-stepper tap.
showMoments eidosbooleantrueShow the moment chips (Mañana / Mediodía / …).
disabled / readonly / requiredbooleanfalseInteraction state.
locale / dirstring / 'ltr' | 'rtl'soma configi18n.
+
+
Parts
+

+ NaturalTimePicker (root) renders Panel inline by default; + Trigger + Content opt into the popover variant. Footer is the + shared PickerShell.Clear / .Cancel / .Close. +

+
+ {/if} + + {#if tab === 'morfo'} +
+

morfo · declarative contract

+

+ Source: src/uix/morfo/components/natural-time-picker.ts. Open / close are + delegated to the composed Popover, so this morfo declares the provider + trigger + + content surface and no events. +

+ +
+ + + + + + + + + +
FieldValue
name"{naturalTimePickerMorfo.name}"
kebab"{naturalTimePickerMorfo.kebab}"
scope[{naturalTimePickerMorfo.scope.map((s) => `"${s}"`).join(', ')}]
parts.length{naturalTimePickerMorfo.parts.length}
events.length{naturalTimePickerMorfo.events.length}
+
+ +
Parts
+
+ + + + {#each partsList as part} + + + + + + + + + {/each} + +
PartMarkerElementRoleArchetypeStates
{part.kebab}[{part.marker}]<{part.defaultElement}>{part.role ?? '—'}{part.archetype ?? '—'}{part.states.length ? part.states.join(' | ') : '—'}
+
+
+ {/if} + + {#if tab === 'recipe'} +
+

Eidos recipe

+

+ Selectors at src/uix/eidos/components/natural-time-picker/natural-time-picker.css. + Everything uses system tokens (surface / content / border / space / radius). The ONE + bespoke value is the daylight gradient — fixed-tone literal stops (only the night + endpoint flips light/dark), the same doctrine as the Words rail. +

+
+ + + + + + + + + +
SelectorSource
[data-natural-time-picker-panel]eidos
[data-natural-time-picker-band] · the daylight gradienteidos (fixed-tone)
[data-natural-time-picker-band] [data-slider-thumb] · sun/mooneidos
[data-natural-time-picker-trigger][data-state]morfo + eidos
[data-picker-footer] (PickerShell)eidos
+
+
+ {/if} + + {#if tab === 'a11y'} +
+

Accessibility

+

+ The trigger is a button with aria-haspopup="dialog" / aria-expanded; + the popover is the composed Popover (WAI-ARIA dialog). The band is a + slider + (arrow keys step; Home/End jump). Steppers + chips are buttons. +

+
Keyboard
+
+ + + + + + + + +
KeyAction
Enter / Space on TriggerOpen the popover.
ArrowLeft / ArrowRight on bandStep the time by one minute.
Home / End on bandJump to 00:00 / 23:59.
Escapeinline mode: dismiss (discards the draft in deferred mode).
+
+
+ {/if} +