Calendar chrome: - Shared CalendarNav context — Calendar/RangeCalendar/DateRangePicker reuse the refined month/year selects + prev/next buttons; deleted the range pickers' bespoke month/year selects. New calendar-nav-button, calendar-select.css, visual-context, soma calendar-nav. TimePicker (datetime building blocks): - DayPeriodToggle composes the system ToggleGroup (was native radios): type=single, deselectable=false, solid/primary, right-aligned. - Sliders self-hide per granularity (hour -> only hour); ruler -> showScales; slider labels follow the canonical label scale per size. - Active-toggle hover fixes: pin bg to the resting fill (no wash) + archetypes hover rule excludes data-state=on (it was flipping a pressed toggle's text to content-primary / black in light themes). - DatePicker demo: locale-derived hour cycle + composed TimePicker time row. Perf (audit A31): lifted selection/checked sets ($derived Set, O(1) .has()) across checkbox, combobox, grid-list, listbox, select, tag-group, toggle-group, tree-grid, tree-view providers. Popover: fix trigger re-click bounce (exclude trigger node in onInteractOutside). langs: picker-shell footer strings. CONTINUE.md hand-off for the next-session DateTimePicker refactor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>active-uix
parent
25ce77c142
commit
92f988e7e7
@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Shared visual for a calendar header navigation button (prev / next). Composes
|
||||
* the system `<IconButton>` with a back / forward chevron, so EVERY
|
||||
* calendar-flavoured component's prev/next renders the same chrome — each part
|
||||
* only wires its own soma button (for the morfo/sema behaviour) and hands the
|
||||
* `child` props through here.
|
||||
*/
|
||||
import type { Snippet } from 'svelte';
|
||||
import { IconButton } from '$uix/eidos/components/icon-button';
|
||||
import { ChevronLeft, ChevronRight } from '$uix/eidos/components/icon';
|
||||
|
||||
let {
|
||||
props,
|
||||
label,
|
||||
direction,
|
||||
children
|
||||
}: {
|
||||
props: Record<string, unknown>;
|
||||
label: string;
|
||||
direction: 'prev' | 'next';
|
||||
children?: Snippet;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<IconButton {...props} variant="ghost" size="sm" aria-label={label}>
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else if direction === 'prev'}
|
||||
<ChevronLeft />
|
||||
{:else}
|
||||
<ChevronRight />
|
||||
{/if}
|
||||
</IconButton>
|
||||
@ -0,0 +1,45 @@
|
||||
/* Header month / year SELECTORS — shared chrome for every calendar-flavoured
|
||||
component (Calendar, DatePicker, RangeCalendar, DateRangePicker). Imported by
|
||||
the selector components themselves so the styling travels with them instead of
|
||||
living on the [data-calendar] host (which range/date-range never render). */
|
||||
|
||||
/* The trigger Button's own font scale is coarser than the calendar's (its xs is
|
||||
14px while the calendar's is 12px), which left the button box sized for a
|
||||
bigger font than the label showed (the size incoherence). Pin the Button font
|
||||
to the calendar font so the box, label and chevron read as ONE size. */
|
||||
[data-calendar-month-select][data-button],
|
||||
[data-calendar-year-select][data-button] {
|
||||
font-size: var(--_calendar-font-size, var(--calendar-font-size-md));
|
||||
}
|
||||
|
||||
[data-calendar-select-label] {
|
||||
/* Bump the trigger label to the host's type size (the Button's own sm font is
|
||||
smaller). Falls back to the md token outside a [data-calendar] host so the
|
||||
shared selector still reads correctly in range / date-range. */
|
||||
font-size: var(--_calendar-font-size, var(--calendar-font-size-md));
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-calendar-select-chevron] {
|
||||
flex: 0 0 auto;
|
||||
inline-size: var(--_calendar-font-size, var(--calendar-font-size-md));
|
||||
block-size: var(--_calendar-font-size, var(--calendar-font-size-md));
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* The grid sits inside the popover's own surface — flatten its standalone
|
||||
surface (border / bg / shadow / outer padding) so the panel isn't doubled. */
|
||||
[data-calendar-select-popover] :is([data-month-grid], [data-year-grid]) {
|
||||
border: none;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Spanish (and other locales') Intl month abbreviations come back lowercase
|
||||
("ene", "feb"); capitalize so the month reads the same regardless of locale —
|
||||
matching the English "Jan" / "Feb". */
|
||||
[data-calendar-month-select],
|
||||
[data-calendar-select-popover] [data-month-grid-cell] {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
import { getContext, setContext } from 'svelte';
|
||||
import type { CalendarSize } from './types';
|
||||
|
||||
/**
|
||||
* Shared visual context for the calendar's overlay choosers. The month / year
|
||||
* select popovers reuse `MonthGrid` / `YearGrid`, which must render at the SAME
|
||||
* size as the calendar (picker) that hosts them. The calendar root — and the
|
||||
* date-picker's calendar — publish their resolved size here so those grids
|
||||
* inherit it instead of defaulting to `md`.
|
||||
*/
|
||||
const CALENDAR_VISUAL = Symbol('calendar-visual');
|
||||
|
||||
export interface CalendarVisual {
|
||||
getSize(): CalendarSize;
|
||||
}
|
||||
|
||||
export function setCalendarVisual(ctx: CalendarVisual): CalendarVisual {
|
||||
setContext(CALENDAR_VISUAL, ctx);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export function getCalendarVisual(): CalendarVisual | undefined {
|
||||
return getContext<CalendarVisual | undefined>(CALENDAR_VISUAL);
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
<script lang="ts">
|
||||
import * as DateRangePicker from '$soma/components/date-range-picker';
|
||||
import type {
|
||||
DateRangePickerMonthSelectProps,
|
||||
DateRangePickerMonthSelectSnippetProps
|
||||
} from './types';
|
||||
|
||||
let { children: bodyContent, ...rest }: DateRangePickerMonthSelectProps = $props();
|
||||
</script>
|
||||
|
||||
{#if bodyContent}
|
||||
<DateRangePicker.MonthSelect {...rest}>
|
||||
{#snippet children(snippetProps: DateRangePickerMonthSelectSnippetProps)}
|
||||
{@render bodyContent(snippetProps)}
|
||||
{/snippet}
|
||||
</DateRangePicker.MonthSelect>
|
||||
{:else}
|
||||
<DateRangePicker.MonthSelect {...rest} />
|
||||
{/if}
|
||||
@ -1,10 +1,26 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Eidos `DateRangePicker.NextButton` — wires soma's next button to the SHARED
|
||||
* `<CalendarNavButton>` (system IconButton + forward chevron), the same chrome
|
||||
* Calendar uses, instead of a bespoke native button.
|
||||
*/
|
||||
import { ActiveEidos } from '$uix/eidos';
|
||||
import * as DateRangePicker from '$soma/components/date-range-picker';
|
||||
import CalendarNavButton from '../calendar/calendar-nav-button.svelte';
|
||||
import type { DateRangePickerNextButtonProps } from './types';
|
||||
|
||||
let { children, ...rest }: DateRangePickerNextButtonProps = $props();
|
||||
let {
|
||||
'aria-label': ariaLabel,
|
||||
children: bodyContent,
|
||||
...rest
|
||||
}: DateRangePickerNextButtonProps = $props();
|
||||
|
||||
const eidos = ActiveEidos.require();
|
||||
const label = $derived(ariaLabel ?? eidos.langs.ts('#?common.calendar.next-month|Next month'));
|
||||
</script>
|
||||
|
||||
<DateRangePicker.NextButton {...rest}>
|
||||
{@render children?.()}
|
||||
{#snippet child({ props }: { props: Record<string, unknown> })}
|
||||
<CalendarNavButton {props} {label} direction="next" children={bodyContent} />
|
||||
{/snippet}
|
||||
</DateRangePicker.NextButton>
|
||||
|
||||
@ -1,10 +1,26 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Eidos `DateRangePicker.PrevButton` — wires soma's prev button to the SHARED
|
||||
* `<CalendarNavButton>` (system IconButton + back chevron), the same chrome
|
||||
* Calendar uses, instead of a bespoke native button.
|
||||
*/
|
||||
import { ActiveEidos } from '$uix/eidos';
|
||||
import * as DateRangePicker from '$soma/components/date-range-picker';
|
||||
import CalendarNavButton from '../calendar/calendar-nav-button.svelte';
|
||||
import type { DateRangePickerPrevButtonProps } from './types';
|
||||
|
||||
let { children, ...rest }: DateRangePickerPrevButtonProps = $props();
|
||||
let {
|
||||
'aria-label': ariaLabel,
|
||||
children: bodyContent,
|
||||
...rest
|
||||
}: DateRangePickerPrevButtonProps = $props();
|
||||
|
||||
const eidos = ActiveEidos.require();
|
||||
const label = $derived(ariaLabel ?? eidos.langs.ts('#?common.calendar.prev-month|Previous month'));
|
||||
</script>
|
||||
|
||||
<DateRangePicker.PrevButton {...rest}>
|
||||
{@render children?.()}
|
||||
{#snippet child({ props }: { props: Record<string, unknown> })}
|
||||
<CalendarNavButton {props} {label} direction="prev" children={bodyContent} />
|
||||
{/snippet}
|
||||
</DateRangePicker.PrevButton>
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
<script lang="ts">
|
||||
import * as DateRangePicker from '$soma/components/date-range-picker';
|
||||
import type {
|
||||
DateRangePickerYearSelectProps,
|
||||
DateRangePickerYearSelectSnippetProps
|
||||
} from './types';
|
||||
|
||||
let { children: bodyContent, ...rest }: DateRangePickerYearSelectProps = $props();
|
||||
</script>
|
||||
|
||||
{#if bodyContent}
|
||||
<DateRangePicker.YearSelect {...rest}>
|
||||
{#snippet children(snippetProps: DateRangePickerYearSelectSnippetProps)}
|
||||
{@render bodyContent(snippetProps)}
|
||||
{/snippet}
|
||||
</DateRangePicker.YearSelect>
|
||||
{:else}
|
||||
<DateRangePicker.YearSelect {...rest} />
|
||||
{/if}
|
||||
@ -1,19 +0,0 @@
|
||||
<script lang="ts">
|
||||
import * as RangeCalendar from '$soma/components/range-calendar';
|
||||
import type {
|
||||
RangeCalendarMonthSelectProps,
|
||||
RangeCalendarMonthSelectSnippetProps
|
||||
} from './types';
|
||||
|
||||
let { children: bodyContent, ...rest }: RangeCalendarMonthSelectProps = $props();
|
||||
</script>
|
||||
|
||||
{#if bodyContent}
|
||||
<RangeCalendar.MonthSelect {...rest}>
|
||||
{#snippet children(snippetProps: RangeCalendarMonthSelectSnippetProps)}
|
||||
{@render bodyContent(snippetProps)}
|
||||
{/snippet}
|
||||
</RangeCalendar.MonthSelect>
|
||||
{:else}
|
||||
<RangeCalendar.MonthSelect {...rest} />
|
||||
{/if}
|
||||
@ -1,10 +1,26 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Eidos `RangeCalendar.NextButton` — wires soma's next button to the SHARED
|
||||
* `<CalendarNavButton>` (system <IconButton> + forward chevron) — the same
|
||||
* chrome Calendar uses, instead of a bespoke native button.
|
||||
*/
|
||||
import { ActiveEidos } from '$uix/eidos';
|
||||
import * as RangeCalendar from '$soma/components/range-calendar';
|
||||
import CalendarNavButton from '../calendar/calendar-nav-button.svelte';
|
||||
import type { RangeCalendarNextButtonProps } from './types';
|
||||
|
||||
let { children: bodyContent, ...rest }: RangeCalendarNextButtonProps = $props();
|
||||
let {
|
||||
'aria-label': ariaLabel,
|
||||
children: bodyContent,
|
||||
...rest
|
||||
}: RangeCalendarNextButtonProps = $props();
|
||||
|
||||
const eidos = ActiveEidos.require();
|
||||
const label = $derived(ariaLabel ?? eidos.langs.ts('#?common.calendar.next-month|Next month'));
|
||||
</script>
|
||||
|
||||
<RangeCalendar.NextButton {...rest}>
|
||||
{@render bodyContent?.()}
|
||||
{#snippet child({ props }: { props: Record<string, unknown> })}
|
||||
<CalendarNavButton {props} {label} direction="next" children={bodyContent} />
|
||||
{/snippet}
|
||||
</RangeCalendar.NextButton>
|
||||
|
||||
@ -1,10 +1,26 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Eidos `RangeCalendar.PrevButton` — wires soma's prev button to the SHARED
|
||||
* `<CalendarNavButton>` (system <IconButton> + back chevron) — the same chrome
|
||||
* Calendar uses, instead of a bespoke native button.
|
||||
*/
|
||||
import { ActiveEidos } from '$uix/eidos';
|
||||
import * as RangeCalendar from '$soma/components/range-calendar';
|
||||
import CalendarNavButton from '../calendar/calendar-nav-button.svelte';
|
||||
import type { RangeCalendarPrevButtonProps } from './types';
|
||||
|
||||
let { children: bodyContent, ...rest }: RangeCalendarPrevButtonProps = $props();
|
||||
let {
|
||||
'aria-label': ariaLabel,
|
||||
children: bodyContent,
|
||||
...rest
|
||||
}: RangeCalendarPrevButtonProps = $props();
|
||||
|
||||
const eidos = ActiveEidos.require();
|
||||
const label = $derived(ariaLabel ?? eidos.langs.ts('#?common.calendar.prev-month|Previous month'));
|
||||
</script>
|
||||
|
||||
<RangeCalendar.PrevButton {...rest}>
|
||||
{@render bodyContent?.()}
|
||||
{#snippet child({ props }: { props: Record<string, unknown> })}
|
||||
<CalendarNavButton {props} {label} direction="prev" children={bodyContent} />
|
||||
{/snippet}
|
||||
</RangeCalendar.PrevButton>
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
<script lang="ts">
|
||||
import * as RangeCalendar from '$soma/components/range-calendar';
|
||||
import type {
|
||||
RangeCalendarYearSelectProps,
|
||||
RangeCalendarYearSelectSnippetProps
|
||||
} from './types';
|
||||
|
||||
let { children: bodyContent, ...rest }: RangeCalendarYearSelectProps = $props();
|
||||
</script>
|
||||
|
||||
{#if bodyContent}
|
||||
<RangeCalendar.YearSelect {...rest}>
|
||||
{#snippet children(snippetProps: RangeCalendarYearSelectSnippetProps)}
|
||||
{@render bodyContent(snippetProps)}
|
||||
{/snippet}
|
||||
</RangeCalendar.YearSelect>
|
||||
{:else}
|
||||
<RangeCalendar.YearSelect {...rest} />
|
||||
{/if}
|
||||
@ -1,17 +1,45 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Eidos `<TimePicker.DayPeriodToggle>` — composes the system
|
||||
* `<ToggleGroup>` (single-select, segmented) wired to the picker's AM/PM
|
||||
* state, instead of soma's bespoke native radios. The soma part exposes the
|
||||
* value / setter / items via its `asChild` snippet; this wrapper paints them
|
||||
* with the framework component so the toggle matches the rest of the system.
|
||||
*/
|
||||
import * as TimePicker from '$soma/components/time-picker';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { ToggleGroup } from '$uix/eidos/components/toggle-group';
|
||||
import { getTimePickerVisualContext } from './context.svelte';
|
||||
|
||||
type Props = {
|
||||
id?: string;
|
||||
children?: Snippet;
|
||||
class?: string;
|
||||
style?: string;
|
||||
};
|
||||
|
||||
let { children, ...rest }: Props = $props();
|
||||
let { ...rest }: Props = $props();
|
||||
|
||||
const visual = getTimePickerVisualContext();
|
||||
</script>
|
||||
|
||||
<TimePicker.DayPeriodToggle {...rest}>
|
||||
{@render children?.()}
|
||||
{#snippet child({ props, value, setValue, disabled, items })}
|
||||
<ToggleGroup
|
||||
{...props}
|
||||
type="single"
|
||||
deselectable={false}
|
||||
value={[value]}
|
||||
onValueChange={(next) => {
|
||||
if (next[0]) setValue(next[0] as 'AM' | 'PM');
|
||||
}}
|
||||
{disabled}
|
||||
attached
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size={visual?.getSize() ?? 'md'}
|
||||
>
|
||||
{#each items as item (item.value)}
|
||||
<ToggleGroup.Item value={item.value}>{item.label}</ToggleGroup.Item>
|
||||
{/each}
|
||||
</ToggleGroup>
|
||||
{/snippet}
|
||||
</TimePicker.DayPeriodToggle>
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
import type { LangNode } from '$libs/langs';
|
||||
|
||||
/**
|
||||
* Default strings for the metrics component. Merged under `components.metrics.*`
|
||||
* by `ActiveUix` (via the `componentLangs` barrel).
|
||||
*
|
||||
* The trend phrases give the Delta an accessible name: the arrow glyph is
|
||||
* `aria-hidden`, so a screen reader reads "Increased by 12%" instead of an
|
||||
* undecipherable "▲ 12%". `{{value}}` interpolates the delta's value text.
|
||||
*/
|
||||
export const metricsLangs = {
|
||||
label: {
|
||||
es: 'Métrica',
|
||||
en: 'Metric'
|
||||
},
|
||||
'trend-up': {
|
||||
es: 'Aumentó {{value}}',
|
||||
en: 'Increased by {{value}}'
|
||||
},
|
||||
'trend-down': {
|
||||
es: 'Disminuyó {{value}}',
|
||||
en: 'Decreased by {{value}}'
|
||||
},
|
||||
'trend-flat': {
|
||||
es: 'Sin cambios',
|
||||
en: 'No change'
|
||||
}
|
||||
} satisfies LangNode;
|
||||
@ -0,0 +1,24 @@
|
||||
import type { LangNode } from '$libs/langs';
|
||||
|
||||
/**
|
||||
* Default strings for the picker-shell footer actions (Clear / Cancel / Done),
|
||||
* shared by every composite picker (Date / Time / Color pickers). Merged under
|
||||
* `components.picker-shell.*` by `ActiveUix` (via the `componentLangs` barrel).
|
||||
*
|
||||
* The eidos parts reference these by absolute idlangref:
|
||||
* `'#?components.picker-shell.{key}|fallback'`.
|
||||
*/
|
||||
export const pickerShellLangs = {
|
||||
clear: {
|
||||
es: 'Borrar',
|
||||
en: 'Clear'
|
||||
},
|
||||
cancel: {
|
||||
es: 'Cancelar',
|
||||
en: 'Cancel'
|
||||
},
|
||||
close: {
|
||||
es: 'Listo',
|
||||
en: 'Done'
|
||||
}
|
||||
} satisfies LangNode;
|
||||
@ -0,0 +1,30 @@
|
||||
import { context } from '../../provider';
|
||||
import type { DateValue } from '$libs/days';
|
||||
|
||||
/**
|
||||
* The minimal calendar-navigation surface the refined header chrome (the month
|
||||
* and year selectors, prev / next buttons) consumes. BOTH `CalendarProvider`
|
||||
* and `RangeCalendarProvider` implement it and publish themselves under
|
||||
* `calendarNavContext`, so a SINGLE set of eidos chrome parts drives the
|
||||
* Calendar, DatePicker, RangeCalendar and DateRangePicker — none re-implement
|
||||
* their own selectors/buttons.
|
||||
*
|
||||
* It deliberately omits the day-grid surface: range calendars genuinely differ
|
||||
* there (in-range / hover-preview), so the grid parts stay per component. Only
|
||||
* navigation chrome — which is identical between the two providers — is shared.
|
||||
*/
|
||||
export interface CalendarNav {
|
||||
/** Resolved locale used for month / year formatting and the grids. */
|
||||
readonly effectiveLocale: string;
|
||||
/** The currently displayed month (placeholder) the chrome reads + writes. */
|
||||
getPlaceholder(): DateValue;
|
||||
setMonth(month: number, target?: HTMLElement): void;
|
||||
setYear(year: number, target?: HTMLElement): void;
|
||||
formatYear(year: number): string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared context published by every calendar-flavoured provider. Refined chrome
|
||||
* reads it instead of a concrete provider, so it stays provider-agnostic.
|
||||
*/
|
||||
export const calendarNavContext = context<CalendarNav>('CalendarNav');
|
||||
Loading…
Reference in new issue