feat(picker-shell): extend to Root + Header + Body + Footer layout

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 `<PickerShell.Root>` flex-column wrapper (emits data-picker-shell,
  data-mode) plus `<PickerShell.Header>` and `<PickerShell.Body>` zones
  matching the existing Footer's spacing + border tokens.
- Auto-wrap all 5 *-content.svelte popovers (date / date-range / time /
  time-range / color) in `<PickerShell.Root>` so consumers only compose
  Body + Footer.
- Refresh all 5 demos to use `<PickerShell.Body>` 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) <noreply@anthropic.com>
active-uix
dev 5 months ago
parent da10548a68
commit afb09c2254

@ -1,6 +1,7 @@
<script lang="ts">
import { ActiveEidos } from '$uix/eidos';
import PopoverContent from '../popover/popover-content.svelte';
import PickerShellRoot from '../picker-shell/picker-shell.svelte';
import { getColorPickerVisualContext } from './context.svelte';
import type { ColorPickerContentProps } from './types';
@ -28,6 +29,8 @@
data-color-picker-content=""
>
{#snippet children(snippetProps)}
{@render bodyContent?.(snippetProps)}
<PickerShellRoot>
{@render bodyContent?.(snippetProps)}
</PickerShellRoot>
{/snippet}
</PopoverContent>

@ -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 { getDatePickerVisualContext } from './context.svelte';
import type { DatePickerContentProps } from './types';
@ -71,6 +72,8 @@
style={composedStyle}
>
{#snippet children(snippetProps)}
{@render bodyContent?.(snippetProps)}
<PickerShellRoot>
{@render bodyContent?.(snippetProps)}
</PickerShellRoot>
{/snippet}
</PopoverContent>

@ -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 { getDateRangePickerVisualContext } from './context.svelte';
import type { DateRangePickerContentProps } from './types';
@ -71,6 +72,8 @@
style={composedStyle}
>
{#snippet children(snippetProps)}
{@render bodyContent?.(snippetProps)}
<PickerShellRoot>
{@render bodyContent?.(snippetProps)}
</PickerShellRoot>
{/snippet}
</PopoverContent>

@ -1,41 +1,64 @@
import PickerShellRoot from './picker-shell.svelte'
import Header from './picker-shell-header.svelte'
import Body from './picker-shell-body.svelte'
import Footer from './picker-shell-footer.svelte'
import Clear from './picker-shell-clear.svelte'
import Cancel from './picker-shell-cancel.svelte'
import Close from './picker-shell-close.svelte'
/**
* PickerShell — generic Footer + action row shared by every composite
* picker that registers a `PickerShellHandle` via `pickerShellContext`.
* PickerShell — canonical 4-zone layout (Root → Header / Body / Footer)
* + action row shared by every composite picker that registers a
* `PickerShellHandle` via `pickerShellContext`.
*
* import { PickerShell } from '$uix/eidos/components/picker-shell';
*
* <PickerShell.Footer>
* <PickerShell.Clear />
* <PickerShell.Cancel />
* <PickerShell.Close />
* </PickerShell.Footer>
* <PickerShell>
* <PickerShell.Header>...</PickerShell.Header>
* <PickerShell.Body>
* <DatePicker.Calendar>...</DatePicker.Calendar>
* </PickerShell.Body>
* <PickerShell.Footer>
* <PickerShell.Clear />
* <PickerShell.Cancel />
* <PickerShell.Close />
* </PickerShell.Footer>
* </PickerShell>
*
* Each composite picker re-exports these as `X.Footer / X.Clear / …`
* for namespace consistency:
*
* <ColorPicker.Footer>
* <ColorPicker.Clear />
* <ColorPicker.Cancel />
* <ColorPicker.Close />
* </ColorPicker.Footer>
*
* is the same render — both reach the host picker through context.
* Header is optional. Footer / Clear / Cancel / Close are also exposed
* under each composite picker's namespace for symmetry
* (`<DatePicker.Footer>`, `<ColorPicker.Clear>`, …).
*/
export const PickerShell = {
Footer,
Clear,
Cancel,
Close
type PickerShellNamespace = typeof PickerShellRoot & {
Root: typeof PickerShellRoot
Header: typeof Header
Body: typeof Body
Footer: typeof Footer
Clear: typeof Clear
Cancel: typeof Cancel
Close: typeof Close
}
export { Footer, Clear, Cancel, Close }
const PickerShell = PickerShellRoot as PickerShellNamespace
PickerShell.Root = PickerShellRoot
PickerShell.Header = Header
PickerShell.Body = Body
PickerShell.Footer = Footer
PickerShell.Clear = Clear
PickerShell.Cancel = Cancel
PickerShell.Close = Close
export { PickerShell }
export default PickerShell
export { PickerShellRoot as Root, Header, Body, Footer, Clear, Cancel, Close }
export type {
PickerShellProps as RootProps,
PickerShellProps,
PickerShellHeaderProps as HeaderProps,
PickerShellBodyProps as BodyProps,
PickerShellFooterProps as FooterProps,
PickerShellClearProps as ClearProps,
PickerShellCancelProps as CancelProps,

@ -0,0 +1,12 @@
<script lang="ts">
import type { PickerShellBodyProps } from './types';
// Body zone — the scrollable middle area that contains the
// picker-specific content (calendar / clock / color area + sliders).
// Pure layout, no behaviour.
let { children, ...rest }: PickerShellBodyProps = $props();
</script>
<div data-picker-body {...rest}>
{@render children?.()}
</div>

@ -0,0 +1,12 @@
<script lang="ts">
import type { PickerShellHeaderProps } from './types';
// Header zone — optional top strip inside a picker popover. Mirrors
// the Footer's bottom border by emitting a bottom-border + spacing
// block via [data-picker-header] CSS. Pure layout, no behaviour.
let { children, ...rest }: PickerShellHeaderProps = $props();
</script>
<div data-picker-header {...rest}>
{@render children?.()}
</div>

@ -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);

@ -0,0 +1,17 @@
<script lang="ts">
import { getPickerShellHandle } from '$soma/components/picker-shell';
import type { PickerShellProps } from './types';
// PickerShell root — outer flex column wrapping the canonical
// Header / Body / Footer zones for every composite picker. Pure
// composition; reads `mode` off `pickerShellContext` when available so
// the CSS can react to inline vs modal.
let { children, ...rest }: PickerShellProps = $props();
const shell = getPickerShellHandle();
const mode = $derived(shell?.getMode() ?? 'inline');
</script>
<div data-picker-shell data-mode={mode} {...rest}>
{@render children?.()}
</div>

@ -1,6 +1,21 @@
import type { Snippet } from 'svelte'
import type { HTMLAttributes, HTMLButtonAttributes } from 'svelte/elements'
/** Props for `<PickerShell>` / `<PickerShell.Root>` — outer 4-zone layout. */
export type PickerShellProps = HTMLAttributes<HTMLDivElement> & {
children?: Snippet
}
/** Props for `<PickerShell.Header>`. */
export type PickerShellHeaderProps = HTMLAttributes<HTMLDivElement> & {
children?: Snippet
}
/** Props for `<PickerShell.Body>`. */
export type PickerShellBodyProps = HTMLAttributes<HTMLDivElement> & {
children?: Snippet
}
/** Props for `<PickerShell.Footer>`. */
export type PickerShellFooterProps = HTMLAttributes<HTMLDivElement> & {
children?: Snippet

@ -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)}
<PickerShellRoot>
{@render bodyContent?.(snippetProps)}
</PickerShellRoot>
{/snippet}
</PopoverContent>

@ -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)}
<PickerShellRoot>
{@render bodyContent?.(snippetProps)}
</PickerShellRoot>
{/snippet}
</PopoverContent>

@ -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 @@
' <ColorPicker.Trigger><ColorPicker.ValueText /></ColorPicker.Trigger>',
' </ColorPicker.Control>',
' <ColorPicker.Content>',
' <ColorPicker.Area>',
' <ColorPicker.AreaBackground />',
' <ColorPicker.AreaThumb />',
' </ColorPicker.Area>',
' <ColorPicker.ChannelSlider channel="hue">',
' <ColorPicker.ChannelSliderTrack /><ColorPicker.ChannelSliderThumb />',
' </ColorPicker.ChannelSlider>',
enableAlpha && ' <ColorPicker.ChannelSlider channel="alpha">',
enableAlpha && ' <ColorPicker.ChannelSliderTrack /><ColorPicker.ChannelSliderThumb />',
enableAlpha && ' </ColorPicker.ChannelSlider>',
' <ColorPicker.ValueSwatch />',
' <ColorPicker.ChannelInput />',
' <ColorPicker.EyeDropper />',
' <ColorPicker.SwatchGroup>',
' {#each presets as c}<ColorPicker.SwatchTrigger color={c}>',
' <ColorPicker.Swatch color={c} />',
' <ColorPicker.SwatchIndicator />',
' </ColorPicker.SwatchTrigger>{/each}',
' </ColorPicker.SwatchGroup>',
' <PickerShell.Body>',
' <ColorPicker.Area>',
' <ColorPicker.AreaBackground />',
' <ColorPicker.AreaThumb />',
' </ColorPicker.Area>',
' <ColorPicker.ChannelSlider channel="hue">',
' <ColorPicker.ChannelSliderTrack /><ColorPicker.ChannelSliderThumb />',
' </ColorPicker.ChannelSlider>',
enableAlpha && ' <ColorPicker.ChannelSlider channel="alpha">',
enableAlpha && ' <ColorPicker.ChannelSliderTrack /><ColorPicker.ChannelSliderThumb />',
enableAlpha && ' </ColorPicker.ChannelSlider>',
' <ColorPicker.ValueSwatch />',
' <ColorPicker.ChannelInput />',
' <ColorPicker.EyeDropper />',
' <ColorPicker.SwatchGroup>',
' {#each presets as c}<ColorPicker.SwatchTrigger color={c}>',
' <ColorPicker.Swatch color={c} />',
' <ColorPicker.SwatchIndicator />',
' </ColorPicker.SwatchTrigger>{/each}',
' </ColorPicker.SwatchGroup>',
' </PickerShell.Body>',
showFooter && (showClear || showCancel || showClose) && ' <PickerShell.Footer>',
showFooter && showClear && ' <PickerShell.Clear />',
showFooter && showCancel && ' <PickerShell.Cancel />',
@ -184,6 +187,7 @@
[
"<script lang='ts'>",
" import { ColorPicker } from '$uix/eidos/components/color-picker';",
" import { PickerShell } from '$uix/eidos/components/picker-shell';",
" import { colorValueFromRgb, type ColorValue } from '$libs/color';",
'',
' let value = $state<ColorValue | undefined>(colorValueFromRgb({ r: 30, g: 144, b: 255, a: 1 }));',
@ -207,16 +211,18 @@
' </ColorPicker.Control>',
' <ColorPicker.Portal>',
' <ColorPicker.Content>',
' <ColorPicker.Area />',
' <ColorPicker.ChannelSlider channel="hue" />',
enableAlpha && ' <ColorPicker.ChannelSlider channel="alpha" />',
' <!-- compact value row: swatch + channel input + eyedropper -->',
' <ColorPicker.ValueSwatch />',
' <ColorPicker.ChannelInput />',
' <ColorPicker.EyeDropper />',
' <ColorPicker.SwatchGroup>',
' {#each presets as c}<ColorPicker.SwatchTrigger color={c} />{/each}',
' </ColorPicker.SwatchGroup>',
' <PickerShell.Body>',
' <ColorPicker.Area />',
' <ColorPicker.ChannelSlider channel="hue" />',
enableAlpha && ' <ColorPicker.ChannelSlider channel="alpha" />',
' <!-- compact value row: swatch + channel input + eyedropper -->',
' <ColorPicker.ValueSwatch />',
' <ColorPicker.ChannelInput />',
' <ColorPicker.EyeDropper />',
' <ColorPicker.SwatchGroup>',
' {#each presets as c}<ColorPicker.SwatchTrigger color={c} />{/each}',
' </ColorPicker.SwatchGroup>',
' </PickerShell.Body>',
showFooter && (showClear || showCancel || showClose) && ' <ColorPicker.Footer>',
showFooter && showClear && ' <ColorPicker.Clear />',
showFooter && showCancel && ' <ColorPicker.Cancel />',
@ -279,45 +285,47 @@
<ColorPicker.Portal>
<ColorPicker.Content>
<ColorPicker.Area />
<!-- Channel sliders with explicit row labels so users
know what each gradient drives. -->
<div class="cp-slider-row">
<span class="cp-slider-label">Hue</span>
<ColorPicker.ChannelSlider channel="hue" />
</div>
{#if enableAlpha}
<PickerShell.Body>
<ColorPicker.Area />
<!-- Channel sliders with explicit row labels so users
know what each gradient drives. -->
<div class="cp-slider-row">
<span class="cp-slider-label">Alpha</span>
<ColorPicker.ChannelSlider channel="alpha" />
<span class="cp-slider-label">Hue</span>
<ColorPicker.ChannelSlider channel="hue" />
</div>
{#if enableAlpha}
<div class="cp-slider-row">
<span class="cp-slider-label">Alpha</span>
<ColorPicker.ChannelSlider channel="alpha" />
</div>
{/if}
<!-- Compact value row: live preview swatch + segmented channel
input + format select + eyedropper, all on one line. -->
<div class="cp-value-row">
<ColorPicker.ValueSwatch />
<ColorPicker.ChannelInput>
{#snippet children({ segments }: ChannelInputSnippetProps)}
{#each segments as { part, value }, i (`${part}-${i}`)}
<ColorPicker.ChannelSegment {part}>{value}</ColorPicker.ChannelSegment>
{/each}
<ColorPicker.FormatSelect />
{/snippet}
</ColorPicker.ChannelInput>
<ColorPicker.EyeDropper aria-label="Pick color from screen" />
</div>
{/if}
<!-- Compact value row: live preview swatch + segmented channel
input + format select + eyedropper, all on one line. -->
<div class="cp-value-row">
<ColorPicker.ValueSwatch />
<ColorPicker.ChannelInput>
{#snippet children({ segments }: ChannelInputSnippetProps)}
{#each segments as { part, value }, i (`${part}-${i}`)}
<ColorPicker.ChannelSegment {part}>{value}</ColorPicker.ChannelSegment>
{/each}
<ColorPicker.FormatSelect />
{/snippet}
</ColorPicker.ChannelInput>
<ColorPicker.EyeDropper aria-label="Pick color from screen" />
</div>
<div class="cp-section-label">Preset colors</div>
<ColorPicker.SwatchGroup aria-label="Preset colors">
{#each presets as preset}
<ColorPicker.SwatchTrigger color={preset}>
<ColorPicker.Swatch color={preset} />
<ColorPicker.SwatchIndicator />
</ColorPicker.SwatchTrigger>
{/each}
</ColorPicker.SwatchGroup>
<div class="cp-section-label">Preset colors</div>
<ColorPicker.SwatchGroup aria-label="Preset colors">
{#each presets as preset}
<ColorPicker.SwatchTrigger color={preset}>
<ColorPicker.Swatch color={preset} />
<ColorPicker.SwatchIndicator />
</ColorPicker.SwatchTrigger>
{/each}
</ColorPicker.SwatchGroup>
</PickerShell.Body>
{#if showFooter && (showClear || showCancel || showClose)}
<ColorPicker.Footer>

@ -8,6 +8,7 @@
type DatePickerSize,
type DatePickerVariant
} from '$uix/eidos/components/date-picker';
import { PickerShell } from '$uix/eidos/components/picker-shell';
import { compileMorfo } from '$uix/morfo';
import { datePickerMorfo } from '@/uix/morfo/components/date-picker';
import { calendarMorfo } from '@/uix/morfo/components/calendar';
@ -65,6 +66,10 @@
let showClear = $state(true);
let showCancel = $state(true);
let showClose = $state(true);
// PickerShell.Header toggle — demonstrates the optional top zone of
// the canonical 4-part picker layout. The Header sits above Body and
// renders with its own bottom border, mirroring the Footer.
let showShellHeader = $state(false);
let disableDaysOutsideMonth = $state(true);
let fixedWeeks = $state(false);
@ -301,17 +306,25 @@
' </DatePicker.Input>',
' <DatePicker.Portal>',
' <DatePicker.Content align="end" sideOffset={10} collisionPadding={12}>',
' <DatePicker.Calendar>',
' {#snippet children({ months, weekdays })}',
' <DatePicker.Header>...</DatePicker.Header>',
' {#each months as month}',
' {#if months.length > 1}',
' <DatePicker.Heading id={monthHeadingId(month.value)} value={month.value} />',
' {/if}',
' <DatePicker.Grid>...</DatePicker.Grid>',
' {/each}',
' {/snippet}',
' </DatePicker.Calendar>',
showShellHeader && ' <PickerShell.Header>Pick a date</PickerShell.Header>',
' <PickerShell.Body>',
' <DatePicker.Calendar>',
' {#snippet children({ months, weekdays })}',
' <DatePicker.Header>...</DatePicker.Header>',
' {#each months as month}',
' {#if months.length > 1}',
' <DatePicker.Heading id={monthHeadingId(month.value)} value={month.value} />',
' {/if}',
' <DatePicker.Grid>...</DatePicker.Grid>',
' {/each}',
' {/snippet}',
' </DatePicker.Calendar>',
' </PickerShell.Body>',
showFooter && (showClear || showCancel || showClose) && ' <DatePicker.Footer>',
showFooter && showClear && ' <DatePicker.Clear />',
showFooter && showCancel && ' <DatePicker.Cancel />',
showFooter && showClose && ' <DatePicker.Close />',
showFooter && (showClear || showCancel || showClose) && ' </DatePicker.Footer>',
' </DatePicker.Content>',
' </DatePicker.Portal>',
'</DatePicker.Provider>'
@ -324,6 +337,7 @@
[
"<script lang='ts'>",
" import { DatePicker } from '$uix/eidos/components/date-picker';",
" import { PickerShell } from '$uix/eidos/components/picker-shell';",
" import { CalendarDate, type DateValue } from '$libs/days';",
'',
` const minValue = ${dateCtor(minValue)};`,
@ -373,17 +387,25 @@
' </DatePicker.Input>',
' <DatePicker.Portal>',
' <DatePicker.Content align="end" sideOffset={10} collisionPadding={12}>',
' <DatePicker.Calendar>',
' {#snippet children({ months, weekdays })}',
' <DatePicker.Header>...</DatePicker.Header>',
' {#each months as month}',
' {#if months.length > 1}',
' <DatePicker.Heading id={monthHeadingId(month.value)} value={month.value} />',
' {/if}',
' <DatePicker.Grid>...</DatePicker.Grid>',
' {/each}',
' {/snippet}',
' </DatePicker.Calendar>',
showShellHeader && ' <PickerShell.Header>Pick a date</PickerShell.Header>',
' <PickerShell.Body>',
' <DatePicker.Calendar>',
' {#snippet children({ months, weekdays })}',
' <DatePicker.Header>...</DatePicker.Header>',
' {#each months as month}',
' {#if months.length > 1}',
' <DatePicker.Heading id={monthHeadingId(month.value)} value={month.value} />',
' {/if}',
' <DatePicker.Grid>...</DatePicker.Grid>',
' {/each}',
' {/snippet}',
' </DatePicker.Calendar>',
' </PickerShell.Body>',
showFooter && (showClear || showCancel || showClose) && ' <DatePicker.Footer>',
showFooter && showClear && ' <DatePicker.Clear />',
showFooter && showCancel && ' <DatePicker.Cancel />',
showFooter && showClose && ' <DatePicker.Close />',
showFooter && (showClear || showCancel || showClose) && ' </DatePicker.Footer>',
' </DatePicker.Content>',
' </DatePicker.Portal>',
'</DatePicker>'
@ -467,17 +489,22 @@
</div>
<DatePicker.Portal>
<DatePicker.Content align="end" sideOffset={10} collisionPadding={12}>
{#if kind === 'year'}
<DatePicker.YearView />
{:else if kind === 'month'}
<DatePicker.MonthView />
{:else}
<DatePicker.Calendar>
{#snippet children(args: DatePickerCalendarSnippetProps)}
{@render calendarBody(args)}
{/snippet}
</DatePicker.Calendar>
{#if showShellHeader}
<PickerShell.Header>Pick a date</PickerShell.Header>
{/if}
<PickerShell.Body>
{#if kind === 'year'}
<DatePicker.YearView />
{:else if kind === 'month'}
<DatePicker.MonthView />
{:else}
<DatePicker.Calendar>
{#snippet children(args: DatePickerCalendarSnippetProps)}
{@render calendarBody(args)}
{/snippet}
</DatePicker.Calendar>
{/if}
</PickerShell.Body>
{@render pickerFooter()}
</DatePicker.Content>
</DatePicker.Portal>
@ -671,6 +698,18 @@
></span
>
</label>
<label data-uix-control>
<span data-uix-control-label
>shell header <span data-uix-control-hint
>optional &lt;PickerShell.Header&gt; zone</span
></span
>
<span data-uix-switch
><input type="checkbox" bind:checked={showShellHeader} /><span
data-uix-switch-label>Header</span
></span
>
</label>
<label data-uix-control>
<span data-uix-control-label>calendar behavior</span>
<span data-uix-switch><input type="checkbox" bind:checked={closeOnDateSelect} /><span data-uix-switch-label>close on select</span></span>

@ -8,6 +8,7 @@
type DateRangePickerSize,
type DateRangePickerVariant
} from '$uix/eidos/components/date-range-picker';
import { PickerShell } from '$uix/eidos/components/picker-shell';
import { compileMorfo } from '$uix/morfo';
import { dateRangePickerMorfo } from '@/uix/morfo/components/date-range-picker';
import { dateRangeFieldMorfo } from '@/uix/morfo/components/date-range-field';
@ -387,7 +388,14 @@
' </DateRangePicker.Input>',
' <DateRangePicker.Portal>',
' <DateRangePicker.Content align="end" sideOffset={10} collisionPadding={12}>',
' <DateRangePicker.Calendar>...</DateRangePicker.Calendar>',
' <PickerShell.Body>',
' <DateRangePicker.Calendar>...</DateRangePicker.Calendar>',
' </PickerShell.Body>',
' <DateRangePicker.Footer>',
' <DateRangePicker.Clear />',
' <DateRangePicker.Cancel />',
' <DateRangePicker.Close />',
' </DateRangePicker.Footer>',
' </DateRangePicker.Content>',
' </DateRangePicker.Portal>',
'</DateRangePicker.Provider>'
@ -400,6 +408,7 @@
[
"<script lang='ts'>",
" import { DateRangePicker } from '$uix/eidos/components/date-range-picker';",
" import { PickerShell } from '$uix/eidos/components/picker-shell';",
" import { CalendarDate, type DateRange, type DateValue } from '$libs/days';",
'',
` const minValue = ${dateCtor(minValue)};`,
@ -449,7 +458,14 @@
' </div>',
' <DateRangePicker.Portal>',
' <DateRangePicker.Content align="end" sideOffset={10} collisionPadding={12}>',
' <DateRangePicker.Calendar>...</DateRangePicker.Calendar>',
' <PickerShell.Body>',
' <DateRangePicker.Calendar>...</DateRangePicker.Calendar>',
' </PickerShell.Body>',
' <DateRangePicker.Footer>',
' <DateRangePicker.Clear />',
' <DateRangePicker.Cancel />',
' <DateRangePicker.Close />',
' </DateRangePicker.Footer>',
' </DateRangePicker.Content>',
' </DateRangePicker.Portal>',
'</DateRangePicker>'
@ -539,17 +555,19 @@
</div>
<DateRangePicker.Portal>
<DateRangePicker.Content align="end" sideOffset={10} collisionPadding={12}>
{#if kind === 'year'}
<DateRangePicker.YearView />
{:else if kind === 'month'}
<DateRangePicker.MonthView />
{:else}
<DateRangePicker.Calendar>
{#snippet children(args: DateRangePickerCalendarSnippetProps)}
{@render calendarBody(args)}
{/snippet}
</DateRangePicker.Calendar>
{/if}
<PickerShell.Body>
{#if kind === 'year'}
<DateRangePicker.YearView />
{:else if kind === 'month'}
<DateRangePicker.MonthView />
{:else}
<DateRangePicker.Calendar>
{#snippet children(args: DateRangePickerCalendarSnippetProps)}
{@render calendarBody(args)}
{/snippet}
</DateRangePicker.Calendar>
{/if}
</PickerShell.Body>
{@render pickerFooter()}
</DateRangePicker.Content>
</DateRangePicker.Portal>

@ -6,6 +6,7 @@
type TimePickerVariant,
type InputSnippetProps
} from '$uix/eidos/components/time-picker';
import { PickerShell } from '$uix/eidos/components/picker-shell';
import { compileMorfo } from '$uix/morfo';
import { timePickerMorfo } from '@/uix/morfo/components/time-picker';
import { timeFieldMorfo } from '@/uix/morfo/components/time-field';
@ -156,6 +157,7 @@
[
"<script lang='ts'>",
" import { TimePicker } from '$uix/eidos/components/time-picker';",
" import { PickerShell } from '$uix/eidos/components/picker-shell';",
" import { Time, type TimeValue } from '$libs/days';",
'',
' let value = $state<TimeValue | undefined>(new Time(9, 30, 0));',
@ -186,17 +188,19 @@
' </TimePicker.Input>',
' <TimePicker.Portal>',
' <TimePicker.Content>',
' <TimePicker.Clock>',
' <TimePicker.HourSlider />',
' <TimePicker.MinuteSlider />',
granularity === 'second' && ' <TimePicker.SecondSlider />',
hourCycle === 12 && ' <TimePicker.DayPeriodToggle />',
showFooter && (showClear || showCancel || showClose) && ' <TimePicker.Footer>',
showFooter && showClear && ' <TimePicker.Clear />',
showFooter && showCancel && ' <TimePicker.Cancel />',
showFooter && showClose && ' <TimePicker.Close />',
showFooter && (showClear || showCancel || showClose) && ' </TimePicker.Footer>',
' </TimePicker.Clock>',
' <PickerShell.Body>',
' <TimePicker.Clock>',
' <TimePicker.HourSlider />',
' <TimePicker.MinuteSlider />',
granularity === 'second' && ' <TimePicker.SecondSlider />',
hourCycle === 12 && ' <TimePicker.DayPeriodToggle />',
' </TimePicker.Clock>',
' </PickerShell.Body>',
showFooter && (showClear || showCancel || showClose) && ' <TimePicker.Footer>',
showFooter && showClear && ' <TimePicker.Clear />',
showFooter && showCancel && ' <TimePicker.Cancel />',
showFooter && showClose && ' <TimePicker.Close />',
showFooter && (showClear || showCancel || showClose) && ' </TimePicker.Footer>',
' </TimePicker.Content>',
' </TimePicker.Portal>',
'</TimePicker>'
@ -259,24 +263,26 @@
<TimePicker.Portal>
<TimePicker.Content>
<TimePicker.Clock>
<TimePicker.HourSlider />
<TimePicker.MinuteSlider />
{#if granularity === 'second'}
<TimePicker.SecondSlider />
{/if}
{#if hourCycle === 12}
<TimePicker.DayPeriodToggle />
{/if}
<PickerShell.Body>
<TimePicker.Clock>
<TimePicker.HourSlider />
<TimePicker.MinuteSlider />
{#if granularity === 'second'}
<TimePicker.SecondSlider />
{/if}
{#if hourCycle === 12}
<TimePicker.DayPeriodToggle />
{/if}
</TimePicker.Clock>
</PickerShell.Body>
{#if showFooter && (showClear || showCancel || showClose)}
<TimePicker.Footer>
{#if showClear}<TimePicker.Clear />{/if}
{#if showCancel}<TimePicker.Cancel />{/if}
{#if showClose}<TimePicker.Close />{/if}
</TimePicker.Footer>
{/if}
</TimePicker.Clock>
{#if showFooter && (showClear || showCancel || showClose)}
<TimePicker.Footer>
{#if showClear}<TimePicker.Clear />{/if}
{#if showCancel}<TimePicker.Cancel />{/if}
{#if showClose}<TimePicker.Close />{/if}
</TimePicker.Footer>
{/if}
</TimePicker.Content>
</TimePicker.Portal>
</TimePicker>

@ -6,6 +6,7 @@
type TimeRangePickerVariant,
type InputSnippetProps
} from '$uix/eidos/components/time-range-picker';
import { PickerShell } from '$uix/eidos/components/picker-shell';
import { compileMorfo } from '$uix/morfo';
import { timeRangePickerMorfo } from '@/uix/morfo/components/time-range-picker';
import { timeRangeFieldMorfo } from '@/uix/morfo/components/time-range-field';
@ -170,6 +171,7 @@
[
"<script lang='ts'>",
" import { TimeRangePicker } from '$uix/eidos/components/time-range-picker';",
" import { PickerShell } from '$uix/eidos/components/picker-shell';",
" import { Time, type TimeRange } from '$libs/days';",
'',
' let value = $state<TimeRange | undefined>({',
@ -211,31 +213,29 @@
' </TimeRangePicker.Input>',
' <TimeRangePicker.Portal>',
' <TimeRangePicker.Content>',
' <TimeRangePicker.Clock>',
' <div class="time-range-picker-endpoint">',
' <span class="time-range-picker-endpoint-label">Start</span>',
' <TimeRangePicker.HourSlider type="start" />',
' <TimeRangePicker.MinuteSlider type="start" />',
granularity === 'second' && ' <TimeRangePicker.SecondSlider type="start" />',
hourCycle === 12 && ' <TimeRangePicker.DayPeriodToggle type="start" />',
' </div>',
' <div class="time-range-picker-endpoint">',
' <span class="time-range-picker-endpoint-label">End</span>',
' <TimeRangePicker.HourSlider type="end" />',
' <TimeRangePicker.MinuteSlider type="end" />',
granularity === 'second' && ' <TimeRangePicker.SecondSlider type="end" />',
hourCycle === 12 && ' <TimeRangePicker.DayPeriodToggle type="end" />',
' </div>',
showFooter &&
(showClear || showCancel || showClose) &&
' <TimeRangePicker.Footer>',
showFooter && showClear && ' <TimeRangePicker.Clear />',
showFooter && showCancel && ' <TimeRangePicker.Cancel />',
showFooter && showClose && ' <TimeRangePicker.Close />',
showFooter &&
(showClear || showCancel || showClose) &&
' </TimeRangePicker.Footer>',
' </TimeRangePicker.Clock>',
' <PickerShell.Body>',
' <TimeRangePicker.Clock>',
' <div class="time-range-picker-endpoint">',
' <span class="time-range-picker-endpoint-label">Start</span>',
' <TimeRangePicker.HourSlider type="start" />',
' <TimeRangePicker.MinuteSlider type="start" />',
granularity === 'second' && ' <TimeRangePicker.SecondSlider type="start" />',
hourCycle === 12 && ' <TimeRangePicker.DayPeriodToggle type="start" />',
' </div>',
' <div class="time-range-picker-endpoint">',
' <span class="time-range-picker-endpoint-label">End</span>',
' <TimeRangePicker.HourSlider type="end" />',
' <TimeRangePicker.MinuteSlider type="end" />',
granularity === 'second' && ' <TimeRangePicker.SecondSlider type="end" />',
hourCycle === 12 && ' <TimeRangePicker.DayPeriodToggle type="end" />',
' </div>',
' </TimeRangePicker.Clock>',
' </PickerShell.Body>',
showFooter && (showClear || showCancel || showClose) && ' <TimeRangePicker.Footer>',
showFooter && showClear && ' <TimeRangePicker.Clear />',
showFooter && showCancel && ' <TimeRangePicker.Cancel />',
showFooter && showClose && ' <TimeRangePicker.Close />',
showFooter && (showClear || showCancel || showClose) && ' </TimeRangePicker.Footer>',
' </TimeRangePicker.Content>',
' </TimeRangePicker.Portal>',
'</TimeRangePicker>'
@ -308,38 +308,40 @@
<TimeRangePicker.Portal>
<TimeRangePicker.Content>
<TimeRangePicker.Clock>
<div class="time-range-picker-endpoint">
<span class="time-range-picker-endpoint-label">Start</span>
<TimeRangePicker.HourSlider type="start" />
<TimeRangePicker.MinuteSlider type="start" />
{#if granularity === 'second'}
<TimeRangePicker.SecondSlider type="start" />
{/if}
{#if hourCycle === 12}
<TimeRangePicker.DayPeriodToggle type="start" />
{/if}
</div>
<div class="time-range-picker-endpoint">
<span class="time-range-picker-endpoint-label">End</span>
<TimeRangePicker.HourSlider type="end" />
<TimeRangePicker.MinuteSlider type="end" />
{#if granularity === 'second'}
<TimeRangePicker.SecondSlider type="end" />
{/if}
{#if hourCycle === 12}
<TimeRangePicker.DayPeriodToggle type="end" />
{/if}
</div>
<PickerShell.Body>
<TimeRangePicker.Clock>
<div class="time-range-picker-endpoint">
<span class="time-range-picker-endpoint-label">Start</span>
<TimeRangePicker.HourSlider type="start" />
<TimeRangePicker.MinuteSlider type="start" />
{#if granularity === 'second'}
<TimeRangePicker.SecondSlider type="start" />
{/if}
{#if hourCycle === 12}
<TimeRangePicker.DayPeriodToggle type="start" />
{/if}
</div>
<div class="time-range-picker-endpoint">
<span class="time-range-picker-endpoint-label">End</span>
<TimeRangePicker.HourSlider type="end" />
<TimeRangePicker.MinuteSlider type="end" />
{#if granularity === 'second'}
<TimeRangePicker.SecondSlider type="end" />
{/if}
{#if hourCycle === 12}
<TimeRangePicker.DayPeriodToggle type="end" />
{/if}
</div>
</TimeRangePicker.Clock>
</PickerShell.Body>
{#if showFooter && (showClear || showCancel || showClose)}
<TimeRangePicker.Footer>
{#if showClear}<TimeRangePicker.Clear />{/if}
{#if showCancel}<TimeRangePicker.Cancel />{/if}
{#if showClose}<TimeRangePicker.Close />{/if}
</TimeRangePicker.Footer>
{/if}
</TimeRangePicker.Clock>
{#if showFooter && (showClear || showCancel || showClose)}
<TimeRangePicker.Footer>
{#if showClear}<TimeRangePicker.Clear />{/if}
{#if showCancel}<TimeRangePicker.Cancel />{/if}
{#if showClose}<TimeRangePicker.Close />{/if}
</TimeRangePicker.Footer>
{/if}
</TimeRangePicker.Content>
</TimeRangePicker.Portal>
</TimeRangePicker>

Loading…
Cancel
Save

Powered by TurnKey Linux.