Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .specs/calendar.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ spec_version: 3
figma:
url: https://www.figma.com/design/t97pXRs7xME3SJDs5iZ5RF/Webkit?node-id=5087-17336
node_id: 5087:17336
checksum: bc6f6c9d9c59a6b729676a7b2ba8932cac7407d67d25fb2cd43da9e8e61b2018
checksum: 38ab752a76919d45a23184bfefdd6e9027c3b1cfe1acd23ee5178d54ef53db25
created: 2026-06-25
last_updated: 2026-07-02
last_updated: 2026-07-30
---

# Calendar — Component Spec

## Purpose

Date-range picker. A trigger button opens a popover that holds a month grid, Start/End date + time fields, an optional timezone selector, and an Apply action; it also offers a "Select Period" mode whose text input parses relative spans (`45m`, `12 hours`, `last month`, `yesterday`, `1/1 - 1/2`). When consumer-provided `presets` are supplied, the trigger splits into two side-by-side segments — a preset dropdown (left, `🕐 <label> ⌄`) that applies a preset range in one click, and the calendar (right, `📅 <range>`) that opens the grid for fine-tuning. Selection is staged in a draft inside the popover and committed to `v-model` only on Apply (or immediately when `show-apply` is false); a preset commits immediately. In period mode, once a span is applied the trigger shows the same two-segment shape — the relative token (e.g. `45m`) beside the concrete window it resolves to (e.g. `14:44 – 23:59`) — since a period selects a time interval rather than whole days. Date math uses the native `Date` API only — no date library; timezone is a selectable IANA label used for display formatting via `Intl` (the picker does not re-interpret `Date` objects across zones).
Date-range picker. A trigger button opens a popover that holds a month grid, Start/End date + time fields (suppressible with `show-fields`), an optional timezone selector, and an Apply action; it also offers a "Select Period" mode whose text input parses relative spans (`45m`, `12 hours`, `last month`, `yesterday`, `1/1 - 1/2`). When consumer-provided `presets` are supplied, the trigger splits into two side-by-side segments — a preset dropdown (left, `🕐 <label> ⌄`) that applies a preset range in one click, and the calendar (right, `📅 <range>`) that opens the grid for fine-tuning. Selection is staged in a draft inside the popover and committed to `v-model` only on Apply (or immediately when `show-apply` is false); a preset commits immediately. In period mode, once a span is applied the trigger shows the same two-segment shape — the relative token (e.g. `45m`) beside the concrete window it resolves to (e.g. `14:44 – 23:59`) — since a period selects a time interval rather than whole days. Date math uses the native `Date` API only — no date library; timezone is a selectable IANA label used for display formatting via `Intl` (the picker does not re-interpret `Date` objects across zones).

## Usage

Expand Down Expand Up @@ -93,6 +93,7 @@ import CalendarClear from '@aziontech/webkit/calendar-clear'
| `open` | `boolean \| undefined` | `undefined` | no | Controlled open state of the popover. Use with v-model:open; omit for uncontrolled. |
| `placeholder` | `string` | `'Select a Date Range'` | no | Trigger text shown when there is no selection. |
| `presets` | `CalendarPresetItem[]` | `[]` | no | Consumer-provided shortcuts; each is `{ label, value }` where value is a `Date` or range. When present, the trigger becomes a two-part control whose left segment opens a dropdown of these presets. |
| `showFields` | `boolean` | `true` | no | Shows the Start/End date fields in the panel. Set false when the shortcuts and the grid are the whole interaction and typing a boundary by hand is not offered. |
| `showTime` | `boolean` | `false` | no | Shows Start/End time fields alongside the date fields. |
| `showTimezone` | `boolean` | `false` | no | Shows the timezone selector below the fields. |
| `timezone` | `string` | `''` | no | Selected IANA timezone for display formatting (v-model:timezone). Empty resolves to the local zone. |
Expand Down
18 changes: 16 additions & 2 deletions packages/webkit/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -4721,6 +4721,13 @@
"required": "no",
"doc": "Consumer-provided shortcuts; each is `{ label, value }` where value is a `Date` or range. When present, the trigger becomes a two-part control whose left segment opens a dropdown of these presets."
},
{
"name": "showFields",
"type": "boolean",
"default": "true",
"required": "no",
"doc": "Shows the Start/End date fields in the panel. Set false when the shortcuts and the grid are the whole interaction and typing a boundary by hand is not offered."
},
{
"name": "showTime",
"type": "boolean",
Expand Down Expand Up @@ -4829,7 +4836,7 @@
"notes": "Custom footer/action-bar content (e.g. `<Calendar.Clear>`) alongside Apply."
}
],
"purpose": "Date-range picker. A trigger button opens a popover that holds a month grid, Start/End date + time fields, an optional timezone selector, and an Apply action; it also offers a \"Select Period\" mode whose text input parses relative spans (`45m`, `12 hours`, `last month`, `yesterday`, `1/1 - 1/2`). When consumer-provided `presets` are supplied, the trigger splits into two side-by-side segments — a preset dropdown (left, `🕐 <label> ⌄`) that applies a preset range in one click, and the calendar (right, `📅 <range>`) that opens the grid for fine-tuning. Selection is staged in a draft inside the popover and committed to `v-model` only on Apply (or immediately when `show-apply` is false); a preset commits immediately. In period mode, once a span is applied the trigger shows the same two-segment shape — the relative token (e.g. `45m`) beside the concrete window it resolves to (e.g. `14:44 – 23:59`) — since a period selects a time interval rather than whole days. Date math uses the native `Date` API only — no date library; timezone is a selectable IANA label used for display formatting via `Intl` (the picker does not re-interpret `Date` objects across zones)."
"purpose": "Date-range picker. A trigger button opens a popover that holds a month grid, Start/End date + time fields (suppressible with `show-fields`), an optional timezone selector, and an Apply action; it also offers a \"Select Period\" mode whose text input parses relative spans (`45m`, `12 hours`, `last month`, `yesterday`, `1/1 - 1/2`). When consumer-provided `presets` are supplied, the trigger splits into two side-by-side segments — a preset dropdown (left, `🕐 <label> ⌄`) that applies a preset range in one click, and the calendar (right, `📅 <range>`) that opens the grid for fine-tuning. Selection is staged in a draft inside the popover and committed to `v-model` only on Apply (or immediately when `show-apply` is false); a preset commits immediately. In period mode, once a span is applied the trigger shows the same two-segment shape — the relative token (e.g. `45m`) beside the concrete window it resolves to (e.g. `14:44 – 23:59`) — since a period selects a time interval rather than whole days. Date math uses the native `Date` API only — no date library; timezone is a selectable IANA label used for display formatting via `Intl` (the picker does not re-interpret `Date` objects across zones)."
},
"calendar-root": {
"import": "@aziontech/webkit/calendar-root",
Expand Down Expand Up @@ -4904,6 +4911,13 @@
"required": "no",
"doc": "Consumer-provided shortcuts; each is `{ label, value }` where value is a `Date` or range. When present, the trigger becomes a two-part control whose left segment opens a dropdown of these presets."
},
{
"name": "showFields",
"type": "boolean",
"default": "true",
"required": "no",
"doc": "Shows the Start/End date fields in the panel. Set false when the shortcuts and the grid are the whole interaction and typing a boundary by hand is not offered."
},
{
"name": "showTime",
"type": "boolean",
Expand Down Expand Up @@ -5012,7 +5026,7 @@
"notes": "Custom footer/action-bar content (e.g. `<Calendar.Clear>`) alongside Apply."
}
],
"purpose": "Date-range picker. A trigger button opens a popover that holds a month grid, Start/End date + time fields, an optional timezone selector, and an Apply action; it also offers a \"Select Period\" mode whose text input parses relative spans (`45m`, `12 hours`, `last month`, `yesterday`, `1/1 - 1/2`). When consumer-provided `presets` are supplied, the trigger splits into two side-by-side segments — a preset dropdown (left, `🕐 <label> ⌄`) that applies a preset range in one click, and the calendar (right, `📅 <range>`) that opens the grid for fine-tuning. Selection is staged in a draft inside the popover and committed to `v-model` only on Apply (or immediately when `show-apply` is false); a preset commits immediately. In period mode, once a span is applied the trigger shows the same two-segment shape — the relative token (e.g. `45m`) beside the concrete window it resolves to (e.g. `14:44 – 23:59`) — since a period selects a time interval rather than whole days. Date math uses the native `Date` API only — no date library; timezone is a selectable IANA label used for display formatting via `Intl` (the picker does not re-interpret `Date` objects across zones)."
"purpose": "Date-range picker. A trigger button opens a popover that holds a month grid, Start/End date + time fields (suppressible with `show-fields`), an optional timezone selector, and an Apply action; it also offers a \"Select Period\" mode whose text input parses relative spans (`45m`, `12 hours`, `last month`, `yesterday`, `1/1 - 1/2`). When consumer-provided `presets` are supplied, the trigger splits into two side-by-side segments — a preset dropdown (left, `🕐 <label> ⌄`) that applies a preset range in one click, and the calendar (right, `📅 <range>`) that opens the grid for fine-tuning. Selection is staged in a draft inside the popover and committed to `v-model` only on Apply (or immediately when `show-apply` is false); a preset commits immediately. In period mode, once a span is applied the trigger shows the same two-segment shape — the relative token (e.g. `45m`) beside the concrete window it resolves to (e.g. `14:44 – 23:59`) — since a period selects a time interval rather than whole days. Date math uses the native `Date` API only — no date library; timezone is a selectable IANA label used for display formatting via `Intl` (the picker does not re-interpret `Date` objects across zones)."
},
"calendar-preset": {
"import": "@aziontech/webkit/calendar-preset",
Expand Down
29 changes: 29 additions & 0 deletions packages/webkit/src/components/inputs/calendar/calendar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,35 @@ describe('Calendar', () => {
expect(events[0][0]).toEqual({ start: null, end: null })
})

// ---- show-fields ----------------------------------------------------------------

it('show-fields=false hides the Start/End fields but keeps the grid and Apply', async () => {
const { getByTestId } = render(Calendar, {
props: { mode: 'range', modelValue: { start: OCT_8, end: null }, showFields: false }
})

await fireEvent.click(getByTestId('input-calendar__trigger'))
await waitFor(() => expect(getPopover()).not.toBeNull())

const popover = getPopover() as HTMLElement
expect(popover.querySelector('[data-testid="input-calendar__fields"]')).toBeNull()
expect(popover.querySelector('[data-testid="input-calendar__grid"]')).not.toBeNull()
expect(popover.querySelector('[data-testid="input-calendar__apply"]')).not.toBeNull()
})

it('show-fields defaults to true: the Start/End fields render in the panel', async () => {
const { getByTestId } = render(Calendar, {
props: { mode: 'range', modelValue: { start: OCT_8, end: null } }
})

await fireEvent.click(getByTestId('input-calendar__trigger'))
await waitFor(() => expect(getPopover()).not.toBeNull())

expect(
(getPopover() as HTMLElement).querySelector('[data-testid="input-calendar__fields"]')
).not.toBeNull()
})

// ---- Presets (two-part trigger) -------------------------------------------------

const PRESETS = [
Expand Down
15 changes: 13 additions & 2 deletions packages/webkit/src/components/inputs/calendar/calendar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@
placeholder?: string
/** Data-driven shortcuts rendered in the presets rail; each is { label, value }. */
presets?: CalendarPresetItem[]
/** Shows the Start/End date fields in the panel. */
showFields?: boolean
/** Shows Start/End time fields alongside the date fields. */
showTime?: boolean
/** Shows the timezone selector below the fields. */
Expand Down Expand Up @@ -89,6 +91,7 @@
open: undefined,
placeholder: 'Select a Date Range',
presets: () => [],
showFields: true,
showTime: false,
showTimezone: false,
timezone: '',
Expand Down Expand Up @@ -635,6 +638,14 @@
</span>

<Teleport to="body">
<!-- The panel is content-sized, so its WIDEST child sets its width — and one of
those children is the consumer's `#footer` slot. A row of shortcut chips (or
any wide footer content) would otherwise stretch the panel without bound and
spread the month grid across it. `max-w` is the hard stop: `44rem` rather than
a px container token because the content that grows is text, so the cap has to
grow with the root font the same way the grid and the chips do; `90vw` keeps it
inside a narrow viewport. It sits above every built-in layout, including
`horizontal` (grid + fields column), so nothing that ships is clipped by it. -->
<Transition
enter-active-class="animate-popup-scale-in motion-reduce:animate-none"
leave-active-class="animate-popup-scale-out motion-reduce:animate-none"
Expand All @@ -648,7 +659,7 @@
:data-state="isOpen ? 'open' : 'closed'"
:data-placement="resolvedPlacement"
:style="panelStyle"
class="flex flex-col overflow-hidden rounded-[var(--shape-card)] border border-[var(--border-default)] bg-[var(--bg-surface-raised)] shadow-[var(--shadow-sm)] outline-none [transform-origin:var(--popup-origin,top_left)]"
class="flex max-w-[min(90vw,44rem)] flex-col overflow-hidden rounded-[var(--shape-card)] border border-[var(--border-default)] bg-[var(--bg-surface-raised)] shadow-[var(--shadow-sm)] outline-none [transform-origin:var(--popup-origin,top_left)]"
@keydown="onPanelKeydown"
>
<div
Expand All @@ -670,7 +681,7 @@
: 'border-t border-[var(--border-default)]'
"
>
<CalendarFields />
<CalendarFields v-if="showFields" />

<span
v-if="$slots['footer']"
Expand Down
Loading