@spfx-fast/sharepoint-ui 0.0.0-stage → 0.4.0

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 (158) hide show
  1. package/AGENTS.md +211 -0
  2. package/LICENSE +21 -0
  3. package/README.md +897 -2
  4. package/components.json +5810 -0
  5. package/dist/appearance.d.ts +6 -0
  6. package/dist/appearance.js +217 -0
  7. package/dist/elements/accordion.d.ts +9 -0
  8. package/dist/elements/accordion.js +338 -0
  9. package/dist/elements/alert.d.ts +2 -0
  10. package/dist/elements/alert.js +62 -0
  11. package/dist/elements/avatar-group-primitives.d.ts +3 -0
  12. package/dist/elements/avatar-group-primitives.js +189 -0
  13. package/dist/elements/avatar-group.d.ts +2 -0
  14. package/dist/elements/avatar-group.js +110 -0
  15. package/dist/elements/avatar.d.ts +4 -0
  16. package/dist/elements/avatar.js +251 -0
  17. package/dist/elements/badge.d.ts +8 -0
  18. package/dist/elements/badge.js +182 -0
  19. package/dist/elements/breadcrumb.d.ts +4 -0
  20. package/dist/elements/breadcrumb.js +233 -0
  21. package/dist/elements/button-group.d.ts +1 -0
  22. package/dist/elements/button-group.js +20 -0
  23. package/dist/elements/button-variants.d.ts +6 -0
  24. package/dist/elements/button-variants.js +339 -0
  25. package/dist/elements/button.d.ts +5 -0
  26. package/dist/elements/button.js +233 -0
  27. package/dist/elements/card-primitives.d.ts +3 -0
  28. package/dist/elements/card-primitives.js +83 -0
  29. package/dist/elements/card.d.ts +5 -0
  30. package/dist/elements/card.js +213 -0
  31. package/dist/elements/carousel.d.ts +92 -0
  32. package/dist/elements/carousel.js +1112 -0
  33. package/dist/elements/color-picker.d.ts +24 -0
  34. package/dist/elements/color-picker.js +1004 -0
  35. package/dist/elements/combobox.d.ts +1 -0
  36. package/dist/elements/combobox.js +1012 -0
  37. package/dist/elements/data-grid-model.d.ts +117 -0
  38. package/dist/elements/data-grid-model.js +294 -0
  39. package/dist/elements/data-grid.d.ts +29 -0
  40. package/dist/elements/data-grid.js +1567 -0
  41. package/dist/elements/date-picker.d.ts +1 -0
  42. package/dist/elements/date-picker.js +486 -0
  43. package/dist/elements/dialog.d.ts +23 -0
  44. package/dist/elements/dialog.js +409 -0
  45. package/dist/elements/divider.d.ts +2 -0
  46. package/dist/elements/divider.js +66 -0
  47. package/dist/elements/drawer.d.ts +3 -0
  48. package/dist/elements/drawer.js +308 -0
  49. package/dist/elements/field-composition.d.ts +26 -0
  50. package/dist/elements/field-composition.js +133 -0
  51. package/dist/elements/field.d.ts +1 -0
  52. package/dist/elements/field.js +67 -0
  53. package/dist/elements/file-upload.d.ts +6 -0
  54. package/dist/elements/file-upload.js +211 -0
  55. package/dist/elements/floating-position.d.ts +26 -0
  56. package/dist/elements/floating-position.js +39 -0
  57. package/dist/elements/image.d.ts +28 -0
  58. package/dist/elements/image.js +38 -0
  59. package/dist/elements/info.d.ts +9 -0
  60. package/dist/elements/info.js +102 -0
  61. package/dist/elements/input.d.ts +5 -0
  62. package/dist/elements/input.js +255 -0
  63. package/dist/elements/label.d.ts +15 -0
  64. package/dist/elements/label.js +56 -0
  65. package/dist/elements/link.d.ts +1 -0
  66. package/dist/elements/link.js +92 -0
  67. package/dist/elements/list.d.ts +19 -0
  68. package/dist/elements/list.js +297 -0
  69. package/dist/elements/menu.d.ts +33 -0
  70. package/dist/elements/menu.js +1103 -0
  71. package/dist/elements/message-bar.d.ts +10 -0
  72. package/dist/elements/message-bar.js +299 -0
  73. package/dist/elements/nav.d.ts +32 -0
  74. package/dist/elements/nav.js +791 -0
  75. package/dist/elements/overflow.d.ts +9 -0
  76. package/dist/elements/overflow.js +218 -0
  77. package/dist/elements/persona.d.ts +3 -0
  78. package/dist/elements/persona.js +124 -0
  79. package/dist/elements/popover.d.ts +77 -0
  80. package/dist/elements/popover.js +840 -0
  81. package/dist/elements/progress-bar.d.ts +4 -0
  82. package/dist/elements/progress-bar.js +82 -0
  83. package/dist/elements/radio-group.d.ts +1 -0
  84. package/dist/elements/radio-group.js +303 -0
  85. package/dist/elements/rating-display.d.ts +2 -0
  86. package/dist/elements/rating-display.js +125 -0
  87. package/dist/elements/rating.d.ts +2 -0
  88. package/dist/elements/rating.js +206 -0
  89. package/dist/elements/rich-text-editor.d.ts +18 -0
  90. package/dist/elements/rich-text-editor.js +508 -0
  91. package/dist/elements/search.d.ts +3 -0
  92. package/dist/elements/search.js +207 -0
  93. package/dist/elements/select.d.ts +6 -0
  94. package/dist/elements/select.js +291 -0
  95. package/dist/elements/shared.d.ts +19 -0
  96. package/dist/elements/shared.js +396 -0
  97. package/dist/elements/skeleton.d.ts +6 -0
  98. package/dist/elements/skeleton.js +91 -0
  99. package/dist/elements/slider.d.ts +1 -0
  100. package/dist/elements/slider.js +248 -0
  101. package/dist/elements/spin-button.d.ts +7 -0
  102. package/dist/elements/spin-button.js +499 -0
  103. package/dist/elements/spinner.d.ts +4 -0
  104. package/dist/elements/spinner.js +124 -0
  105. package/dist/elements/swatch-picker.d.ts +5 -0
  106. package/dist/elements/swatch-picker.js +363 -0
  107. package/dist/elements/table.d.ts +21 -0
  108. package/dist/elements/table.js +261 -0
  109. package/dist/elements/tabs.d.ts +1 -0
  110. package/dist/elements/tabs.js +276 -0
  111. package/dist/elements/tag-picker.d.ts +29 -0
  112. package/dist/elements/tag-picker.js +697 -0
  113. package/dist/elements/tags.d.ts +21 -0
  114. package/dist/elements/tags.js +421 -0
  115. package/dist/elements/teaching-popover.d.ts +32 -0
  116. package/dist/elements/teaching-popover.js +412 -0
  117. package/dist/elements/text.d.ts +5 -0
  118. package/dist/elements/text.js +69 -0
  119. package/dist/elements/textarea.d.ts +1 -0
  120. package/dist/elements/textarea.js +182 -0
  121. package/dist/elements/toast.d.ts +25 -0
  122. package/dist/elements/toast.js +337 -0
  123. package/dist/elements/toaster.d.ts +6 -0
  124. package/dist/elements/toaster.js +356 -0
  125. package/dist/elements/toggle.d.ts +2 -0
  126. package/dist/elements/toggle.js +246 -0
  127. package/dist/elements/toolbar.d.ts +12 -0
  128. package/dist/elements/toolbar.js +381 -0
  129. package/dist/elements/tooltip.d.ts +76 -0
  130. package/dist/elements/tooltip.js +510 -0
  131. package/dist/elements/tree-model.d.ts +19 -0
  132. package/dist/elements/tree-model.js +34 -0
  133. package/dist/elements/tree.d.ts +5 -0
  134. package/dist/elements/tree.js +273 -0
  135. package/dist/elements/view-card.d.ts +2 -0
  136. package/dist/elements/view-card.js +78 -0
  137. package/dist/elements/virtual-list.d.ts +41 -0
  138. package/dist/elements/virtual-list.js +369 -0
  139. package/dist/elements.d.ts +72 -0
  140. package/dist/elements.js +207 -0
  141. package/dist/index.d.ts +76 -0
  142. package/dist/index.js +66 -0
  143. package/dist/react-theme-styles.d.ts +2 -0
  144. package/dist/react-theme-styles.js +75 -0
  145. package/dist/react.d.ts +4540 -0
  146. package/dist/react.js +6799 -0
  147. package/dist/theme.d.ts +169 -0
  148. package/dist/theme.js +167 -0
  149. package/dist/virtualizer-hooks.d.ts +80 -0
  150. package/dist/virtualizer-hooks.js +253 -0
  151. package/package.json +87 -4
  152. package/scripts/generate-token-index.mjs +43 -0
  153. package/scripts/sync-component-catalog-signatures.mjs +55 -0
  154. package/scripts/validate-component-catalog.mjs +44 -0
  155. package/skills/sharepoint-ui/SKILL.md +12 -0
  156. package/skills/sharepoint-ui/references/choosing-components.md +20 -0
  157. package/skills/sharepoint-ui/references/sharepoint-patterns.md +17 -0
  158. package/tokens.json +3777 -0
@@ -0,0 +1 @@
1
+ export declare function defineDatePicker(): void;
@@ -0,0 +1,486 @@
1
+ import { base, syncHostAccessibleName } from "./shared.js";
2
+ const isoDate = (date) => `${date.getFullYear().toString().padStart(4, "0")}-${(date.getMonth() + 1).toString().padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`;
3
+ const fromIso = (value) => {
4
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
5
+ if (!match)
6
+ return undefined;
7
+ const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
8
+ return date.getFullYear() === Number(match[1]) && date.getMonth() === Number(match[2]) - 1 && date.getDate() === Number(match[3]) ? date : undefined;
9
+ };
10
+ const addDays = (date, amount) => new Date(date.getFullYear(), date.getMonth(), date.getDate() + amount);
11
+ const addMonths = (date, amount) => new Date(date.getFullYear(), date.getMonth() + amount, Math.min(date.getDate(), new Date(date.getFullYear(), date.getMonth() + amount + 1, 0).getDate()));
12
+ export function defineDatePicker() {
13
+ if (customElements.get("spui-date-picker"))
14
+ return;
15
+ class SpuiDatePicker extends HTMLElement {
16
+ static formAssociated = true;
17
+ static observedAttributes = ["label", "value", "min", "max", "disabled", "required", "read-only", "name", "placeholder", "aria-label", "aria-labelledby", "aria-description", "aria-describedby", "aria-invalid", "previous-month-label", "next-month-label", "calendar-label", "invalid-date-message", "required-message"];
18
+ internals;
19
+ currentValue;
20
+ formDisabled = false;
21
+ textInvalid = false;
22
+ customError = "";
23
+ viewDate = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
24
+ activeDate = new Date();
25
+ onDocumentPointer = (event) => { if (!this.contains(event.target) && !this.shadowRoot?.contains(event.target))
26
+ this.closeCalendar(false); };
27
+ onDocumentKey = (event) => { if (event.key === "Escape" && !this.shadowRoot?.querySelector(".calendar")?.hidden) {
28
+ event.preventDefault();
29
+ this.closeCalendar(true);
30
+ } };
31
+ constructor() {
32
+ super();
33
+ try {
34
+ this.internals = this.attachInternals();
35
+ }
36
+ catch { /* Keep the date control usable when ElementInternals is unavailable. */ }
37
+ }
38
+ connectedCallback() { this.render(); }
39
+ disconnectedCallback() { document.removeEventListener("pointerdown", this.onDocumentPointer, true); document.removeEventListener("keydown", this.onDocumentKey, true); }
40
+ attributeChangedCallback(name, _oldValue, value) {
41
+ if (!this.isConnected)
42
+ return;
43
+ if (name === "value") {
44
+ this.currentValue = value ?? "";
45
+ this.textInvalid = false;
46
+ this.syncInput();
47
+ }
48
+ else if (name === "label") {
49
+ const label = this.shadowRoot?.querySelector("label");
50
+ if (label)
51
+ label.textContent = value ?? "";
52
+ }
53
+ else if (name === "disabled" || name === "read-only")
54
+ this.syncDisabled();
55
+ else if (name === "required") {
56
+ const input = this.shadowRoot?.querySelector(".control input");
57
+ if (input) {
58
+ input.required = this.hasAttribute("required");
59
+ if (input.required)
60
+ input.setAttribute("aria-required", "true");
61
+ else
62
+ input.removeAttribute("aria-required");
63
+ }
64
+ }
65
+ else if (name === "placeholder")
66
+ this.syncPlaceholder();
67
+ else if (name === "aria-label" || name === "aria-labelledby" || name === "aria-description" || name === "aria-describedby" || name === "aria-invalid")
68
+ this.syncInputAttributes();
69
+ else if (name === "calendar-label")
70
+ this.shadowRoot?.querySelector(".calendar-button")?.setAttribute("aria-label", this.labelText("calendar"));
71
+ else
72
+ this.renderCalendar();
73
+ this.syncFormState();
74
+ }
75
+ get value() { return this.currentValue ?? this.getAttribute("value") ?? ""; }
76
+ set value(value) { this.currentValue = value ?? ""; this.textInvalid = false; this.syncInput(); this.syncFormState(); }
77
+ get defaultValue() { return this.getAttribute("value") ?? ""; }
78
+ set defaultValue(value) { this.setAttribute("value", value); }
79
+ get form() { return this.internals?.form ?? null; }
80
+ get validity() { return this.internals?.validity ?? this.shadowRoot?.querySelector("input")?.validity; }
81
+ get validationMessage() { return this.internals?.validationMessage ?? this.shadowRoot?.querySelector("input")?.validationMessage ?? ""; }
82
+ get willValidate() { return this.internals?.willValidate ?? !this.hasAttribute("disabled"); }
83
+ checkValidity() { return this.internals?.checkValidity() ?? this.validity?.valid ?? true; }
84
+ reportValidity() { return this.internals?.reportValidity() ?? this.checkValidity(); }
85
+ setCustomValidity(message) { this.customError = message; this.syncFormState(); }
86
+ focus(options) { this.shadowRoot?.querySelector("input")?.focus(options); }
87
+ blur() { this.shadowRoot?.querySelector("input")?.blur(); }
88
+ formDisabledCallback(disabled) { this.formDisabled = disabled; this.syncDisabled(); }
89
+ formResetCallback() { this.value = this.defaultValue; this.closeCalendar(false); }
90
+ formStateRestoreCallback(state) { if (typeof state === "string")
91
+ this.value = state; }
92
+ locale() { return this.closest("[lang]")?.getAttribute("lang") || document.documentElement.lang || navigator.language || "en-US"; }
93
+ direction() { return getComputedStyle(this).direction === "rtl" ? "rtl" : "ltr"; }
94
+ labelText(key) {
95
+ const attribute = { previous: "previous-month-label", next: "next-month-label", calendar: "calendar-label", invalid: "invalid-date-message", required: "required-message" }[key];
96
+ const override = this.getAttribute(attribute);
97
+ if (override)
98
+ return override;
99
+ const locale = this.locale();
100
+ const language = locale.split("-")[0].toLowerCase();
101
+ const values = {
102
+ ar: { previous: "الشهر السابق", next: "الشهر التالي", calendar: "التقويم", invalid: "أدخل تاريخًا صالحًا.", required: "اختر تاريخًا." },
103
+ de: { previous: "Vorheriger Monat", next: "Nächster Monat", calendar: "Kalender öffnen", invalid: "Geben Sie ein gültiges Datum ein.", required: "Wählen Sie ein Datum aus." },
104
+ es: { previous: "Mes anterior", next: "Mes siguiente", calendar: "Abrir calendario", invalid: "Introduzca una fecha válida.", required: "Seleccione una fecha." },
105
+ fr: { previous: "Mois précédent", next: "Mois suivant", calendar: "Ouvrir le calendrier", invalid: "Saisissez une date valide.", required: "Choisissez une date." },
106
+ ja: { previous: "前の月", next: "次の月", calendar: "カレンダーを開く", invalid: "有効な日付を入力してください。", required: "日付を選択してください。" },
107
+ pt: { previous: "Mês anterior", next: "Próximo mês", calendar: "Abrir calendário", invalid: "Insira uma data válida.", required: "Escolha uma data." },
108
+ };
109
+ const fallback = { previous: "Previous month", next: "Next month", calendar: "Open calendar", invalid: "Enter a valid date.", required: "Choose a date." };
110
+ return values[language]?.[key] ?? fallback[key];
111
+ }
112
+ formatter(options) { return new Intl.DateTimeFormat(this.locale(), options); }
113
+ formatDate(date) { return this.formatter({ year: "numeric", month: "numeric", day: "numeric" }).format(date); }
114
+ placeholder() {
115
+ const date = new Date(2006, 10, 22);
116
+ return this.formatter({ year: "numeric", month: "2-digit", day: "2-digit" }).formatToParts(date).map(part => part.type === "year" ? "YYYY" : part.type === "month" ? "MM" : part.type === "day" ? "DD" : part.value).join("");
117
+ }
118
+ parseText(value) {
119
+ const sample = this.formatter({ year: "numeric", month: "2-digit", day: "2-digit" }).formatToParts(new Date(2006, 10, 22));
120
+ const order = sample.filter(part => ["year", "month", "day"].includes(part.type)).map(part => part.type);
121
+ const digitMap = new Map();
122
+ for (let digit = 0; digit <= 9; digit++)
123
+ digitMap.set(new Intl.NumberFormat(this.locale(), { useGrouping: false }).format(digit), String(digit));
124
+ const normalized = [...value].map(char => digitMap.get(char) ?? char).join("");
125
+ const numbers = normalized.match(/\p{Decimal_Number}+/gu)?.map(Number) ?? [];
126
+ if (numbers.length !== 3)
127
+ return undefined;
128
+ const parts = {};
129
+ order.forEach((part, index) => { parts[part] = numbers[index]; });
130
+ const date = new Date(parts.year, parts.month - 1, parts.day);
131
+ return date.getFullYear() === parts.year && date.getMonth() === parts.month - 1 && date.getDate() === parts.day ? date : undefined;
132
+ }
133
+ firstDayOfWeek() {
134
+ try {
135
+ const locale = new Intl.Locale(this.locale());
136
+ const weekInfo = locale.weekInfo ?? locale.getWeekInfo?.();
137
+ if (weekInfo)
138
+ return weekInfo.firstDay % 7;
139
+ }
140
+ catch { /* Older browsers use the Sunday-first fallback. */ }
141
+ return 0;
142
+ }
143
+ clampDate(date) {
144
+ const min = fromIso(this.getAttribute("min") ?? "");
145
+ const max = fromIso(this.getAttribute("max") ?? "");
146
+ if (min && date < min)
147
+ return min;
148
+ if (max && date > max)
149
+ return max;
150
+ return date;
151
+ }
152
+ isDisabled(date) {
153
+ const value = isoDate(date);
154
+ return this.hasAttribute("disabled") || this.formDisabled || Boolean(this.getAttribute("min") && value < this.getAttribute("min")) || Boolean(this.getAttribute("max") && value > this.getAttribute("max"));
155
+ }
156
+ isOutOfRange(date) { const value = isoDate(date); return Boolean((this.getAttribute("min") && value < this.getAttribute("min")) || (this.getAttribute("max") && value > this.getAttribute("max"))); }
157
+ render() {
158
+ const shadow = this.shadowRoot ?? this.attachShadow({ mode: "open" });
159
+ const id = `date-${crypto.randomUUID()}`;
160
+ const label = document.createElement("label");
161
+ label.htmlFor = `${id}-input`;
162
+ label.textContent = this.getAttribute("label") ?? "";
163
+ label.part.add("label");
164
+ const control = document.createElement("div");
165
+ control.className = "control";
166
+ const input = document.createElement("input");
167
+ input.id = `${id}-input`;
168
+ input.type = "text";
169
+ input.autocomplete = "off";
170
+ input.setAttribute("role", "combobox");
171
+ input.setAttribute("aria-haspopup", "dialog");
172
+ input.setAttribute("aria-expanded", "false");
173
+ input.setAttribute("aria-controls", `${id}-calendar`);
174
+ input.part.add("control");
175
+ const calendarButton = document.createElement("button");
176
+ calendarButton.type = "button";
177
+ calendarButton.className = "calendar-button";
178
+ calendarButton.setAttribute("aria-label", this.labelText("calendar"));
179
+ calendarButton.setAttribute("aria-haspopup", "dialog");
180
+ calendarButton.setAttribute("aria-expanded", "false");
181
+ calendarButton.setAttribute("aria-controls", `${id}-calendar`);
182
+ calendarButton.part.add("calendar-button");
183
+ calendarButton.innerHTML = '<svg aria-hidden="true" viewBox="0 0 20 20" width="18" height="18" fill="none"><path d="M6 2.75v2.5m8-2.5v2.5M3.75 7.25h12.5m-11-3h9.5c.83 0 1.5.67 1.5 1.5v10c0 .83-.67 1.5-1.5 1.5h-9.5c-.83 0-1.5-.67-1.5-1.5v-10c0-.83.67-1.5 1.5-1.5Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>';
184
+ const calendar = document.createElement("div");
185
+ calendar.id = `${id}-calendar`;
186
+ calendar.className = "calendar";
187
+ calendar.setAttribute("role", "dialog");
188
+ calendar.setAttribute("aria-modal", "false");
189
+ calendar.hidden = true;
190
+ calendar.part.add("calendar");
191
+ control.append(input, calendarButton);
192
+ shadow.replaceChildren(document.createElement("style"), label, control, calendar);
193
+ const style = shadow.querySelector("style");
194
+ style.textContent = `${base}:host{position:relative;display:grid;gap:var(--spui-control-gap,6px);min-width:0;color:var(--spui-ui-text,var(--spui-semantic-body-text,inherit));font:inherit}.control{display:flex;position:relative;min-width:0}.control input{box-sizing:border-box;width:100%;min-width:0;min-height:var(--spui-control-height,36px);padding-inline:var(--spui-control-padding-inline,12px) 42px;border:1px solid var(--spui-control-border,var(--spui-ui-stroke,var(--spui-semantic-input-border,var(--colorNeutralStroke1,#8a8886))));border-radius:var(--spui-control-radius,4px);background:var(--spui-control-background,var(--spui-ui-surface,var(--spui-semantic-input-background,var(--colorNeutralBackground1,#fff))));box-shadow:var(--spui-control-shadow,none);color:inherit;font:inherit}.control input:focus-visible,.calendar-button:focus-visible,.calendar button:focus-visible{outline:2px solid var(--spui-focus-ring,var(--spui-ui-accent,#0078d4));outline-offset:1px}.control input[aria-invalid=true]{border-color:var(--spui-semantic-danger,var(--colorPaletteRedBorderActive,#d13438))}.calendar-button{position:absolute;inset-block:2px;inset-inline-end:2px;display:grid;place-items:center;width:36px;border:0;border-radius:var(--spui-control-radius,4px);background:transparent;color:var(--spui-ui-muted,var(--colorNeutralForeground2,#616161));cursor:pointer}.calendar-button:hover{background:var(--spui-ui-surface-hover,var(--colorNeutralBackground1Hover,#f5f5f5));color:var(--spui-ui-text,inherit)}label{font-size:14px;font-weight:600;color:var(--spui-ui-muted,var(--spui-semantic-body-text,inherit))}.calendar{position:absolute;inset-block-start:calc(100% + 4px);inset-inline-start:0;z-index:var(--spui-layer-dropdown,20);width:min(320px,calc(100vw - 24px));padding:12px;border:1px solid var(--spui-ui-stroke,var(--colorNeutralStroke1,#d1d1d1));border-radius:var(--spui-card-radius,8px);background:var(--spui-ui-surface,var(--colorNeutralBackground1,#fff));box-shadow:var(--spui-card-shadow,0 4px 16px #0002);color:var(--spui-ui-text,var(--colorNeutralForeground1,#242424));direction:inherit}.calendar[hidden]{display:none}.calendar-header{display:grid;grid-template-columns:36px 1fr 36px;align-items:center;gap:4px;margin-block-end:8px}.month-label{text-align:center;font-weight:600}.calendar button{font:inherit}.month-nav{display:grid;place-items:center;width:36px;height:36px;border:0;border-radius:var(--spui-control-radius,4px);background:transparent;color:inherit;cursor:pointer}.month-nav:hover{background:var(--spui-ui-surface-hover,#f5f5f5)}.weekdays{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:2px}.days{display:grid;gap:2px}.days>[role=row]{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:2px}.weekday{display:grid;place-items:center;height:32px;color:var(--spui-ui-muted,var(--colorNeutralForeground2,#616161));font-size:12px;font-weight:600}.day{display:grid;place-items:center;min-width:0;aspect-ratio:1;border:0;border-radius:50%;background:transparent;color:inherit;cursor:pointer}.day:hover:not(:disabled){background:var(--spui-ui-surface-hover,var(--colorNeutralBackground1Hover,#f5f5f5))}.day[aria-selected=true]{background:var(--spui-ui-accent,var(--colorBrandBackground,#0f6cbd));color:var(--spui-ui-accent-text,var(--colorNeutralForegroundOnBrand,#fff))}.day[aria-current=date]{outline:1px solid var(--spui-ui-accent,var(--colorBrandForeground1,#0f6cbd));outline-offset:-2px}.day.outside-month{color:var(--spui-ui-muted,var(--colorNeutralForeground3,#a19f9d))}.day:disabled{opacity:.42;cursor:default}.calendar[dir=rtl]{direction:rtl}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important}}@media(forced-colors:active){.calendar,.control input{border-color:FieldText}.day[aria-selected=true]{outline:2px solid Highlight}.calendar-button,.month-nav,.day{forced-color-adjust:auto}}`;
195
+ style.textContent += `.control input:disabled,.calendar-button:disabled,.month-nav:disabled{cursor:not-allowed;opacity:.5}.calendar-button:disabled:hover,.month-nav:disabled:hover{background:transparent}.day[aria-disabled=true]{opacity:.42;cursor:default}`;
196
+ this.syncInput();
197
+ this.syncInputAttributes();
198
+ this.syncDisabled();
199
+ this.syncPlaceholder();
200
+ input.required = this.hasAttribute("required");
201
+ if (input.required)
202
+ input.setAttribute("aria-required", "true");
203
+ input.addEventListener("input", () => { this.currentValue = ""; const text = input.value.trim(); const candidate = fromIso(text) ?? this.parseText(text); this.textInvalid = Boolean(text && (!candidate || (candidate && this.isOutOfRange(candidate)))); this.syncFormState(); });
204
+ input.addEventListener("change", () => this.commitText());
205
+ input.addEventListener("blur", () => { if (!calendar.contains(document.activeElement))
206
+ this.commitText(); });
207
+ input.addEventListener("click", () => this.openCalendar());
208
+ input.addEventListener("keydown", event => {
209
+ if (event.key === "ArrowDown") {
210
+ event.preventDefault();
211
+ this.openCalendar();
212
+ this.focusActiveDate();
213
+ }
214
+ else if (event.key === "Enter") {
215
+ event.preventDefault();
216
+ if (!calendar.hidden)
217
+ this.selectDate(this.activeDate);
218
+ else
219
+ this.commitText();
220
+ }
221
+ });
222
+ calendarButton.addEventListener("click", () => calendar.hidden ? this.openCalendar() : this.closeCalendar(true));
223
+ calendar.addEventListener("click", event => {
224
+ const target = event.target.closest("button[data-date],button[data-month]");
225
+ if (!target || target.disabled)
226
+ return;
227
+ if (target.dataset.month) {
228
+ this.viewDate = addMonths(this.viewDate, Number(target.dataset.month));
229
+ this.activeDate = this.clampDate(new Date(this.viewDate.getFullYear(), this.viewDate.getMonth(), 1));
230
+ this.renderCalendar();
231
+ this.focusActiveDate();
232
+ }
233
+ else {
234
+ const date = fromIso(target.dataset.date ?? "");
235
+ if (date)
236
+ this.selectDate(date);
237
+ }
238
+ });
239
+ calendar.addEventListener("keydown", event => this.onCalendarKey(event));
240
+ document.addEventListener("pointerdown", this.onDocumentPointer, true);
241
+ document.addEventListener("keydown", this.onDocumentKey, true);
242
+ this.syncFormState();
243
+ this.renderCalendar();
244
+ }
245
+ syncInput() {
246
+ const input = this.shadowRoot?.querySelector(".control input");
247
+ if (!input)
248
+ return;
249
+ const date = fromIso(this.value);
250
+ input.value = date ? this.formatDate(date) : this.value;
251
+ if (date) {
252
+ this.activeDate = date;
253
+ this.viewDate = new Date(date.getFullYear(), date.getMonth(), 1);
254
+ this.renderCalendar();
255
+ }
256
+ }
257
+ syncInputAttributes() {
258
+ const input = this.shadowRoot?.querySelector(".control input");
259
+ if (!input)
260
+ return;
261
+ syncHostAccessibleName(this, input);
262
+ const invalid = this.getAttribute("aria-invalid");
263
+ if (invalid === null)
264
+ input.removeAttribute("aria-invalid");
265
+ else
266
+ input.setAttribute("aria-invalid", invalid);
267
+ }
268
+ syncPlaceholder() { const input = this.shadowRoot?.querySelector(".control input"); if (input)
269
+ input.placeholder = this.getAttribute("placeholder") ?? this.placeholder(); }
270
+ syncDisabled() {
271
+ const disabled = this.formDisabled || this.hasAttribute("disabled");
272
+ const input = this.shadowRoot?.querySelector(".control input");
273
+ const button = this.shadowRoot?.querySelector(".calendar-button");
274
+ if (input)
275
+ input.disabled = disabled;
276
+ if (button)
277
+ button.disabled = disabled || this.hasAttribute("read-only");
278
+ if (input)
279
+ input.readOnly = this.hasAttribute("read-only");
280
+ if (disabled || this.hasAttribute("read-only"))
281
+ this.closeCalendar(false);
282
+ }
283
+ syncFormState() {
284
+ const input = this.shadowRoot?.querySelector(".control input");
285
+ const parsed = fromIso(this.value);
286
+ const empty = this.value.trim() === "";
287
+ const textDate = parsed ?? this.parseText(input?.value ?? "");
288
+ const validText = empty || Boolean(textDate);
289
+ const required = this.hasAttribute("required");
290
+ const underflow = Boolean(parsed && this.getAttribute("min") && this.value < this.getAttribute("min"));
291
+ const overflow = Boolean(parsed && this.getAttribute("max") && this.value > this.getAttribute("max"));
292
+ const invalid = this.getAttribute("aria-invalid") === "true" || !validText || this.textInvalid || (required && empty) || underflow || overflow || Boolean(this.customError);
293
+ if (input) {
294
+ if (invalid)
295
+ input.setAttribute("aria-invalid", "true");
296
+ else
297
+ input.removeAttribute("aria-invalid");
298
+ }
299
+ this.internals?.setFormValue(parsed && !underflow && !overflow ? this.value : null);
300
+ const validity = { valueMissing: required && empty, badInput: !validText || this.textInvalid, rangeUnderflow: underflow, rangeOverflow: overflow, customError: Boolean(this.customError) };
301
+ try {
302
+ this.internals?.setValidity(invalid ? validity : {}, invalid ? this.customError || (required && empty ? this.labelText("required") : this.labelText("invalid")) : "", input ?? undefined);
303
+ }
304
+ catch { /* The control still exposes its inline invalid state without ElementInternals. */ }
305
+ }
306
+ commitText() {
307
+ const input = this.shadowRoot?.querySelector(".control input");
308
+ if (!input || this.hasAttribute("read-only"))
309
+ return;
310
+ const text = input.value.trim();
311
+ if (!text) {
312
+ this.currentValue = "";
313
+ this.textInvalid = false;
314
+ this.syncFormState();
315
+ this.dispatchChange();
316
+ return;
317
+ }
318
+ const date = fromIso(text) ?? this.parseText(text);
319
+ if (!date || this.isOutOfRange(date) || this.isDisabled(date)) {
320
+ this.textInvalid = true;
321
+ input.setAttribute("aria-invalid", "true");
322
+ this.syncFormState();
323
+ return;
324
+ }
325
+ this.textInvalid = false;
326
+ this.selectDate(date, false);
327
+ }
328
+ dispatchChange() { this.dispatchEvent(new CustomEvent("spui-change", { detail: { value: this.value }, bubbles: true, composed: true })); }
329
+ selectDate(date, close = true) {
330
+ if (this.isDisabled(date) || this.hasAttribute("read-only"))
331
+ return;
332
+ this.currentValue = isoDate(date);
333
+ this.textInvalid = false;
334
+ const input = this.shadowRoot?.querySelector(".control input");
335
+ if (input)
336
+ input.value = this.formatDate(date);
337
+ this.syncFormState();
338
+ this.dispatchChange();
339
+ if (close)
340
+ this.closeCalendar(true);
341
+ }
342
+ openCalendar() {
343
+ if (this.hasAttribute("disabled") || this.hasAttribute("read-only") || this.formDisabled)
344
+ return;
345
+ const calendar = this.shadowRoot?.querySelector(".calendar");
346
+ const input = this.shadowRoot?.querySelector(".control input");
347
+ if (!calendar)
348
+ return;
349
+ const current = fromIso(this.value) ?? this.parseText(input?.value ?? "") ?? this.clampDate(new Date());
350
+ this.activeDate = current;
351
+ this.viewDate = new Date(current.getFullYear(), current.getMonth(), 1);
352
+ calendar.hidden = false;
353
+ input?.setAttribute("aria-expanded", "true");
354
+ this.shadowRoot?.querySelector(".calendar-button")?.setAttribute("aria-expanded", "true");
355
+ this.renderCalendar();
356
+ }
357
+ closeCalendar(restoreFocus) {
358
+ const calendar = this.shadowRoot?.querySelector(".calendar");
359
+ if (!calendar || calendar.hidden)
360
+ return;
361
+ calendar.hidden = true;
362
+ this.shadowRoot?.querySelector(".control input")?.setAttribute("aria-expanded", "false");
363
+ this.shadowRoot?.querySelector(".calendar-button")?.setAttribute("aria-expanded", "false");
364
+ if (restoreFocus)
365
+ this.shadowRoot?.querySelector(".control input")?.focus();
366
+ }
367
+ focusActiveDate() { this.shadowRoot?.querySelector(`button[data-date="${isoDate(this.activeDate)}"]`)?.focus(); }
368
+ onCalendarKey(event) {
369
+ const current = event.target.closest("button[data-date]");
370
+ if (!current)
371
+ return;
372
+ let next;
373
+ const horizontal = this.direction() === "rtl" ? -1 : 1;
374
+ if (event.key === "ArrowRight")
375
+ next = addDays(this.activeDate, horizontal);
376
+ else if (event.key === "ArrowLeft")
377
+ next = addDays(this.activeDate, -horizontal);
378
+ else if (event.key === "ArrowDown")
379
+ next = addDays(this.activeDate, 7);
380
+ else if (event.key === "ArrowUp")
381
+ next = addDays(this.activeDate, -7);
382
+ else if (event.key === "Home" || event.key === "End") {
383
+ const offset = (this.activeDate.getDay() - this.firstDayOfWeek() + 7) % 7;
384
+ next = addDays(this.activeDate, event.key === "Home" ? -offset : 6 - offset);
385
+ }
386
+ else if (event.key === "PageUp")
387
+ next = addMonths(this.activeDate, event.shiftKey ? -12 : -1);
388
+ else if (event.key === "PageDown")
389
+ next = addMonths(this.activeDate, event.shiftKey ? 12 : 1);
390
+ else if (event.key === "Enter" || event.key === " ") {
391
+ event.preventDefault();
392
+ this.selectDate(this.activeDate);
393
+ return;
394
+ }
395
+ else
396
+ return;
397
+ event.preventDefault();
398
+ this.activeDate = next;
399
+ this.viewDate = new Date(this.activeDate.getFullYear(), this.activeDate.getMonth(), 1);
400
+ this.renderCalendar();
401
+ this.focusActiveDate();
402
+ }
403
+ renderCalendar() {
404
+ const calendar = this.shadowRoot?.querySelector(".calendar");
405
+ if (!calendar)
406
+ return;
407
+ calendar.setAttribute("aria-label", this.formatter({ month: "long", year: "numeric" }).format(this.viewDate));
408
+ calendar.dir = this.direction();
409
+ const header = document.createElement("div");
410
+ header.className = "calendar-header";
411
+ const monthKey = `${this.viewDate.getFullYear().toString().padStart(4, "0")}-${(this.viewDate.getMonth() + 1).toString().padStart(2, "0")}`;
412
+ const previous = document.createElement("button");
413
+ previous.type = "button";
414
+ previous.className = "month-nav";
415
+ previous.dataset.month = "-1";
416
+ previous.setAttribute("aria-label", this.labelText("previous"));
417
+ previous.innerHTML = this.direction() === "rtl" ? "&#8250;" : "&#8249;";
418
+ previous.disabled = Boolean(this.getAttribute("min") && monthKey <= this.getAttribute("min").slice(0, 7));
419
+ const title = document.createElement("div");
420
+ title.className = "month-label";
421
+ title.textContent = this.formatter({ month: "long", year: "numeric" }).format(this.viewDate);
422
+ title.setAttribute("aria-live", "polite");
423
+ const next = document.createElement("button");
424
+ next.type = "button";
425
+ next.className = "month-nav";
426
+ next.dataset.month = "1";
427
+ next.setAttribute("aria-label", this.labelText("next"));
428
+ next.innerHTML = this.direction() === "rtl" ? "&#8249;" : "&#8250;";
429
+ next.disabled = Boolean(this.getAttribute("max") && monthKey >= this.getAttribute("max").slice(0, 7));
430
+ header.append(previous, title, next);
431
+ const weekdays = document.createElement("div");
432
+ weekdays.className = "weekdays";
433
+ weekdays.setAttribute("role", "row");
434
+ const weekStart = this.firstDayOfWeek();
435
+ const weekBase = new Date(2024, 0, 7 + weekStart);
436
+ for (let index = 0; index < 7; index++) {
437
+ const day = addDays(weekBase, index);
438
+ const cell = document.createElement("span");
439
+ cell.className = "weekday";
440
+ cell.setAttribute("role", "columnheader");
441
+ cell.setAttribute("aria-label", this.formatter({ weekday: "long" }).format(day));
442
+ cell.textContent = this.formatter({ weekday: "short" }).format(day);
443
+ weekdays.append(cell);
444
+ }
445
+ const grid = document.createElement("div");
446
+ grid.className = "days";
447
+ grid.setAttribute("role", "grid");
448
+ grid.setAttribute("aria-label", title.textContent);
449
+ grid.append(weekdays);
450
+ const monthStart = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth(), 1);
451
+ const offset = (monthStart.getDay() - weekStart + 7) % 7;
452
+ const count = new Date(this.viewDate.getFullYear(), this.viewDate.getMonth() + 1, 0).getDate();
453
+ const rows = Math.ceil((offset + count) / 7);
454
+ const selected = this.value;
455
+ for (let rowIndex = 0; rowIndex < rows; rowIndex++) {
456
+ const row = document.createElement("div");
457
+ row.setAttribute("role", "row");
458
+ for (let columnIndex = 0; columnIndex < 7; columnIndex++) {
459
+ const date = addDays(monthStart, rowIndex * 7 + columnIndex - offset);
460
+ const value = isoDate(date);
461
+ const button = document.createElement("button");
462
+ button.type = "button";
463
+ button.className = "day";
464
+ button.dataset.date = value;
465
+ button.textContent = this.formatter({ day: "numeric" }).format(date);
466
+ button.setAttribute("role", "gridcell");
467
+ button.setAttribute("aria-label", this.formatter({ dateStyle: "full" }).format(date));
468
+ if (value === selected)
469
+ button.setAttribute("aria-selected", "true");
470
+ if (value === isoDate(new Date()))
471
+ button.setAttribute("aria-current", "date");
472
+ if (date.getMonth() !== this.viewDate.getMonth())
473
+ button.classList.add("outside-month");
474
+ if (this.isOutOfRange(date))
475
+ button.setAttribute("aria-disabled", "true");
476
+ button.disabled = this.hasAttribute("disabled") || this.formDisabled;
477
+ button.tabIndex = value === isoDate(this.activeDate) && !button.disabled ? 0 : -1;
478
+ row.append(button);
479
+ }
480
+ grid.append(row);
481
+ }
482
+ calendar.replaceChildren(header, grid);
483
+ }
484
+ }
485
+ customElements.define("spui-date-picker", SpuiDatePicker);
486
+ }
@@ -0,0 +1,23 @@
1
+ export type DialogOpenChangeData = {
2
+ type: "escapeKeyDown";
3
+ open: boolean;
4
+ event: KeyboardEvent;
5
+ trigger?: HTMLElement;
6
+ } | {
7
+ type: "backdropClick";
8
+ open: boolean;
9
+ event: MouseEvent;
10
+ trigger?: HTMLElement;
11
+ } | {
12
+ type: "triggerClick";
13
+ open: boolean;
14
+ event: MouseEvent;
15
+ trigger?: HTMLElement;
16
+ };
17
+ export declare function defineDialogSurface(): void;
18
+ export declare function defineDialogTitle(): void;
19
+ export declare function defineDialogContent(): void;
20
+ export declare function defineDialogBody(): void;
21
+ export declare function defineDialogActions(): void;
22
+ export declare function defineDialogTrigger(): void;
23
+ export declare function defineDialog(): void;