- Month/year choosers replace the native `<select>` with a NON-MODAL popover whose
content is the reused `MonthGrid` / `YearGrid`. Stays non-modal so it also works
opened inside the date-picker's own popover (no nested modal trap). The viewed
month/year is highlighted with the selected style; `locale` is threaded and the
month is capitalized so es ("ene") and en ("Jan") read consistently. `YearGrid`
gains a `pageAligned` opt so the chooser can start on the selected year.
- Prev / Next compose the system `<IconButton>` (chevron glyph from the primitive);
the month / year triggers compose the system `<Button>` via the Popover.Trigger
`child` snippet. The bespoke border / bg / hover / focus / disabled chrome is
removed from `calendar.css` — it all comes from the primitive now; the trigger
label tracks the picker font (16px).
- The day grid is centered under the slightly wider header.
- The date-picker reuses these calendar parts, so it inherits all of the above.
WIP (parked, not composed anywhere): the soma `CalendarProvider` also carries an
unused `view` state + `Calendar.MonthView` / `YearView` / `HeadingTrigger` from an
in-place view-switch attempt. The popover approach above is the active one.
Excludes words/palabras/chronos.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
active-uix
parent
36770da202
commit
f950973b03
@ -0,0 +1,28 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Eidos `<Calendar.HeadingTrigger>` — the view-switch heading button. Opt-in
|
||||||
|
* alternative to the static `<Calendar.Heading>`: wraps it in a button that,
|
||||||
|
* on click, drills UP the navigation view (day → month → year), reusing the
|
||||||
|
* `MonthGrid` / `YearGrid` rendered by `<Calendar.MonthView>` /
|
||||||
|
* `<Calendar.YearView>`. Use it only when those views are composed.
|
||||||
|
*
|
||||||
|
* It nests the soma `<Calendar.Heading>` so the label, id registration and
|
||||||
|
* `aria-labelledby` wiring for the grid keep working unchanged.
|
||||||
|
*/
|
||||||
|
import type { HTMLButtonAttributes } from 'svelte/elements';
|
||||||
|
import * as Calendar from '$soma/components/calendar';
|
||||||
|
import { CalendarProvider } from '$soma/components/calendar';
|
||||||
|
|
||||||
|
let { ...rest }: HTMLButtonAttributes = $props();
|
||||||
|
|
||||||
|
const provider = CalendarProvider.require();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-calendar-heading-trigger
|
||||||
|
onclick={() => provider.advanceView()}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<Calendar.Heading />
|
||||||
|
</button>
|
||||||
@ -1,16 +1,64 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as Calendar from '$soma/components/calendar';
|
/**
|
||||||
import type { CalendarMonthSelectProps, CalendarMonthSelectSnippetProps } from './types';
|
* Eidos `<Calendar.MonthSelect>` — month chooser. A NON-MODAL popover whose
|
||||||
|
* trigger shows the current month + a chevron and whose content is the reused
|
||||||
|
* `MonthGrid` (12 month cells). Picking a month sets the calendar month and
|
||||||
|
* closes. Replaces the former native `<select>`; staying non-modal means it
|
||||||
|
* also works opened inside the date-picker's own popover (no nested trap).
|
||||||
|
*
|
||||||
|
* No `value` is passed to the grid — a controlled value would make the grid
|
||||||
|
* fire a select on mount and slam the popover shut as it opens.
|
||||||
|
*/
|
||||||
|
import { CalendarProvider } from '$soma/components/calendar';
|
||||||
|
import { ChevronDown } from '$uix/eidos/components/icon';
|
||||||
|
import { Button } from '$uix/eidos/components/button';
|
||||||
|
import Popover from '../popover';
|
||||||
|
import MonthGrid from '../month-grid';
|
||||||
|
import type { MonthValue, MonthGridProviderSnippetProps } from '$soma/components/month-grid';
|
||||||
|
|
||||||
let { children: bodyContent, ...rest }: CalendarMonthSelectProps = $props();
|
const provider = CalendarProvider.require();
|
||||||
|
let open = $state(false);
|
||||||
|
|
||||||
|
const placeholderYear = $derived(provider.opts.placeholder.current.year);
|
||||||
|
const placeholderMonth = $derived(provider.opts.placeholder.current.month);
|
||||||
|
// Abbreviated month (matches the grid) so the trigger stays compact and never
|
||||||
|
// grows the header.
|
||||||
|
const monthLabel = $derived(
|
||||||
|
new Intl.DateTimeFormat(provider.effectiveLocale, { month: 'short' }).format(
|
||||||
|
new Date(2000, placeholderMonth - 1, 1)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const placeholder = $derived<MonthValue>({ year: placeholderYear, month: placeholderMonth });
|
||||||
|
// Highlight the viewed month with the SELECTED style (filled), like the grid.
|
||||||
|
const value = $derived<MonthValue>({ year: placeholderYear, month: placeholderMonth });
|
||||||
|
|
||||||
|
function onValueChange(next: MonthValue | undefined): void {
|
||||||
|
if (!next) return;
|
||||||
|
provider.setMonth(next.month);
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if bodyContent}
|
<Popover bind:open modal={false}>
|
||||||
<Calendar.MonthSelect {...rest}>
|
<Popover.Trigger data-calendar-month-select>
|
||||||
{#snippet children(snippetProps: CalendarMonthSelectSnippetProps)}
|
{#snippet child({ props }: { props: Record<string, unknown> })}
|
||||||
{@render bodyContent(snippetProps)}
|
<Button {...props} variant="ghost" size="sm">
|
||||||
|
<span data-calendar-select-label>{monthLabel}</span>
|
||||||
|
<ChevronDown data-calendar-select-chevron aria-hidden="true" />
|
||||||
|
</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Calendar.MonthSelect>
|
</Popover.Trigger>
|
||||||
{:else}
|
<Popover.Portal>
|
||||||
<Calendar.MonthSelect {...rest} />
|
<Popover.Content data-calendar-select-popover>
|
||||||
{/if}
|
<MonthGrid {placeholder} {value} locale={provider.effectiveLocale} {onValueChange}>
|
||||||
|
{#snippet children({ months }: MonthGridProviderSnippetProps)}
|
||||||
|
<MonthGrid.Grid>
|
||||||
|
{#each months as month (month)}
|
||||||
|
<MonthGrid.Cell {month} />
|
||||||
|
{/each}
|
||||||
|
</MonthGrid.Grid>
|
||||||
|
{/snippet}
|
||||||
|
</MonthGrid>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Portal>
|
||||||
|
</Popover>
|
||||||
|
|||||||
@ -1,10 +1,30 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Eidos `Calendar.NextButton` — composes the system `<IconButton>` (which itself
|
||||||
|
* composes `<Button>`) via soma's `child` snippet, instead of styling a bespoke
|
||||||
|
* native `<button>`. The forward-chevron glyph plus sizing / hover / focus /
|
||||||
|
* disabled / a11y all come from the primitive — no duplicated chrome.
|
||||||
|
*/
|
||||||
|
import { ActiveEidos } from '$uix/eidos';
|
||||||
import * as Calendar from '$soma/components/calendar';
|
import * as Calendar from '$soma/components/calendar';
|
||||||
|
import { IconButton } from '$uix/eidos/components/icon-button';
|
||||||
|
import { ChevronRight } from '$uix/eidos/components/icon';
|
||||||
import type { CalendarNextButtonProps } from './types';
|
import type { CalendarNextButtonProps } from './types';
|
||||||
|
|
||||||
let { children: bodyContent, ...rest }: CalendarNextButtonProps = $props();
|
let {
|
||||||
|
'aria-label': ariaLabel,
|
||||||
|
children: bodyContent,
|
||||||
|
...rest
|
||||||
|
}: CalendarNextButtonProps = $props();
|
||||||
|
|
||||||
|
const eidos = ActiveEidos.require();
|
||||||
|
const label = $derived(ariaLabel ?? eidos.langs.ts('#?common.calendar.next-month|Next month'));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Calendar.NextButton {...rest}>
|
<Calendar.NextButton {...rest}>
|
||||||
{@render bodyContent?.()}
|
{#snippet child({ props }: { props: Record<string, unknown> })}
|
||||||
|
<IconButton {...props} variant="ghost" size="sm" aria-label={label}>
|
||||||
|
{#if bodyContent}{@render bodyContent()}{:else}<ChevronRight />{/if}
|
||||||
|
</IconButton>
|
||||||
|
{/snippet}
|
||||||
</Calendar.NextButton>
|
</Calendar.NextButton>
|
||||||
|
|||||||
@ -1,10 +1,30 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Eidos `Calendar.PrevButton` — composes the system `<IconButton>` (which itself
|
||||||
|
* composes `<Button>`) via soma's `child` snippet, instead of styling a bespoke
|
||||||
|
* native `<button>`. The back-chevron glyph plus sizing / hover / focus /
|
||||||
|
* disabled / a11y all come from the primitive — no duplicated chrome.
|
||||||
|
*/
|
||||||
|
import { ActiveEidos } from '$uix/eidos';
|
||||||
import * as Calendar from '$soma/components/calendar';
|
import * as Calendar from '$soma/components/calendar';
|
||||||
|
import { IconButton } from '$uix/eidos/components/icon-button';
|
||||||
|
import { ChevronLeft } from '$uix/eidos/components/icon';
|
||||||
import type { CalendarPrevButtonProps } from './types';
|
import type { CalendarPrevButtonProps } from './types';
|
||||||
|
|
||||||
let { children: bodyContent, ...rest }: CalendarPrevButtonProps = $props();
|
let {
|
||||||
|
'aria-label': ariaLabel,
|
||||||
|
children: bodyContent,
|
||||||
|
...rest
|
||||||
|
}: CalendarPrevButtonProps = $props();
|
||||||
|
|
||||||
|
const eidos = ActiveEidos.require();
|
||||||
|
const label = $derived(ariaLabel ?? eidos.langs.ts('#?common.calendar.prev-month|Previous month'));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Calendar.PrevButton {...rest}>
|
<Calendar.PrevButton {...rest}>
|
||||||
{@render bodyContent?.()}
|
{#snippet child({ props }: { props: Record<string, unknown> })}
|
||||||
|
<IconButton {...props} variant="ghost" size="sm" aria-label={label}>
|
||||||
|
{#if bodyContent}{@render bodyContent()}{:else}<ChevronLeft />{/if}
|
||||||
|
</IconButton>
|
||||||
|
{/snippet}
|
||||||
</Calendar.PrevButton>
|
</Calendar.PrevButton>
|
||||||
|
|||||||
@ -1,16 +1,80 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as Calendar from '$soma/components/calendar';
|
/**
|
||||||
import type { CalendarYearSelectProps, CalendarYearSelectSnippetProps } from './types';
|
* Eidos `<Calendar.YearSelect>` — year chooser. A NON-MODAL popover whose
|
||||||
|
* trigger shows the current year + a chevron and whose content is the reused
|
||||||
|
* `YearGrid` (a decade page with prev/next). Picking a year sets the calendar
|
||||||
|
* year and closes. Replaces the former native `<select>`.
|
||||||
|
*
|
||||||
|
* The decade browsing drives a LOCAL nav-year (re-seeded from the calendar each
|
||||||
|
* time the popover opens) instead of syncing back into the calendar — that
|
||||||
|
* decoupling avoids a placeholder feedback loop. No `value` is passed so the
|
||||||
|
* grid never fires a select on mount and slams the popover shut.
|
||||||
|
*/
|
||||||
|
import { CalendarProvider } from '$soma/components/calendar';
|
||||||
|
import { ChevronDown, ChevronLeft, ChevronRight } from '$uix/eidos/components/icon';
|
||||||
|
import { Button } from '$uix/eidos/components/button';
|
||||||
|
import Popover from '../popover';
|
||||||
|
import YearGrid from '../year-grid';
|
||||||
|
import type { YearValue, YearPlaceholder, YearGridProviderSnippetProps } from '$soma/components/year-grid';
|
||||||
|
|
||||||
let { children: bodyContent, ...rest }: CalendarYearSelectProps = $props();
|
const provider = CalendarProvider.require();
|
||||||
|
let open = $state(false);
|
||||||
|
|
||||||
|
const selectedYear = $derived(provider.opts.placeholder.current.year);
|
||||||
|
// Local decade browsing — re-seeded from the calendar on each open so the
|
||||||
|
// page starts on the selected year.
|
||||||
|
let navYear = $state(provider.opts.placeholder.current.year);
|
||||||
|
$effect(() => {
|
||||||
|
if (open) navYear = provider.opts.placeholder.current.year;
|
||||||
|
});
|
||||||
|
|
||||||
|
const yearLabel = $derived(provider.formatYear(selectedYear));
|
||||||
|
const placeholder = $derived<YearPlaceholder>({ year: navYear });
|
||||||
|
// Highlight the selected year with the SELECTED (filled) style.
|
||||||
|
const value = $derived<YearValue>({ year: selectedYear });
|
||||||
|
|
||||||
|
function onPlaceholderChange(next: YearPlaceholder): void {
|
||||||
|
navYear = next.year;
|
||||||
|
}
|
||||||
|
function onValueChange(next: YearValue | undefined): void {
|
||||||
|
if (!next) return;
|
||||||
|
provider.setYear(next.year);
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if bodyContent}
|
<Popover bind:open modal={false}>
|
||||||
<Calendar.YearSelect {...rest}>
|
<Popover.Trigger data-calendar-year-select>
|
||||||
{#snippet children(snippetProps: CalendarYearSelectSnippetProps)}
|
{#snippet child({ props }: { props: Record<string, unknown> })}
|
||||||
{@render bodyContent(snippetProps)}
|
<Button {...props} variant="ghost" size="sm">
|
||||||
|
<span data-calendar-select-label>{yearLabel}</span>
|
||||||
|
<ChevronDown data-calendar-select-chevron aria-hidden="true" />
|
||||||
|
</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Calendar.YearSelect>
|
</Popover.Trigger>
|
||||||
{:else}
|
<Popover.Portal>
|
||||||
<Calendar.YearSelect {...rest} />
|
<Popover.Content data-calendar-select-popover>
|
||||||
{/if}
|
<YearGrid
|
||||||
|
{placeholder}
|
||||||
|
{value}
|
||||||
|
pageAligned={false}
|
||||||
|
locale={provider.effectiveLocale}
|
||||||
|
{onPlaceholderChange}
|
||||||
|
{onValueChange}
|
||||||
|
>
|
||||||
|
{#snippet children({ years }: YearGridProviderSnippetProps)}
|
||||||
|
<YearGrid.Header>
|
||||||
|
<YearGrid.PrevButton><ChevronLeft aria-hidden="true" /></YearGrid.PrevButton>
|
||||||
|
<YearGrid.Heading />
|
||||||
|
<YearGrid.NextButton><ChevronRight aria-hidden="true" /></YearGrid.NextButton>
|
||||||
|
</YearGrid.Header>
|
||||||
|
<YearGrid.Grid>
|
||||||
|
{#each years as year (year)}
|
||||||
|
<YearGrid.Cell {year} />
|
||||||
|
{/each}
|
||||||
|
</YearGrid.Grid>
|
||||||
|
{/snippet}
|
||||||
|
</YearGrid>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Portal>
|
||||||
|
</Popover>
|
||||||
|
|||||||
Loading…
Reference in new issue