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 @@
[
"