From afb09c2254661a76c7a4dfdc2aff0fa9386cecbc Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 22 May 2026 12:54:43 +0200 Subject: [PATCH] feat(picker-shell): extend to Root + Header + Body + Footer layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promote PickerShell from action-row-only to a canonical 4-zone shell (Root → Header / Body / Footer) shared by every composite picker so date/time/color popovers get the same visual contract for free. - Add `` flex-column wrapper (emits data-picker-shell, data-mode) plus `` and `` zones matching the existing Footer's spacing + border tokens. - Auto-wrap all 5 *-content.svelte popovers (date / date-range / time / time-range / color) in `` so consumers only compose Body + Footer. - Refresh all 5 demos to use `` around picker content; date-picker demo gains a `showShellHeader` toggle showcasing the new Header zone. - CSS scales section gap + header spacing across xs/sm/md/lg picker sizes via [data-popover-content][data-picker-size] scopes, in sync with the existing Footer scale. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../color-picker/color-picker-content.svelte | 5 +- .../date-picker/date-picker-content.svelte | 5 +- .../date-range-picker-content.svelte | 5 +- .../eidos/components/picker-shell/index.ts | 69 ++++++--- .../picker-shell/picker-shell-body.svelte | 12 ++ .../picker-shell/picker-shell-header.svelte | 12 ++ .../components/picker-shell/picker-shell.css | 83 +++++++++-- .../picker-shell/picker-shell.svelte | 17 +++ .../eidos/components/picker-shell/types.ts | 15 ++ .../time-picker/time-picker-content.svelte | 5 +- .../time-range-picker-content.svelte | 5 +- .../uix/components/color-picker/+page.svelte | 138 +++++++++--------- .../uix/components/date-picker/+page.svelte | 103 +++++++++---- .../components/date-range-picker/+page.svelte | 44 ++++-- .../uix/components/time-picker/+page.svelte | 62 ++++---- .../components/time-range-picker/+page.svelte | 114 ++++++++------- 16 files changed, 463 insertions(+), 231 deletions(-) create mode 100644 src/uix/eidos/components/picker-shell/picker-shell-body.svelte create mode 100644 src/uix/eidos/components/picker-shell/picker-shell-header.svelte create mode 100644 src/uix/eidos/components/picker-shell/picker-shell.svelte diff --git a/src/uix/eidos/components/color-picker/color-picker-content.svelte b/src/uix/eidos/components/color-picker/color-picker-content.svelte index 2149b4dd6..6f07d7d19 100644 --- a/src/uix/eidos/components/color-picker/color-picker-content.svelte +++ b/src/uix/eidos/components/color-picker/color-picker-content.svelte @@ -1,6 +1,7 @@ + +
+ {@render children?.()} +
diff --git a/src/uix/eidos/components/picker-shell/picker-shell-header.svelte b/src/uix/eidos/components/picker-shell/picker-shell-header.svelte new file mode 100644 index 000000000..e6f5bcda6 --- /dev/null +++ b/src/uix/eidos/components/picker-shell/picker-shell-header.svelte @@ -0,0 +1,12 @@ + + +
+ {@render children?.()} +
diff --git a/src/uix/eidos/components/picker-shell/picker-shell.css b/src/uix/eidos/components/picker-shell/picker-shell.css index b32699ed3..d8364dec6 100644 --- a/src/uix/eidos/components/picker-shell/picker-shell.css +++ b/src/uix/eidos/components/picker-shell/picker-shell.css @@ -1,12 +1,51 @@ -/* PickerShell — visual layer for the canonical Footer + action row - * shared by every composite picker (date, time, color, range). +/* PickerShell — visual layer for the canonical 4-zone layout + * (Root → Header / Body / Footer) + action row shared by every + * composite picker (date, time, color, range). * * The host picker scopes us via the parent `[data-{x}-picker]` / - * `[data-{x}-picker-content]` element, but the action selectors - * (`[data-picker-footer/clear/cancel/close]`) are intentionally generic - * so every picker gets the same visual contract for free. + * `[data-{x}-picker-content]` element, but the layout selectors + * (`[data-picker-shell/header/body/footer/clear/cancel/close]`) are + * intentionally generic so every picker gets the same visual contract + * for free. */ +[data-picker-shell] { + /* Outer column wrapper. Size-scaling vars default to `sm`; host + * pickers re-tune via `[data-popover-content][data-picker-size='X']` + * scopes below. */ + --_picker-shell-section-gap: var(--space-3); + + display: flex; + flex-direction: column; + gap: var(--_picker-shell-section-gap); + min-block-size: 0; +} + +[data-picker-header] { + /* Mirror the Footer's bottom border by emitting a bottom-border + + * spacing block. Reuses the same `--_picker-shell-row-spacing` token + * so xs/sm/md/lg scale symmetrically with the action row below. */ + --_picker-shell-row-spacing: var(--space-3); + + display: flex; + align-items: center; + gap: var(--space-2); + padding-block-end: var(--_picker-shell-row-spacing); + border-block-end: var(--border-width) solid var(--color-border-subtle); + color: var(--color-content-primary); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + line-height: var(--leading-ui); +} + +[data-picker-body] { + display: flex; + flex-direction: column; + gap: var(--space-3); + flex: 1 1 auto; + min-block-size: 0; +} + [data-picker-footer] { /* Size-scaling vars. Defaults match `sm`. The host picker scopes these * via `[data-popover-content][data-picker-size='X']` so xs/md/lg tune @@ -21,10 +60,15 @@ align-items: center; gap: var(--_picker-shell-gap); padding-block-start: var(--_picker-shell-row-spacing); - margin-block-start: var(--_picker-shell-row-spacing); border-block-start: var(--border-width) solid var(--color-border-subtle); } +/* Standalone Footer (legacy composition without Root) keeps its own + * top margin so it still feels separated from picker content above. */ +[data-picker-footer]:not([data-picker-shell] > *) { + margin-block-start: var(--_picker-shell-row-spacing); +} + [data-picker-clear], [data-picker-cancel] { margin-inline-end: auto; @@ -51,9 +95,17 @@ } /* Size variants — host pickers emit `data-picker-size` on the popover - * content so portaled descendants can scale. The button row inherits - * the picker's size; popover chrome (padding / min-width) keeps its - * own size variant via [data-size]. */ + * content so portaled descendants can scale. The whole shell (gap, + * header spacing, button row) inherits the picker's size; popover + * chrome (padding / min-width) keeps its own size variant via + * [data-size]. */ +[data-popover-content][data-picker-size='xs'] [data-picker-shell] { + --_picker-shell-section-gap: var(--space-2); +} +[data-popover-content][data-picker-size='xs'] [data-picker-header] { + --_picker-shell-row-spacing: var(--space-2); + font-size: var(--font-size-xs); +} [data-popover-content][data-picker-size='xs'] [data-picker-footer] { --_picker-shell-control-h: var(--control-height-xxs); --_picker-shell-padding-x: var(--space-2); @@ -62,12 +114,25 @@ --_picker-shell-row-spacing: var(--space-2); } +[data-popover-content][data-picker-size='md'] [data-picker-shell] { + --_picker-shell-section-gap: var(--space-3-5); +} +[data-popover-content][data-picker-size='md'] [data-picker-header] { + font-size: var(--font-size-md); +} [data-popover-content][data-picker-size='md'] [data-picker-footer] { --_picker-shell-control-h: var(--control-height-md); --_picker-shell-padding-x: var(--space-3-5); --_picker-shell-font: var(--font-size-md); } +[data-popover-content][data-picker-size='lg'] [data-picker-shell] { + --_picker-shell-section-gap: var(--space-4); +} +[data-popover-content][data-picker-size='lg'] [data-picker-header] { + --_picker-shell-row-spacing: var(--space-4); + font-size: var(--font-size-md); +} [data-popover-content][data-picker-size='lg'] [data-picker-footer] { --_picker-shell-control-h: var(--control-height-lg); --_picker-shell-padding-x: var(--space-4); diff --git a/src/uix/eidos/components/picker-shell/picker-shell.svelte b/src/uix/eidos/components/picker-shell/picker-shell.svelte new file mode 100644 index 000000000..82111da37 --- /dev/null +++ b/src/uix/eidos/components/picker-shell/picker-shell.svelte @@ -0,0 +1,17 @@ + + +
+ {@render children?.()} +
diff --git a/src/uix/eidos/components/picker-shell/types.ts b/src/uix/eidos/components/picker-shell/types.ts index a9d3ea266..eb5db056e 100644 --- a/src/uix/eidos/components/picker-shell/types.ts +++ b/src/uix/eidos/components/picker-shell/types.ts @@ -1,6 +1,21 @@ import type { Snippet } from 'svelte' import type { HTMLAttributes, HTMLButtonAttributes } from 'svelte/elements' +/** Props for `` / `` — outer 4-zone layout. */ +export type PickerShellProps = HTMLAttributes & { + children?: Snippet +} + +/** Props for ``. */ +export type PickerShellHeaderProps = HTMLAttributes & { + children?: Snippet +} + +/** Props for ``. */ +export type PickerShellBodyProps = HTMLAttributes & { + children?: Snippet +} + /** Props for ``. */ export type PickerShellFooterProps = HTMLAttributes & { children?: Snippet diff --git a/src/uix/eidos/components/time-picker/time-picker-content.svelte b/src/uix/eidos/components/time-picker/time-picker-content.svelte index 60f79aaaa..36bf11c1a 100644 --- a/src/uix/eidos/components/time-picker/time-picker-content.svelte +++ b/src/uix/eidos/components/time-picker/time-picker-content.svelte @@ -2,6 +2,7 @@ import { ActiveEidos } from '$uix/eidos'; import { composeInlineStyle } from '$uix/eidos/lib/style'; import PopoverContent from '../popover/popover-content.svelte'; + import PickerShellRoot from '../picker-shell/picker-shell.svelte'; import { getTimePickerVisualContext } from './context.svelte'; import type { TimePickerContentProps } from './types'; @@ -67,6 +68,8 @@ style={composedStyle} > {#snippet children(snippetProps)} - {@render bodyContent?.(snippetProps)} + + {@render bodyContent?.(snippetProps)} + {/snippet} diff --git a/src/uix/eidos/components/time-range-picker/time-range-picker-content.svelte b/src/uix/eidos/components/time-range-picker/time-range-picker-content.svelte index fd98dda8b..db11006e8 100644 --- a/src/uix/eidos/components/time-range-picker/time-range-picker-content.svelte +++ b/src/uix/eidos/components/time-range-picker/time-range-picker-content.svelte @@ -2,6 +2,7 @@ import { ActiveEidos } from '$uix/eidos'; import { composeInlineStyle } from '$uix/eidos/lib/style'; import PopoverContent from '../popover/popover-content.svelte'; + import PickerShellRoot from '../picker-shell/picker-shell.svelte'; import { getTimeRangePickerVisualContext } from './context.svelte'; import type { TimeRangePickerContentProps } from './types'; @@ -67,6 +68,8 @@ style={composedStyle} > {#snippet children(snippetProps)} - {@render bodyContent?.(snippetProps)} + + {@render bodyContent?.(snippetProps)} + {/snippet} diff --git a/web/routes/uix/components/color-picker/+page.svelte b/web/routes/uix/components/color-picker/+page.svelte index fc06f332a..e6839305d 100644 --- a/web/routes/uix/components/color-picker/+page.svelte +++ b/web/routes/uix/components/color-picker/+page.svelte @@ -6,6 +6,7 @@ type ColorPickerVariant, type ChannelInputSnippetProps } from '$uix/eidos/components/color-picker'; + import { PickerShell } from '$uix/eidos/components/picker-shell'; import { compileMorfo } from '$uix/morfo'; import { colorPickerMorfo } from '@/uix/morfo/components/color-picker'; import { colorFieldMorfo } from '@/uix/morfo/components/color-field'; @@ -149,25 +150,27 @@ ' ', ' ', ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - enableAlpha && ' ', - enableAlpha && ' ', - enableAlpha && ' ', - ' ', - ' ', - ' ', - ' ', - ' {#each presets as c}', - ' ', - ' ', - ' {/each}', - ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + enableAlpha && ' ', + enableAlpha && ' ', + enableAlpha && ' ', + ' ', + ' ', + ' ', + ' ', + ' {#each presets as c}', + ' ', + ' ', + ' {/each}', + ' ', + ' ', showFooter && (showClear || showCancel || showClose) && ' ', showFooter && showClear && ' ', showFooter && showCancel && ' ', @@ -184,6 +187,7 @@ [ "