@adia-ai/web-components 0.6.32 → 0.6.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/components/accordion/accordion.css +2 -2
  3. package/components/action-list/action-list.css +2 -2
  4. package/components/agent-artifact/agent-artifact.css +31 -31
  5. package/components/agent-feedback-bar/agent-feedback-bar.css +10 -10
  6. package/components/agent-questions/agent-questions.css +57 -57
  7. package/components/agent-reasoning/agent-reasoning.css +62 -62
  8. package/components/agent-suggestions/agent-suggestions.css +4 -4
  9. package/components/agent-trace/agent-trace.css +53 -53
  10. package/components/alert/alert.css +41 -41
  11. package/components/avatar/avatar.css +27 -27
  12. package/components/badge/badge.css +27 -27
  13. package/components/block/block.css +16 -16
  14. package/components/breadcrumb/breadcrumb.css +23 -23
  15. package/components/button/button.css +101 -91
  16. package/components/calendar-grid/calendar-grid.a2ui.json +136 -0
  17. package/components/calendar-grid/calendar-grid.css +226 -0
  18. package/components/calendar-grid/calendar-grid.d.ts +37 -0
  19. package/components/calendar-grid/calendar-grid.js +17 -0
  20. package/components/calendar-grid/calendar-grid.yaml +116 -0
  21. package/components/calendar-grid/class.js +300 -0
  22. package/components/calendar-picker/calendar-picker.css +139 -139
  23. package/components/canvas/canvas.css +12 -12
  24. package/components/card/card.css +83 -83
  25. package/components/chart/chart.css +224 -224
  26. package/components/chart-legend/chart-legend.css +26 -26
  27. package/components/check/check.css +40 -40
  28. package/components/code/code.css +125 -125
  29. package/components/col/col.css +15 -15
  30. package/components/color-picker/color-picker.css +55 -55
  31. package/components/combobox/class.js +861 -0
  32. package/components/combobox/combobox.a2ui.json +363 -0
  33. package/components/combobox/combobox.css +244 -0
  34. package/components/combobox/combobox.d.ts +113 -0
  35. package/components/combobox/combobox.examples.md +59 -0
  36. package/components/combobox/combobox.js +17 -0
  37. package/components/combobox/combobox.test.js +181 -0
  38. package/components/combobox/combobox.yaml +369 -0
  39. package/components/command/command.css +90 -90
  40. package/components/date-range-picker/class.js +775 -0
  41. package/components/date-range-picker/date-range-picker.a2ui.json +300 -0
  42. package/components/date-range-picker/date-range-picker.css +178 -0
  43. package/components/date-range-picker/date-range-picker.d.ts +82 -0
  44. package/components/date-range-picker/date-range-picker.examples.md +37 -0
  45. package/components/date-range-picker/date-range-picker.js +17 -0
  46. package/components/date-range-picker/date-range-picker.test.js +387 -0
  47. package/components/date-range-picker/date-range-picker.yaml +285 -0
  48. package/components/datetime-picker/class.js +706 -0
  49. package/components/datetime-picker/datetime-picker.a2ui.json +334 -0
  50. package/components/datetime-picker/datetime-picker.css +150 -0
  51. package/components/datetime-picker/datetime-picker.d.ts +86 -0
  52. package/components/datetime-picker/datetime-picker.examples.md +46 -0
  53. package/components/datetime-picker/datetime-picker.js +17 -0
  54. package/components/datetime-picker/datetime-picker.test.js +454 -0
  55. package/components/datetime-picker/datetime-picker.yaml +332 -0
  56. package/components/demo-toggle/demo-toggle.css +27 -27
  57. package/components/description-list/description-list.css +18 -18
  58. package/components/divider/divider.css +24 -24
  59. package/components/embed/embed.css +6 -6
  60. package/components/empty-state/empty-state.css +27 -27
  61. package/components/feed/feed.css +12 -12
  62. package/components/field/field.css +37 -28
  63. package/components/field/field.test.js +32 -0
  64. package/components/fields/fields.css +5 -5
  65. package/components/grid/grid.css +5 -5
  66. package/components/heatmap/heatmap.css +63 -63
  67. package/components/icon/icon.css +12 -12
  68. package/components/image/image.css +14 -14
  69. package/components/index.js +8 -0
  70. package/components/input/input.css +66 -66
  71. package/components/inspector/inspector.css +6 -6
  72. package/components/integration-card/class.js +410 -0
  73. package/components/integration-card/integration-card.a2ui.json +268 -0
  74. package/components/integration-card/integration-card.css +169 -0
  75. package/components/integration-card/integration-card.d.ts +63 -0
  76. package/components/integration-card/integration-card.examples.md +41 -0
  77. package/components/integration-card/integration-card.js +17 -0
  78. package/components/integration-card/integration-card.test.js +306 -0
  79. package/components/integration-card/integration-card.yaml +280 -0
  80. package/components/kbd/kbd.css +32 -32
  81. package/components/link/link.css +12 -12
  82. package/components/list/list.css +8 -8
  83. package/components/list-window/class.js +688 -0
  84. package/components/list-window/list-window.a2ui.json +277 -0
  85. package/components/list-window/list-window.css +124 -0
  86. package/components/list-window/list-window.d.ts +84 -0
  87. package/components/list-window/list-window.examples.md +73 -0
  88. package/components/list-window/list-window.js +17 -0
  89. package/components/list-window/list-window.test.js +303 -0
  90. package/components/list-window/list-window.yaml +270 -0
  91. package/components/menu/menu.css +8 -8
  92. package/components/modal/modal.css +43 -43
  93. package/components/nav/nav.css +40 -40
  94. package/components/nav-group/nav-group.css +52 -52
  95. package/components/nav-item/nav-item.css +44 -44
  96. package/components/noodles/noodles.css +31 -31
  97. package/components/option-card/option-card.css +69 -69
  98. package/components/otp-input/otp-input.css +30 -30
  99. package/components/page/page.css +18 -18
  100. package/components/pagination/pagination.css +61 -61
  101. package/components/pane/pane.css +57 -57
  102. package/components/pipeline-status/pipeline-status.css +65 -65
  103. package/components/popover/popover.css +17 -17
  104. package/components/progress/progress.css +23 -23
  105. package/components/progress-row/progress-row.css +17 -17
  106. package/components/radio/radio.css +39 -39
  107. package/components/range/range.css +55 -55
  108. package/components/rating/rating.css +28 -28
  109. package/components/richtext/richtext.css +133 -133
  110. package/components/row/row.css +19 -19
  111. package/components/search/search.css +5 -5
  112. package/components/segment/segment.css +24 -24
  113. package/components/segmented/segmented.css +25 -25
  114. package/components/select/select.css +84 -84
  115. package/components/skeleton/skeleton.css +14 -14
  116. package/components/slider/slider.css +46 -46
  117. package/components/spinner/class.js +69 -0
  118. package/components/spinner/spinner.a2ui.json +197 -0
  119. package/components/spinner/spinner.css +165 -0
  120. package/components/spinner/spinner.d.ts +26 -0
  121. package/components/spinner/spinner.examples.md +26 -0
  122. package/components/spinner/spinner.js +17 -0
  123. package/components/spinner/spinner.test.js +234 -0
  124. package/components/spinner/spinner.yaml +230 -0
  125. package/components/stack/stack.css +11 -11
  126. package/components/stat/stat.css +25 -25
  127. package/components/step-progress/step-progress.css +20 -20
  128. package/components/stepper/stepper.css +29 -29
  129. package/components/stream/stream.css +12 -12
  130. package/components/swatch/swatch.css +68 -68
  131. package/components/swiper/swiper.css +57 -57
  132. package/components/switch/switch.css +52 -52
  133. package/components/table/class.js +9 -0
  134. package/components/table/table.a2ui.json +1 -1
  135. package/components/table/table.css +162 -162
  136. package/components/table/table.d.ts +1 -1
  137. package/components/table/table.test.js +53 -0
  138. package/components/table/table.yaml +13 -1
  139. package/components/table-toolbar/table-toolbar.css +32 -32
  140. package/components/tabs/tabs.css +51 -51
  141. package/components/tag/tag.css +48 -48
  142. package/components/text/text.css +44 -44
  143. package/components/textarea/textarea.css +46 -46
  144. package/components/time-picker/class.js +693 -0
  145. package/components/time-picker/time-picker.a2ui.json +267 -0
  146. package/components/time-picker/time-picker.css +122 -0
  147. package/components/time-picker/time-picker.d.ts +75 -0
  148. package/components/time-picker/time-picker.examples.md +35 -0
  149. package/components/time-picker/time-picker.js +17 -0
  150. package/components/time-picker/time-picker.test.js +287 -0
  151. package/components/time-picker/time-picker.yaml +256 -0
  152. package/components/timeline/timeline.css +50 -50
  153. package/components/toast/toast.css +58 -58
  154. package/components/toggle-group/toggle-group.css +6 -6
  155. package/components/toggle-scheme/toggle-scheme.css +2 -2
  156. package/components/toolbar/toolbar.css +17 -17
  157. package/components/tooltip/tooltip.css +2 -2
  158. package/components/tree/tree.css +37 -37
  159. package/components/upload/upload.css +49 -49
  160. package/dist/icons-manifest.js +3 -3
  161. package/dist/web-components.min.css +1 -1
  162. package/dist/web-components.min.js +121 -83
  163. package/package.json +1 -1
  164. package/styles/components.css +8 -0
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `<calendar-grid-ui>` — Substrate primitive — a single-month calendar grid (month nav + day cells + keyboard nav). Renders inline; no trigger, no popover. Used as a substrate by calendar-picker-ui (popover wrapper), date-range-picker-ui (two side-by-side grids), and datetime-picker-ui (one grid + time pane). For a complete date-picker affordance (trigger + popover + form participation), use calendar-picker-ui.
3
+ *
4
+ * @see https://ui-kit.exe.xyz/site/components/calendar-grid
5
+ *
6
+ * Type declarations generated by scripts/build/dts-codegen.mjs from
7
+ * the component's `.a2ui.json` sidecar(s). Edit the source `.yaml`,
8
+ * run `npm run build:components`, then `npm run codegen:dts` to
9
+ * regenerate; or hand-author this file fully if rich event types are
10
+ * needed beyond what the yaml `events:` block can express.
11
+ */
12
+
13
+ import { UIElement } from '../../core/element.js';
14
+
15
+ export type CalendarGridChangeEvent = CustomEvent<unknown>;
16
+ export type CalendarGridInputEvent = CustomEvent<unknown>;
17
+
18
+ export class UICalendarGrid extends UIElement {
19
+ /** Disables all interaction. */
20
+ disabled: boolean;
21
+ /** Maximum selectable date in ISO format (YYYY-MM-DD). */
22
+ max: string;
23
+ /** Minimum selectable date in ISO format (YYYY-MM-DD). */
24
+ min: string;
25
+ /** Blocks selection but allows month navigation. */
26
+ readonly: boolean;
27
+ /** Selected date in ISO format (YYYY-MM-DD). */
28
+ value: string;
29
+
30
+ addEventListener<K extends keyof HTMLElementEventMap>(
31
+ type: K,
32
+ listener: (this: UICalendarGrid, ev: HTMLElementEventMap[K]) => unknown,
33
+ options?: boolean | AddEventListenerOptions,
34
+ ): void;
35
+ addEventListener(type: 'change', listener: (ev: CalendarGridChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
36
+ addEventListener(type: 'input', listener: (ev: CalendarGridInputEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
37
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `<calendar-grid-ui>` — auto-registers the tag on import.
3
+ *
4
+ * For non-side-effect class import (test isolation, tag override), use
5
+ * the `class` subpath:
6
+ *
7
+ * import { UICalendarGrid } from '@adia-ai/web-components/components/calendar-grid/class';
8
+ *
9
+ * @see ../../USAGE.md#registration--auto-vs-explicit
10
+ */
11
+
12
+ import { defineIfFree } from '../../core/register.js';
13
+ import { UICalendarGrid } from './class.js';
14
+
15
+ defineIfFree('calendar-grid-ui', UICalendarGrid);
16
+
17
+ export { UICalendarGrid };
@@ -0,0 +1,116 @@
1
+ # Hand-authored substrate primitive — the inline calendar grid extracted
2
+ # from calendar-picker-ui so date-range-picker-ui + datetime-picker-ui can
3
+ # compose JUST the grid (no trigger, no popover). Edit this file; run
4
+ # `npm run components` to regenerate calendar-grid.a2ui.json.
5
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
6
+ name: UICalendarGrid
7
+ tag: calendar-grid-ui
8
+ status: stable
9
+ component: CalendarGrid
10
+ category: input
11
+ version: 1
12
+ description: >-
13
+ Substrate primitive — a single-month calendar grid (month nav + day
14
+ cells + keyboard nav). Renders inline; no trigger, no popover. Used as
15
+ a substrate by calendar-picker-ui (popover wrapper), date-range-picker-ui
16
+ (two side-by-side grids), and datetime-picker-ui (one grid + time pane).
17
+ For a complete date-picker affordance (trigger + popover + form
18
+ participation), use calendar-picker-ui.
19
+ props:
20
+ value:
21
+ description: Selected date in ISO format (YYYY-MM-DD).
22
+ type: string
23
+ default: ''
24
+ reflect: true
25
+ min:
26
+ description: Minimum selectable date in ISO format (YYYY-MM-DD).
27
+ type: string
28
+ default: ''
29
+ reflect: true
30
+ max:
31
+ description: Maximum selectable date in ISO format (YYYY-MM-DD).
32
+ type: string
33
+ default: ''
34
+ reflect: true
35
+ disabled:
36
+ description: Disables all interaction.
37
+ type: boolean
38
+ default: false
39
+ reflect: true
40
+ readonly:
41
+ description: Blocks selection but allows month navigation.
42
+ type: boolean
43
+ default: false
44
+ reflect: true
45
+ events:
46
+ change:
47
+ description: User selected a date. detail.value is the ISO string.
48
+ input:
49
+ description: Focus moved between days during keyboard nav. detail.focusDate is the ISO string.
50
+ slots: {}
51
+ states:
52
+ - name: disabled
53
+ description: Pointer-events off, reduced opacity, no selection.
54
+ attribute: disabled
55
+ - name: readonly
56
+ description: Navigation allowed, selection blocked.
57
+ attribute: readonly
58
+ traits: []
59
+ tokens:
60
+ --calendar-grid-width:
61
+ description: Grid container width.
62
+ default: 16rem
63
+ --calendar-grid-day-size:
64
+ description: Square cell size for day buttons.
65
+ default: 2rem
66
+ --calendar-grid-day-bg-selected:
67
+ description: Background of the selected day cell.
68
+ default: var(--a-accent)
69
+ --calendar-grid-day-fg-selected:
70
+ description: Text color of the selected day cell.
71
+ default: var(--a-accent-fg)
72
+ --calendar-grid-day-bg-hover:
73
+ description: Background on hover (non-selected, non-outside, non-disabled).
74
+ default: var(--a-bg-hover)
75
+ --calendar-grid-day-today-color:
76
+ description: Accent color used for the today-indicator dot + numeral tint.
77
+ default: var(--a-accent)
78
+ a2ui:
79
+ rules:
80
+ - >-
81
+ Use <CalendarGrid> only as a substrate primitive composed inside a
82
+ higher-level component (date-range picker, datetime picker, custom
83
+ date affordance). For a full single-date input, use
84
+ <CalendarPicker> — it adds a trigger button, popover surface, and
85
+ form-association.
86
+ - >-
87
+ <CalendarGrid> is NOT form-associated. Its emitted `change` event
88
+ is the sole signal to the parent — the parent owns the canonical
89
+ value + form participation.
90
+ anti_patterns:
91
+ - wrong: |
92
+ {"component": "CalendarGrid", "name": "birthdate"}
93
+ why: |
94
+ CalendarGrid is a substrate primitive — it does not participate in
95
+ forms. The `name` attribute has no effect. Use CalendarPicker
96
+ (form-associated) for single-date input.
97
+ fix: |
98
+ {"component": "CalendarPicker", "name": "birthdate"}
99
+ keywords:
100
+ - calendar
101
+ - date
102
+ - grid
103
+ - month
104
+ - day-picker
105
+ - substrate
106
+ synonyms:
107
+ calendar:
108
+ - calendar
109
+ - date
110
+ grid:
111
+ - grid
112
+ - calendar
113
+ related:
114
+ - CalendarPicker
115
+ - DateRangePicker
116
+ - DatetimePicker
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Non-side-effect class export for `<calendar-grid-ui>`.
3
+ *
4
+ * The auto-register path stays at `@adia-ai/web-components/components/calendar-grid`
5
+ * (which imports this file + calls `defineIfFree()`).
6
+ *
7
+ * @see ../../USAGE.md#registration--auto-vs-explicit
8
+ */
9
+
10
+ /**
11
+ * `<calendar-grid-ui>` — Substrate primitive: a single-month calendar grid.
12
+ *
13
+ * Renders ONLY the inline grid (month nav + day-cells + keyboard nav). No
14
+ * trigger, no popover — used as a substrate by <calendar-picker-ui> (trigger
15
+ * + popover wrapper), <date-range-picker-ui> (two side-by-side grids), and
16
+ * <datetime-picker-ui> (one grid + time pane).
17
+ *
18
+ * <calendar-grid-ui value="2026-05-23" min="2026-01-01" max="2026-12-31">
19
+ * </calendar-grid-ui>
20
+ *
21
+ * Properties:
22
+ * value — selected ISO date (YYYY-MM-DD)
23
+ * min — minimum selectable date (ISO)
24
+ * max — maximum selectable date (ISO)
25
+ * focus-date — the date that holds the roving tabindex (ISO, defaults to value or today)
26
+ * disabled / readonly — disables / blocks selection
27
+ *
28
+ * Events:
29
+ * change — { value: "2026-05-23" } when a date is selected
30
+ * input — emitted as the user navigates without committing
31
+ *
32
+ * Keyboard: ArrowUp/Down/Left/Right navigate days; Enter selects;
33
+ * PageUp/Down jump month; Home/End first/last of month.
34
+ */
35
+
36
+ import { UIElement } from '../../core/element.js';
37
+
38
+ const DAYS = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
39
+ const MONTHS_LONG = [
40
+ 'January', 'February', 'March', 'April', 'May', 'June',
41
+ 'July', 'August', 'September', 'October', 'November', 'December',
42
+ ];
43
+
44
+ function pad(n) { return String(n).padStart(2, '0'); }
45
+ function toISO(y, m, d) { return `${y}-${pad(m + 1)}-${pad(d)}`; }
46
+ function parseISO(str) {
47
+ if (!str) return null;
48
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(str).trim());
49
+ if (!m) return null;
50
+ return new Date(+m[1], +m[2] - 1, +m[3]);
51
+ }
52
+ function sameDay(a, b) {
53
+ return a.getFullYear() === b.getFullYear()
54
+ && a.getMonth() === b.getMonth()
55
+ && a.getDate() === b.getDate();
56
+ }
57
+
58
+ export class UICalendarGrid extends UIElement {
59
+ // §154 Phosphor icons stamped without consumer markup.
60
+ static requiredIcons = ['caret-left', 'caret-right'];
61
+
62
+ static properties = {
63
+ value: { type: String, default: '', reflect: true },
64
+ min: { type: String, default: '', reflect: true },
65
+ max: { type: String, default: '', reflect: true },
66
+ disabled: { type: Boolean, default: false, reflect: true },
67
+ readonly: { type: Boolean, default: false, reflect: true },
68
+ };
69
+
70
+ // No html`` template — we render imperatively via #renderCalendar so we can
71
+ // mutate per-day cells without diffing.
72
+ static template = () => null;
73
+
74
+ #viewYear = new Date().getFullYear();
75
+ #viewMonth = new Date().getMonth();
76
+ #focusedDay = null;
77
+ #bound = false;
78
+
79
+ // ── Lifecycle ──────────────────────────────────────────────────────
80
+
81
+ connected() {
82
+ this.setAttribute('role', 'group');
83
+ if (!this.hasAttribute('aria-label')) this.setAttribute('aria-label', 'Calendar');
84
+
85
+ // Initialize view to selected date or today.
86
+ const sel = parseISO(this.value);
87
+ if (sel) {
88
+ this.#viewYear = sel.getFullYear();
89
+ this.#viewMonth = sel.getMonth();
90
+ }
91
+
92
+ if (!this.#bound) {
93
+ this.#bound = true;
94
+ this.addEventListener('click', this.#onClick);
95
+ this.addEventListener('keydown', this.#onKey);
96
+ }
97
+ }
98
+
99
+ disconnected() {
100
+ this.removeEventListener('click', this.#onClick);
101
+ this.removeEventListener('keydown', this.#onKey);
102
+ this.#bound = false;
103
+ }
104
+
105
+ render() {
106
+ // If a selected value changed externally, re-sync the visible month so it
107
+ // contains the selection (without clobbering the user's mid-flight nav).
108
+ const sel = parseISO(this.value);
109
+ if (sel && (sel.getFullYear() !== this.#viewYear || sel.getMonth() !== this.#viewMonth)) {
110
+ this.#viewYear = sel.getFullYear();
111
+ this.#viewMonth = sel.getMonth();
112
+ }
113
+ this.#renderCalendar();
114
+ }
115
+
116
+ // ── Public API ─────────────────────────────────────────────────────
117
+
118
+ /** Move the visible month by ±N months. */
119
+ navigate(delta) {
120
+ this.#viewMonth += delta;
121
+ while (this.#viewMonth > 11) { this.#viewMonth -= 12; this.#viewYear++; }
122
+ while (this.#viewMonth < 0) { this.#viewMonth += 12; this.#viewYear--; }
123
+ this.#focusedDay = null;
124
+ this.#renderCalendar();
125
+ }
126
+
127
+ /** Programmatically focus a day (1-based) inside the visible month. */
128
+ focusDay(day) {
129
+ const daysInMonth = new Date(this.#viewYear, this.#viewMonth + 1, 0).getDate();
130
+ if (day < 1 || day > daysInMonth) return;
131
+ this.#focusedDay = day;
132
+ this.#renderCalendar();
133
+ this.querySelector('[data-cal-day][data-focused]')?.focus?.();
134
+ }
135
+
136
+ // ── Internal — render the grid ────────────────────────────────────
137
+
138
+ #renderCalendar() {
139
+ const year = this.#viewYear;
140
+ const month = this.#viewMonth;
141
+ const today = new Date();
142
+ const selected = parseISO(this.value);
143
+ const minDate = parseISO(this.min);
144
+ const maxDate = parseISO(this.max);
145
+
146
+ // Header — Prev / Title / Next
147
+ let h = `<div data-cal-header>
148
+ <button data-cal-prev type="button" aria-label="Previous month" tabindex="-1"><icon-ui name="caret-left"></icon-ui></button>
149
+ <span data-cal-title>${MONTHS_LONG[month]} ${year}</span>
150
+ <button data-cal-next type="button" aria-label="Next month" tabindex="-1"><icon-ui name="caret-right"></icon-ui></button>
151
+ </div>`;
152
+
153
+ // Day-of-week header
154
+ h += `<div data-cal-weekdays>${DAYS.map((d) => `<span>${d}</span>`).join('')}</div>`;
155
+
156
+ // Day grid
157
+ const firstDay = new Date(year, month, 1).getDay();
158
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
159
+ const daysInPrev = new Date(year, month, 0).getDate();
160
+
161
+ h += '<div data-cal-grid>';
162
+
163
+ // Previous-month trailing days (outside, disabled)
164
+ for (let i = firstDay - 1; i >= 0; i--) {
165
+ const day = daysInPrev - i;
166
+ h += `<button type="button" data-cal-day data-outside disabled tabindex="-1">${day}</button>`;
167
+ }
168
+
169
+ // Current-month days
170
+ for (let d = 1; d <= daysInMonth; d++) {
171
+ const date = new Date(year, month, d);
172
+ const iso = toISO(year, month, d);
173
+ const isToday = sameDay(date, today);
174
+ const isSelected = selected && sameDay(date, selected);
175
+ const isDisabled = (minDate && date < minDate) || (maxDate && date > maxDate);
176
+ const isFocused = this.#focusedDay === d;
177
+
178
+ const attrs = [
179
+ 'type="button"',
180
+ 'data-cal-day',
181
+ `data-date="${iso}"`,
182
+ isToday ? 'data-today' : '',
183
+ isSelected ? 'data-selected' : '',
184
+ isDisabled ? 'disabled' : '',
185
+ isFocused ? 'data-focused' : '',
186
+ `tabindex="${isFocused ? '0' : '-1'}"`,
187
+ ].filter(Boolean).join(' ');
188
+
189
+ h += `<button ${attrs}>${d}</button>`;
190
+ }
191
+
192
+ // Next-month leading days
193
+ const totalCells = firstDay + daysInMonth;
194
+ const remaining = (7 - (totalCells % 7)) % 7;
195
+ for (let d = 1; d <= remaining; d++) {
196
+ h += `<button type="button" data-cal-day data-outside disabled tabindex="-1">${d}</button>`;
197
+ }
198
+
199
+ h += '</div>';
200
+
201
+ this.innerHTML = h;
202
+ }
203
+
204
+ // ── Selection ─────────────────────────────────────────────────────
205
+
206
+ #selectDate(iso) {
207
+ if (this.disabled || this.readonly) return;
208
+ this.value = iso;
209
+ // Re-render to reflect new selection.
210
+ this.#renderCalendar();
211
+ this.dispatchEvent(new CustomEvent('change', {
212
+ bubbles: true,
213
+ detail: { value: iso },
214
+ }));
215
+ }
216
+
217
+ // ── Event handlers ────────────────────────────────────────────────
218
+
219
+ #onClick = (e) => {
220
+ if (this.disabled) return;
221
+ const target = e.target instanceof Element ? e.target : null;
222
+ if (!target) return;
223
+ const prev = target.closest('[data-cal-prev]');
224
+ if (prev) { e.stopPropagation(); this.navigate(-1); return; }
225
+ const next = target.closest('[data-cal-next]');
226
+ if (next) { e.stopPropagation(); this.navigate(1); return; }
227
+ const day = target.closest('[data-cal-day][data-date]:not([disabled])');
228
+ if (day) { e.stopPropagation(); this.#selectDate(day.dataset.date); }
229
+ };
230
+
231
+ #onKey = (e) => {
232
+ if (this.disabled) return;
233
+
234
+ if (e.key === 'Enter' || e.key === ' ') {
235
+ if (this.#focusedDay) {
236
+ e.preventDefault();
237
+ const iso = toISO(this.#viewYear, this.#viewMonth, this.#focusedDay);
238
+ const date = new Date(this.#viewYear, this.#viewMonth, this.#focusedDay);
239
+ const minDate = parseISO(this.min);
240
+ const maxDate = parseISO(this.max);
241
+ if ((!minDate || date >= minDate) && (!maxDate || date <= maxDate)) {
242
+ this.#selectDate(iso);
243
+ }
244
+ }
245
+ return;
246
+ }
247
+
248
+ const arrows = { ArrowUp: -7, ArrowDown: 7, ArrowLeft: -1, ArrowRight: 1 };
249
+ if (arrows[e.key] !== undefined) {
250
+ e.preventDefault();
251
+ const daysInMonth = new Date(this.#viewYear, this.#viewMonth + 1, 0).getDate();
252
+ let day = (this.#focusedDay || 1) + arrows[e.key];
253
+
254
+ if (day < 1) {
255
+ this.navigate(-1);
256
+ const prevDays = new Date(this.#viewYear, this.#viewMonth + 1, 0).getDate();
257
+ this.#focusedDay = prevDays + day;
258
+ } else if (day > daysInMonth) {
259
+ this.navigate(1);
260
+ this.#focusedDay = day - daysInMonth;
261
+ } else {
262
+ this.#focusedDay = day;
263
+ }
264
+
265
+ this.#renderCalendar();
266
+ const focused = this.querySelector('[data-cal-day][data-focused]');
267
+ focused?.focus?.();
268
+ this.dispatchEvent(new CustomEvent('input', {
269
+ bubbles: true,
270
+ detail: { focusDate: toISO(this.#viewYear, this.#viewMonth, this.#focusedDay) },
271
+ }));
272
+ return;
273
+ }
274
+
275
+ if (e.key === 'PageUp') {
276
+ e.preventDefault();
277
+ this.navigate(-1);
278
+ return;
279
+ }
280
+ if (e.key === 'PageDown') {
281
+ e.preventDefault();
282
+ this.navigate(1);
283
+ return;
284
+ }
285
+ if (e.key === 'Home') {
286
+ e.preventDefault();
287
+ this.#focusedDay = 1;
288
+ this.#renderCalendar();
289
+ this.querySelector('[data-cal-day][data-focused]')?.focus?.();
290
+ return;
291
+ }
292
+ if (e.key === 'End') {
293
+ e.preventDefault();
294
+ this.#focusedDay = new Date(this.#viewYear, this.#viewMonth + 1, 0).getDate();
295
+ this.#renderCalendar();
296
+ this.querySelector('[data-cal-day][data-focused]')?.focus?.();
297
+ return;
298
+ }
299
+ };
300
+ }