import { Component, signal } from '@angular/core';import { PctCalendar, PctDay } from '@pacit/components/date';/** The month grid on its own — a roving grid the arrow keys walk. */@Component({ selector: 'demo-calendar', imports: [PctCalendar], styles: ':host { display: block; max-inline-size: 24rem; }', template: ` <pct-calendar [(value)]="day" ariaLabel="Pick a day" /> `,})export class CalendarDemo { readonly day = signal<PctDay | null>(null);}
calendar.demo.ts — the same file runs here and is highlighted under Code
Start here
Usage
One import per component, and the shortest possible use — the package itself is installed once, on the start page.
Import this component
import { PctCalendar } from '@pacit/components/date';
Exported
What it is
PctCalendarCellinterface
One cell of the grid, with everything the template and the reader need already decided.
PctDayPredicatetype(day: PctDay) => boolean
A day the consumer refuses — weekends, holidays, a taken slot.
PctCalendarcomponentpct-calendar
Calendar — one month of days as a role="grid", walked by a roving tabindex.
PctDatePanelAligntype'start' | 'end'
Which edge of the field the panel lines its own up with.
PctDatecomponentpct-date
Date field — a text control the library formats and parses per locale, with a calendar in a panel beside it.
A bound pctDayBounds takes: a day, or a function of the field's context that returns one — the two shapes Angular's own min() takes. One that is not a day, undefined among them, is no bound, as it is on the field's input.
What pctDayBounds reports: minDay for a day before min, maxDay for one after max. Each carries the bound it broke, as the bound was written.
PctDayBoundsConfiginterfaceunreleased
The bounds of pctDayBounds, and the sentence its errors carry — one for both, or one written from the error, which holds the bound.
pctDayBoundsfunctionunreleased
Bounds a date field in a signal form: what Angular's minDate() and maxDate() do for a Date, for a PctDay — which they cannot take (their path is a Date's, and their validator reads the value with getTime()), and which the compiler refuses as an attribute beside [formField] (NG8022). The bounds reach the field's min / max, so the calendar's walk stops at them, and the form grades the value: a day outside is minDay or maxDay.
PctDaytypestring
A calendar day, written the way the platform already writes one: YYYY-MM-DD.
PctDayPartsinterface
The three fields of a day, as numbers. month is 1–12, the way a person says it.
pctDaysInMonthfunction
How many days February has — the Gregorian rule, written once.
isPctDayfunction
Whether a string is a day this library will work with: the right shape and a day the calendar has. 2026-02-30 has the shape and is not a day, and the difference matters — a value read from a server, a query string or a localStorage entry is a string somebody else wrote.
pctDayfunction
Builds a day from its three fields, normalising an overflow the way a person expects — month 13 is January of the next year, day 0 is the last day of the previous month. That is what makes pctAddMonths and pctAddDays one line each instead of three branches.
pctDayAsUtcfunction
The day as the one Date anything outside this file is allowed to see: midnight UTC, which is what Intl.DateTimeFormat with timeZone: 'UTC' reads back as the same three fields it was built from. Handing out a local-time Date is the defect this whole module exists to refuse.
pctDayPartsfunction
The three fields of a day. The caller has already established that it is one.
pctTodayfunction
Today, read from the LOCAL clock — the one read of local time in this file, and the only one that belongs: "what day is it" is a question about where the user is standing, and every other question here is arithmetic on a day that already has an answer.
pctAddDaysfunction
n days on (or back).
pctAddMonthsfunction
n months on (or back), clamped to the length of the month it lands in: 31 January plus one month is 28 February and not 3 March. The clamp is what a calendar's "next month" button means — a walk that changed the day of the month would move the cursor twice.
pctCompareDaysfunction
-1, 0 or 1 — read from the three fields, though the shape invites a string compare and carried one for months.
pctWeekdayfunction
The ISO weekday: Monday is 1, Sunday is 7 — the numbering Intl.Locale.prototype.getWeekInfo() answers in, so the two never need converting between them.
pctClampDayfunction
Held inside the bounds, either of which may be absent — and undefined is what absent is, because that is how the FormUiControl contract's min / max spell an absent bound.
pctMonthGridfunction
The grid of one month: whole weeks, starting on firstDayOfWeek, with the days of the neighbouring months that share those weeks. Six weeks always, and that is a decision rather than an oversight — a month spans four to six weeks depending on where it starts, and a panel that changed height between March and August would move the page under the pointer mid-walk.
PctDayFieldtype'day' | 'month' | 'year'
Which of the three fields a position in the format holds.
PctDayLettersinterface
The letters a format hint is written with — one per field, from PCT_TEXTS.
pctFirstDayOfWeekfunction
Which day the week starts on, 1 (Monday) … 7 (Sunday).
PctDayFormatinterface
Everything about writing and reading a day in one language, built once per locale.
pctDayFormatfunction
The day, weekday and month names of a locale, built once and shared.
PctWeekdayNameinterface
The two names a column header needs: the one the eye reads and the one a reader says.
pctWeekdayNamesfunction
The seven column headings, rotated so that the first is firstDayOfWeek.
pctMonthCaptionfunction
The month and year over the grid ("December 2026"), in the value's own calendar.
<pct-calendar [(value)]="day" ariaLabel="Pick a day" />
Examples
Every state, running
Each example is the component itself, not a screenshot. Open the code under any of them.
Another language, another first day
locale names the month and the weekdays through the platform's own Intl; firstDayOfWeek starts the row where that culture starts it — Monday is 1.
październik 2026
Pponiedziałek
Wwtorek
Śśroda
Cczwartek
Ppiątek
Ssobota
Nniedziela
28
29
30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
6
7
8
API
What it reads, and what it writes back
Read from the source at build time — the types, the defaults and the lines of documentation are the library's own.
unreleased marks 5 of them: on main, and not in @pacit/components@0.2.0 — the next release names the version.
Calendar — one month of days as a role="grid", walked by a roving tabindex.
Inputs
Name
Type
What it does
valuetwo-waydefault null
PctDay | null
The chosen day, or null.
mindefault undefined
unknown
The earliest and latest day the walk may reach; absent means no bound on that side.
maxdefault undefined
unknown
The latest day the walk may reach; absent means no bound on that side — the other half of min.
dateDisableddefault null
PctDayPredicate | null
Days inside the bounds that still cannot be picked. The split is deliberate: min/max are the range and this is the holes in it — the bounds clamp where the keyboard can go, a refused day is reached, announced and simply not takeable.
startAtunreleaseddefault undefined
unknown
The day the grid opens on while there is no value — absent, today. It is where the walk STARTS and nothing more: no day is marked chosen by it, and the bounds still hold it. The datetime field hands it the month a half that is not a date names, so 31.02.2026 opens on February (0090 §2).
disableddefault false
boolean
Freezes the grid: no day can be picked, the walk and the month buttons stop, and aria-disabled says so.
localedefault ''
string
Overrides the application's LOCALE_ID, as on [pctNumber] and <pct-slider>.
firstDayOfWeekdefault 0
number
Which day the week starts on, 1 (Monday) … 7 (Sunday). Absent, the locale decides — through the platform where it answers and through a table of regions where it does not (locale.ts — firefox 151 is one engine that does not, and Angular still supports it).
sizedefault config.defaultSize
PctSize
Scales the grid with the field sizes, so a panel matches the field it opens from; from providePctConfig by default (req-api-config).
ariaLabeldefault ''
string
The accessible name of the grid; the panel that owns one passes its own.
ariaLabelledbydefault ''
string
As ariaLabel, for a name that already stands somewhere on the page.
Outputs
Name
Payload
When it fires
dayPicked
PctDay
A day was chosen by the user — which is not the same event as value changing, and the difference is what closes a panel: a value written from outside must not.
What it writes on the element
The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.
Attribute
Bound to
Note
class
pct-calendar
data-pct-size
size()
data-pct-disabled
disabled() ? "" : null
Styling
Parts to select, tokens to override
Two ways in, both stable. No class names to depend on.
Parts
In a test, PctCalendarHarness from @pacit/components/testing reads a part by the name below and a state by its attribute — the harness gate holds the names to the package.
Part
Selector
What it is
caption
[data-pct-part="caption"]
the month and the year over the grid
day
[data-pct-part="day"]
one day cell
grid
[data-pct-part="grid"]
the grid of days
nav
[data-pct-part="nav"]
the previous-month and next-month buttons
week
[data-pct-part="week"]
one row of the grid
weekday
[data-pct-part="weekday"]
a heading over a column of days
control
[data-pct-part="control"]
the typed input
error
[data-pct-part="error"]
the message when the value is invalid
hint
[data-pct-part="hint"]
the hint under the input
label
[data-pct-part="label"]
the label of the typed input
panel
[data-pct-part="panel"]
the calendar, floating or inline
toggle
[data-pct-part="toggle"]
the button that opens the calendar
warning
[data-pct-part="warning"]
the message when the value is allowed and suspect — after the error, before the hint (0087)
Tokens
Every value the component reads, with the light and the dark default. Override a token at any scope and it cascades like any custom property.
Pattern: the grid of Date Picker Dialog — a <table role="grid"> walked by a roving tabindex
Keyboard
key
effect
test
ArrowRight / ArrowLeft
one day along the inline axis
libs/components/date/src/calendar.spec.ts
ArrowDown / ArrowUp
one week
libs/components/date/src/calendar.spec.ts
Home / End
the first / last day of the week it stands in
libs/components/date/src/calendar.spec.ts
PageDown / PageUp
one month
libs/components/date/src/calendar.spec.ts
Shift+PageDown / Shift+PageUp
one year
apps/sandbox-e2e/src/date.spec.ts
Enter / Space
takes the day the cursor is on
libs/components/date/src/calendar.spec.ts
ArrowRight moves to the next day in an LTR grid and to the previous one in an RTL grid, because a grid's arrows are about the cell to the side and which side that is depends on the direction it is written in — req-token-logical read on the keyboard rather than in a stylesheet. Measured as geometry and as behaviour in apps/sandbox-e2e/src/date.spec.ts.
A walk is not a choice. Moving the cursor never writes the value; only Enter, the space bar or a press does.
What is measured, and what is not yet
Every line is a test in three engines or a gate on every commit. The gaps are listed the same way, each with the requirement that will close it.
MeasuredARIA APG pattern in the JSDoclibs/components/date/src/calendar.ts
MeasuredKeyboard maplibs/components/date/src/calendar.spec.ts and apps/sandbox-e2e/src/date.spec.ts
Measuredaxe auditapps/sandbox-e2e/src/a11y.spec.ts — the /date view holds an inline calendar, so the grid is audited attached and not only inside a panel
MeasuredVisual screenshotapps/sandbox-e2e/src/visual.spec.ts — date-calendar and date-calendar-rtl
Measuredforced-colors: activeapps/sandbox-e2e/src/forced-colors.spec.ts — the chosen day, today and a refused day stay three different things after the palette swap
Measuredprefers-reduced-motiontools/check-styles.mjs point 9 — the day cell's transition takes --pct-motion-transition-duration
MeasuredTouch target ≥ 24×24 pxlibs/components/date/src/calendar.scss — the day cell is --pct-date-day-size (32 px at md, 28 at sm), above the threshold at every size
MeasuredSize axis--pct-date-day-size-sm / -lg — every box in the grid is derived from the day cell, so one number moves the whole month
MeasuredRTLapps/sandbox-e2e/src/date.spec.ts — geometry and arrows; the date-calendar-rtl baseline
MeasuredSSR + hydrationapps/sandbox-e2e/src/hydration.spec.ts — the /date view is in SBX_ROUTES, and the inline calendar renders on the server
n/aFormsnot applicable — the calendar is not a form control. It is <pct-date> that implements FormValueControl
n/aMessage announcednot applicable — the calendar draws no message. The month caption is aria-live="polite", because it is the one thing that changes without the user moving anything they can see
MeasuredDay arithmetic as lawslibs/components/date/src/day.property.spec.ts — the grid's own laws among them: six rows of seven, 42 consecutive days, the first column always firstDayOfWeek, and the month's 1st inside the first row. The bound test this panel reads a cell's disabling from went through < on two day strings until that sweep (lesson-185)
MeasuredParts in the inventorylibs/components/parts.snapshot.md, tools/check-parts.mjs
MeasuredTokens + contrast.policy.jsonlibs/tokens/src/contrast.policy.json — the day, its states and the weekday headings
MeasuredStrings through PCT_TEXTStools/check-texts.mjs — two keys; everything else is Intl
MeasuredSize budgetlibs/components/size.snapshot.md — measured together with the field, one entrypoint
MeasuredScreen-reader logRead 2026-09-28 from docs/acr/at/ — three readers, whole sandbox, and the act on /date that opens the month grid — the only way this component enters the logs. Focus lands on the chosen day and what a reader makes of the cell differs: Orca Choisir une date · dialog · août 2026. · table with 7 rows 7 columns · jeudi column header 27 · row 6 column 4 — the caption, the table's shape, the column heading's long weekday and the cell's TEXT, not the whole date the cell carries as its name; NVDA Choisir une date, dialog, the dialog alone in the act's window; VoiceOver silent in the window and 27 on the row after. So the long weekday reaches a reader through the column heading, as designed, and the cell's date name reached none of the three inside the act's two seconds. The moving focus and the month change are arrows the act does not press, and stay unread
Measureddocs page/components/calendar on the published site — prerendered, the demo's own source is the code tab
Evidence
What the repository holds for this component
Read at build time from the tracked snapshots, not typed here.
0colour pairs measuredlight and dark, at every token build
0visual baselinesChromium, the sandbox's own pictures
84elements in the preview46 listeners · 1 render to settle · 17345 µs in jsdom, held exactly but for the clock
Why it is built this way
It ships in the date entrypoint and is a component of its own rather than a private half of PctDate, for one reason: a calendar standing on the page, always visible, is a real control and not a degenerate date field.<pct-date> puts this one in a panel; a booking screen puts it in a column.
Decisions behind it
0043A day is not an instant, and the field that takes one is text the application formats
0026One channel per politeness, and a message with a home announces from it
lesson-185A day's string is not fixed-width, and two functions had assumed it was
Known limitations
One month at a time. Two months side by side is what a range picker wants, and a range is a question about the tag (0034).
No month or year view. The nav steps a month at a time and Shift+PageUp/PageDown steps a year; a distant date is reached by typing it in the field, which is the whole reason the field is typable. A three-view picker is a feature waiting for the consumer who needs one without a field beside it.
The Gregorian calendar only — see PctDate.
The first day of the week is a table where the platform is silent.Intl.Locale.prototype.getWeekInfo() was absent from firefox 151, which Angular still supports, so 80 regions are written down in locale.ts and checked against the platform's own CLDR for every two-letter region code in locale.spec.ts; date.spec.ts deletes the method in every engine and draws the grid from the table (0084). A consumer can override it outright with firstDayOfWeek.