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

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 (156) hide show
  1. package/AGENTS.md +211 -0
  2. package/LICENSE +21 -0
  3. package/README.md +897 -2
  4. package/components.json +5811 -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 +125 -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 +9 -0
  118. package/dist/elements/text.js +75 -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.d.ts +4546 -0
  144. package/dist/react.js +6837 -0
  145. package/dist/theme.d.ts +169 -0
  146. package/dist/theme.js +167 -0
  147. package/dist/virtualizer-hooks.d.ts +80 -0
  148. package/dist/virtualizer-hooks.js +253 -0
  149. package/package.json +87 -4
  150. package/scripts/generate-token-index.mjs +43 -0
  151. package/scripts/sync-component-catalog-signatures.mjs +55 -0
  152. package/scripts/validate-component-catalog.mjs +44 -0
  153. package/skills/sharepoint-ui/SKILL.md +12 -0
  154. package/skills/sharepoint-ui/references/choosing-components.md +20 -0
  155. package/skills/sharepoint-ui/references/sharepoint-patterns.md +17 -0
  156. package/tokens.json +3777 -0
@@ -0,0 +1,499 @@
1
+ import { base, rememberNativeFormControlDefaults, stopHostAccessibleNameSync, syncHostAccessibleName } from "./shared.js";
2
+ export function defineSpinButton() {
3
+ if (customElements.get("spui-spin-button"))
4
+ return;
5
+ class SpuiSpinButton extends HTMLElement {
6
+ static formAssociated = true;
7
+ static observedAttributes = ["label", "value", "default-value", "display-value", "min", "max", "step", "step-page", "precision", "size", "appearance", "disabled", "required", "read-only", "name", "aria-label", "aria-labelledby", "aria-description", "aria-describedby", "aria-invalid", "aria-valuetext", "increment-label", "decrement-label"];
8
+ internals;
9
+ currentValue;
10
+ formDisabled = false;
11
+ draft;
12
+ previousDraft;
13
+ boundInput;
14
+ slotInputDefaults;
15
+ boundDecrement;
16
+ boundIncrement;
17
+ heldStepDirection = 0;
18
+ heldStepInput;
19
+ heldStepEvent;
20
+ heldStepElapsed = 0;
21
+ heldStepTimer;
22
+ get form() { return this.internals?.form ?? null; }
23
+ constructor() {
24
+ super();
25
+ try {
26
+ this.internals = this.attachInternals();
27
+ }
28
+ catch { /* Native input behavior remains available without ElementInternals. */ }
29
+ }
30
+ connectedCallback() { this.render(); }
31
+ disconnectedCallback() { stopHostAccessibleNameSync(this); this.clearHeldStep(); }
32
+ attributeChangedCallback(name, _oldValue, value) {
33
+ if (!this.isConnected)
34
+ return;
35
+ if (name === "label" || name === "increment-label" || name === "decrement-label") {
36
+ this.render();
37
+ return;
38
+ }
39
+ const input = this.getInput();
40
+ if (!input)
41
+ return;
42
+ if (name === "value") {
43
+ this.currentValue = value ?? "";
44
+ input.value = this.getAttribute("display-value") ?? this.currentValue;
45
+ this.syncAria(input);
46
+ }
47
+ else if (name === "default-value") {
48
+ if (this.getAttribute("value") === null && this.currentValue === undefined) {
49
+ this.currentValue = value ?? "";
50
+ input.value = this.currentValue;
51
+ this.syncAria(input);
52
+ }
53
+ }
54
+ else if (name === "display-value")
55
+ input.value = value ?? this.currentValue ?? "";
56
+ else if (["min", "max", "step"].includes(name)) {
57
+ if (value === null)
58
+ input.removeAttribute(name);
59
+ else
60
+ input.setAttribute(name, value);
61
+ this.updateButtons(input);
62
+ }
63
+ else if (name === "step-page" || name === "precision" || name === "size" || name === "appearance") {
64
+ this.render();
65
+ return;
66
+ }
67
+ else if (name === "disabled")
68
+ input.disabled = this.formDisabled || this.hasAttribute("disabled") || Boolean(this.slotInputDefaults?.disabled);
69
+ else if (name === "required")
70
+ input.required = this.hasAttribute("required") || Boolean(this.slotInputDefaults?.required);
71
+ else if (name === "read-only")
72
+ input.readOnly = this.hasAttribute("read-only") || Boolean(this.slotInputDefaults?.readOnly);
73
+ else if (name === "name") { /* The form-associated host owns the submitted name. */ }
74
+ else if (name === "aria-label" || name === "aria-labelledby" || name === "aria-description" || name === "aria-describedby" || name === "aria-invalid" || name === "aria-valuetext")
75
+ value === null ? input.removeAttribute(name) : input.setAttribute(name, value);
76
+ this.syncAria(input);
77
+ this.syncFormState(input);
78
+ this.updateButtons(input);
79
+ }
80
+ get value() { return this.currentValue ?? this.getAttribute("value") ?? this.getInput()?.value ?? ""; }
81
+ set value(value) {
82
+ this.currentValue = value === null ? "" : String(value);
83
+ const input = this.getInput();
84
+ if (input) {
85
+ input.value = this.getAttribute("display-value") ?? this.currentValue;
86
+ this.syncAria(input);
87
+ this.syncFormState(input);
88
+ this.updateButtons(input);
89
+ }
90
+ }
91
+ get valueAsNumber() { return this.currentValue === "" || this.currentValue === undefined ? NaN : Number(this.currentValue); }
92
+ get defaultValue() {
93
+ const value = this.getAttribute("default-value");
94
+ if (value !== null)
95
+ return value === "" ? null : Number(value);
96
+ const input = this.getInput();
97
+ const fallback = this.shadowRoot?.querySelector(".input-fallback");
98
+ const nativeDefault = input && input !== fallback ? this.slotInputDefaults?.defaultValue ?? input.defaultValue : "";
99
+ return nativeDefault === "" ? null : Number(nativeDefault);
100
+ }
101
+ set defaultValue(value) { if (value === null)
102
+ this.removeAttribute("default-value");
103
+ else
104
+ this.setAttribute("default-value", String(value)); }
105
+ formDisabledCallback(disabled) { this.formDisabled = disabled; this.syncSlots(); }
106
+ formResetCallback() { this.currentValue = this.getAttribute("value") ?? String(this.defaultValue ?? 0); this.draft = undefined; this.previousDraft = undefined; const input = this.getInput(); if (input) {
107
+ input.value = this.getAttribute("display-value") ?? this.currentValue;
108
+ this.syncAria(input);
109
+ this.syncFormState(input);
110
+ this.updateButtons(input);
111
+ } }
112
+ formStateRestoreCallback(state) { if (typeof state === "string")
113
+ this.value = state; }
114
+ numberValue(value = this.currentValue) {
115
+ if (value === null || value === undefined || value.trim() === "")
116
+ return null;
117
+ const number = Number(value);
118
+ return Number.isFinite(number) ? number : null;
119
+ }
120
+ decimals() {
121
+ const precision = Number(this.getAttribute("precision"));
122
+ if (this.hasAttribute("precision") && Number.isFinite(precision))
123
+ return Math.max(0, Math.min(12, Math.trunc(precision)));
124
+ const step = this.getAttribute("step") ?? "1";
125
+ const parts = step.toLowerCase().split("e");
126
+ const fraction = (parts[0].split(".")[1] ?? "").length - Number(parts[1] ?? 0);
127
+ return Math.max(0, Math.min(12, fraction));
128
+ }
129
+ rounded(value) { const scale = 10 ** this.decimals(); return Math.round((value + Number.EPSILON) * scale) / scale; }
130
+ bounds(value) {
131
+ const min = this.hasAttribute("min") ? Number(this.getAttribute("min")) : -Infinity;
132
+ const max = this.hasAttribute("max") ? Number(this.getAttribute("max")) : Infinity;
133
+ return Math.min(max, Math.max(min, this.rounded(value)));
134
+ }
135
+ syncAria(input) {
136
+ syncHostAccessibleName(this, input);
137
+ const value = this.numberValue();
138
+ const min = this.getAttribute("min");
139
+ const max = this.getAttribute("max");
140
+ if (min === null)
141
+ input.removeAttribute("aria-valuemin");
142
+ else
143
+ input.setAttribute("aria-valuemin", min);
144
+ if (max === null)
145
+ input.removeAttribute("aria-valuemax");
146
+ else
147
+ input.setAttribute("aria-valuemax", max);
148
+ if (value === null)
149
+ input.removeAttribute("aria-valuenow");
150
+ else
151
+ input.setAttribute("aria-valuenow", String(value));
152
+ const display = this.getAttribute("display-value");
153
+ const valueText = this.getAttribute("aria-valuetext") ?? display;
154
+ if (valueText)
155
+ input.setAttribute("aria-valuetext", valueText);
156
+ else
157
+ input.removeAttribute("aria-valuetext");
158
+ }
159
+ syncFormState(input) {
160
+ const value = this.numberValue();
161
+ this.internals?.setFormValue(value === null ? null : String(value));
162
+ const v = input.validity;
163
+ const flags = { valueMissing: v.valueMissing, rangeUnderflow: v.rangeUnderflow, rangeOverflow: v.rangeOverflow, stepMismatch: v.stepMismatch, badInput: v.badInput, typeMismatch: v.typeMismatch, patternMismatch: v.patternMismatch };
164
+ try {
165
+ this.internals?.setValidity(v.valid ? {} : flags, input.validationMessage, input);
166
+ }
167
+ catch { /* Keep native input validation when shadow anchors are unsupported. */ }
168
+ }
169
+ updateButtons(input) {
170
+ const value = this.numberValue();
171
+ const min = this.hasAttribute("min") ? Number(this.getAttribute("min")) : -Infinity;
172
+ const max = this.hasAttribute("max") ? Number(this.getAttribute("max")) : Infinity;
173
+ const disabled = input.disabled || input.readOnly;
174
+ const down = this.getStepper("decrement");
175
+ const up = this.getStepper("increment");
176
+ if (down)
177
+ down.disabled = disabled || (value !== null && value <= min);
178
+ if (up)
179
+ up.disabled = disabled || (value !== null && value >= max);
180
+ }
181
+ emitChange(input, sourceEvent, value, displayValue, emitValueChange = true) {
182
+ const detail = { value, displayValue, sourceEvent };
183
+ const committedValue = value === null ? "" : String(value);
184
+ this.currentValue = committedValue;
185
+ input.value = this.getAttribute("display-value") ?? committedValue;
186
+ this.draft = undefined;
187
+ this.previousDraft = undefined;
188
+ this.syncAria(input);
189
+ this.syncFormState(input);
190
+ this.updateButtons(input);
191
+ this.dispatchEvent(new CustomEvent("spui-commit", { detail, bubbles: true, composed: true }));
192
+ if (emitValueChange)
193
+ this.dispatchEvent(new CustomEvent("spui-change", { detail: { value: committedValue, sourceEvent, committed: true }, bubbles: true, composed: true }));
194
+ }
195
+ commit(input, sourceEvent) {
196
+ const text = this.draft;
197
+ const parsed = text === undefined ? this.numberValue() : (text.trim() === "" ? null : Number(text));
198
+ if (parsed !== null && !Number.isFinite(parsed)) {
199
+ input.value = this.getAttribute("display-value") ?? this.currentValue ?? "";
200
+ this.draft = undefined;
201
+ this.previousDraft = undefined;
202
+ return;
203
+ }
204
+ const value = parsed === null ? null : this.bounds(parsed);
205
+ const oldValue = this.numberValue();
206
+ if (oldValue !== value || text !== undefined && text !== this.previousDraft)
207
+ this.emitChange(input, sourceEvent, value, text, false);
208
+ else {
209
+ input.value = this.getAttribute("display-value") ?? (value === null ? "" : String(value));
210
+ this.draft = undefined;
211
+ this.previousDraft = undefined;
212
+ }
213
+ }
214
+ step(input, event, direction, page = false) {
215
+ if (input.disabled || input.readOnly)
216
+ return;
217
+ const baseValue = this.draft !== undefined && this.draft.trim() !== "" ? Number(this.draft) : this.numberValue();
218
+ const step = Number(this.getAttribute(page ? "step-page" : "step") ?? "1");
219
+ const start = baseValue ?? (this.hasAttribute("min") ? Number(this.getAttribute("min")) : 0);
220
+ const next = this.bounds(start + direction * (Number.isFinite(step) && step > 0 ? step : 1));
221
+ if (this.numberValue() !== next || this.draft !== undefined)
222
+ this.emitChange(input, event, next);
223
+ }
224
+ getInput() {
225
+ const slot = this.shadowRoot?.querySelector('slot[name="input"]');
226
+ return slot?.assignedElements().find((element) => element instanceof HTMLInputElement)
227
+ ?? this.shadowRoot?.querySelector(".input-fallback") ?? undefined;
228
+ }
229
+ getStepper(direction) {
230
+ const name = direction === "increment" ? "incrementButton" : "decrementButton";
231
+ const slot = this.shadowRoot?.querySelector(`slot[name="${name}"]`);
232
+ return slot?.assignedElements().find((element) => element instanceof HTMLButtonElement)
233
+ ?? this.shadowRoot?.querySelector(`[part~="${direction}-button"]`) ?? undefined;
234
+ }
235
+ syncSlots() {
236
+ const input = this.getInput();
237
+ if (input) {
238
+ const fallback = this.shadowRoot?.querySelector(".input-fallback");
239
+ if (input !== fallback)
240
+ this.slotInputDefaults = rememberNativeFormControlDefaults(this.slotInputDefaults, input);
241
+ const defaults = input === fallback ? undefined : this.slotInputDefaults;
242
+ if (fallback) {
243
+ const assigned = input !== fallback;
244
+ fallback.hidden = assigned;
245
+ fallback.setAttribute("aria-hidden", String(assigned));
246
+ fallback.removeAttribute("name");
247
+ }
248
+ if (this.boundInput !== input) {
249
+ this.boundInput?.removeEventListener("input", this.handleInput);
250
+ this.boundInput?.removeEventListener("change", this.handleChange);
251
+ this.boundInput?.removeEventListener("blur", this.handleBlur);
252
+ this.boundInput?.removeEventListener("keydown", this.handleKeyDown);
253
+ input.addEventListener("input", this.handleInput);
254
+ input.addEventListener("change", this.handleChange);
255
+ input.addEventListener("blur", this.handleBlur);
256
+ input.addEventListener("keydown", this.handleKeyDown);
257
+ this.boundInput = input;
258
+ }
259
+ if (!input.id)
260
+ input.id = this.shadowRoot?.querySelector("label")?.htmlFor ?? `spin-${crypto.randomUUID()}`;
261
+ if (!input.hasAttribute("role"))
262
+ input.setAttribute("role", "spinbutton");
263
+ if (!input.hasAttribute("aria-label") && !input.hasAttribute("aria-labelledby")) {
264
+ const label = this.getAttribute("label");
265
+ if (label)
266
+ input.setAttribute("aria-label", label);
267
+ }
268
+ for (const name of ["min", "max", "aria-label", "aria-description", "aria-describedby", "aria-invalid"]) {
269
+ if (input.hasAttribute(name))
270
+ continue;
271
+ const value = this.getAttribute(name);
272
+ if (value !== null)
273
+ input.setAttribute(name, value);
274
+ }
275
+ input.disabled = this.formDisabled || this.hasAttribute("disabled") || Boolean(defaults?.disabled);
276
+ input.required = this.hasAttribute("required") || Boolean(defaults?.required);
277
+ input.readOnly = this.hasAttribute("read-only") || Boolean(defaults?.readOnly);
278
+ input.removeAttribute("name");
279
+ if (this.draft === undefined)
280
+ input.value = this.getAttribute("display-value") ?? this.currentValue ?? "";
281
+ this.syncAria(input);
282
+ this.syncFormState(input);
283
+ this.updateButtons(input);
284
+ }
285
+ const decrement = this.getStepper("decrement");
286
+ if (decrement && !decrement.hasAttribute("type"))
287
+ decrement.type = "button";
288
+ const decrementFallback = this.shadowRoot?.querySelector('[part~="decrement-button"]');
289
+ if (decrementFallback) {
290
+ const assigned = decrement !== decrementFallback;
291
+ decrementFallback.hidden = assigned;
292
+ decrementFallback.setAttribute("aria-hidden", String(assigned));
293
+ }
294
+ if (this.boundDecrement !== decrement) {
295
+ this.boundDecrement?.removeEventListener("click", this.handleDecrement);
296
+ this.boundDecrement?.removeEventListener("mousedown", this.handleDecrement);
297
+ this.boundDecrement?.removeEventListener("mouseup", this.handleHeldStepEnd);
298
+ this.boundDecrement?.removeEventListener("mouseleave", this.handleHeldStepEnd);
299
+ decrement?.addEventListener("click", this.handleDecrement);
300
+ decrement?.addEventListener("mousedown", this.handleDecrement);
301
+ decrement?.addEventListener("mouseup", this.handleHeldStepEnd);
302
+ decrement?.addEventListener("mouseleave", this.handleHeldStepEnd);
303
+ this.boundDecrement = decrement;
304
+ }
305
+ const increment = this.getStepper("increment");
306
+ if (increment && !increment.hasAttribute("type"))
307
+ increment.type = "button";
308
+ const incrementFallback = this.shadowRoot?.querySelector('[part~="increment-button"]');
309
+ if (incrementFallback) {
310
+ const assigned = increment !== incrementFallback;
311
+ incrementFallback.hidden = assigned;
312
+ incrementFallback.setAttribute("aria-hidden", String(assigned));
313
+ }
314
+ if (this.boundIncrement !== increment) {
315
+ this.boundIncrement?.removeEventListener("click", this.handleIncrement);
316
+ this.boundIncrement?.removeEventListener("mousedown", this.handleIncrement);
317
+ this.boundIncrement?.removeEventListener("mouseup", this.handleHeldStepEnd);
318
+ this.boundIncrement?.removeEventListener("mouseleave", this.handleHeldStepEnd);
319
+ increment?.addEventListener("click", this.handleIncrement);
320
+ increment?.addEventListener("mousedown", this.handleIncrement);
321
+ increment?.addEventListener("mouseup", this.handleHeldStepEnd);
322
+ increment?.addEventListener("mouseleave", this.handleHeldStepEnd);
323
+ this.boundIncrement = increment;
324
+ }
325
+ }
326
+ handleInput = (event) => {
327
+ const input = event.currentTarget;
328
+ if (this.draft === undefined)
329
+ this.previousDraft = this.getAttribute("display-value") ?? this.currentValue ?? "";
330
+ this.draft = input.value;
331
+ const parsed = this.draft.trim() === "" ? null : Number(this.draft);
332
+ if (parsed === null)
333
+ input.removeAttribute("aria-valuenow");
334
+ else if (Number.isFinite(parsed))
335
+ input.setAttribute("aria-valuenow", String(parsed));
336
+ this.internals?.setFormValue(parsed === null || !Number.isFinite(parsed) ? null : String(parsed));
337
+ this.dispatchEvent(new CustomEvent("spui-change", { detail: { value: parsed === null ? "" : String(parsed), sourceEvent: event, committed: false }, bubbles: true, composed: true }));
338
+ };
339
+ handleChange = (event) => this.commit(event.currentTarget, event);
340
+ handleBlur = (event) => this.commit(event.currentTarget, event);
341
+ handleKeyDown = (rawEvent) => {
342
+ const event = rawEvent;
343
+ const input = event.currentTarget;
344
+ if (input.readOnly)
345
+ return;
346
+ if (event.key === "ArrowUp" || event.key === "ArrowDown")
347
+ this.step(input, event, event.key === "ArrowUp" ? 1 : -1);
348
+ else if (event.key === "PageUp" || event.key === "PageDown") {
349
+ event.preventDefault();
350
+ this.step(input, event, event.key === "PageUp" ? 1 : -1, true);
351
+ }
352
+ else if (event.key === "Home" && !event.shiftKey && this.hasAttribute("min")) {
353
+ const min = Number(this.getAttribute("min"));
354
+ if (this.numberValue() !== min)
355
+ this.emitChange(input, event, min);
356
+ }
357
+ else if (event.key === "End" && !event.shiftKey && this.hasAttribute("max")) {
358
+ const max = Number(this.getAttribute("max"));
359
+ if (this.numberValue() !== max)
360
+ this.emitChange(input, event, max);
361
+ }
362
+ else if (event.key === "Enter") {
363
+ this.commit(input, event);
364
+ this.submitFormFromEnter(input);
365
+ }
366
+ else if (event.key === "Escape" && this.draft !== undefined) {
367
+ input.value = this.getAttribute("display-value") ?? this.currentValue ?? "";
368
+ this.draft = undefined;
369
+ this.previousDraft = undefined;
370
+ this.syncAria(input);
371
+ }
372
+ };
373
+ submitFormFromEnter(input) {
374
+ const form = this.form;
375
+ if (!form || input.form === form)
376
+ return;
377
+ const submitter = Array.from(form.elements).find((control) => control instanceof HTMLButtonElement && control.type === "submit"
378
+ || control instanceof HTMLInputElement && (control.type === "submit" || control.type === "image"));
379
+ if (submitter) {
380
+ if (!submitter.disabled)
381
+ form.requestSubmit(submitter);
382
+ }
383
+ else
384
+ form.requestSubmit();
385
+ }
386
+ handleDecrement = (event) => this.handleStepButton(event, -1);
387
+ handleIncrement = (event) => this.handleStepButton(event, 1);
388
+ handleStepButton(event, direction) {
389
+ if (event.type === "click" && event.detail !== 0)
390
+ return;
391
+ const input = this.getInput();
392
+ if (!input || input.disabled || input.readOnly)
393
+ return;
394
+ if (event.type === "mousedown" && event.button !== 0)
395
+ return;
396
+ this.clearHeldStep();
397
+ this.commit(input, event);
398
+ this.step(input, event, direction);
399
+ input.focus();
400
+ if (event.type !== "mousedown")
401
+ return;
402
+ this.heldStepDirection = direction;
403
+ this.heldStepInput = input;
404
+ this.heldStepEvent = event;
405
+ this.heldStepElapsed = 0;
406
+ this.scheduleHeldStep(300);
407
+ }
408
+ handleHeldStepEnd = () => this.clearHeldStep();
409
+ scheduleHeldStep(delay) {
410
+ this.heldStepTimer = setTimeout(() => {
411
+ const input = this.heldStepInput;
412
+ const stepper = this.getStepper(this.heldStepDirection > 0 ? "increment" : "decrement");
413
+ if (!this.heldStepDirection || !input || !stepper || stepper.disabled || input.disabled || input.readOnly) {
414
+ this.clearHeldStep();
415
+ return;
416
+ }
417
+ this.step(input, this.heldStepEvent ?? new Event("mousedown"), this.heldStepDirection);
418
+ this.heldStepElapsed += delay;
419
+ const progress = Math.min(1, this.heldStepElapsed / 1000);
420
+ this.scheduleHeldStep(300 + (80 - 300) * progress);
421
+ }, delay);
422
+ }
423
+ clearHeldStep() {
424
+ if (this.heldStepTimer !== undefined)
425
+ clearTimeout(this.heldStepTimer);
426
+ this.heldStepTimer = undefined;
427
+ this.heldStepDirection = 0;
428
+ this.heldStepInput = undefined;
429
+ this.heldStepEvent = undefined;
430
+ this.heldStepElapsed = 0;
431
+ }
432
+ render() {
433
+ const shadow = this.shadowRoot ?? this.attachShadow({ mode: "open" });
434
+ const id = `spin-${crypto.randomUUID()}`;
435
+ const label = document.createElement("label");
436
+ label.htmlFor = id;
437
+ label.textContent = this.getAttribute("label") ?? "";
438
+ label.part.add("label");
439
+ const control = document.createElement("div");
440
+ control.part.add("control-group");
441
+ const input = document.createElement("input");
442
+ input.className = "input-fallback";
443
+ input.id = id;
444
+ input.type = "text";
445
+ input.inputMode = "decimal";
446
+ input.autocomplete = "off";
447
+ input.setAttribute("role", "spinbutton");
448
+ const initial = this.currentValue ?? this.getAttribute("value") ?? this.getAttribute("default-value") ?? this.querySelector('input[slot="input"]')?.value ?? "0";
449
+ this.currentValue = initial;
450
+ input.value = this.getAttribute("display-value") ?? initial;
451
+ for (const name of ["min", "max", "aria-label", "aria-description", "aria-describedby", "aria-invalid"]) {
452
+ const value = this.getAttribute(name);
453
+ if (value !== null)
454
+ input.setAttribute(name, value);
455
+ }
456
+ input.disabled = this.hasAttribute("disabled");
457
+ input.required = this.hasAttribute("required");
458
+ input.readOnly = this.hasAttribute("read-only");
459
+ input.part.add("input");
460
+ const buttons = document.createElement("span");
461
+ buttons.part.add("buttons");
462
+ const makeButton = (direction, fallback, symbol) => {
463
+ const button = document.createElement("button");
464
+ button.type = "button";
465
+ button.tabIndex = -1;
466
+ button.setAttribute("aria-label", this.getAttribute(`${direction}-label`) ?? fallback);
467
+ button.part.add(`${direction}-button`);
468
+ button.innerHTML = `<svg aria-hidden="true" viewBox="0 0 16 16"><path d="${symbol}"/></svg>`;
469
+ return button;
470
+ };
471
+ const decrement = makeButton("decrement", "Decrement value", "m3.25 5.5 4.75 4.75 4.75-4.75");
472
+ const increment = makeButton("increment", "Increment value", "m3.25 10.5 4.75-4.75 4.75 4.75");
473
+ const inputSlot = document.createElement("slot");
474
+ inputSlot.name = "input";
475
+ inputSlot.part.add("input");
476
+ inputSlot.append(input);
477
+ inputSlot.addEventListener("slotchange", () => this.syncSlots());
478
+ const incrementSlot = document.createElement("slot");
479
+ incrementSlot.name = "incrementButton";
480
+ incrementSlot.part.add("increment-button-slot");
481
+ incrementSlot.append(increment);
482
+ incrementSlot.addEventListener("slotchange", () => this.syncSlots());
483
+ const decrementSlot = document.createElement("slot");
484
+ decrementSlot.name = "decrementButton";
485
+ decrementSlot.part.add("decrement-button-slot");
486
+ decrementSlot.append(decrement);
487
+ decrementSlot.addEventListener("slotchange", () => this.syncSlots());
488
+ buttons.append(incrementSlot, decrementSlot);
489
+ control.append(inputSlot, buttons);
490
+ const style = document.createElement("style");
491
+ style.textContent = `${base}:host{display:grid;gap:var(--spui-control-gap,6px);width:100%}label{font-size:14px;font-weight:600;color:var(--spui-ui-muted,var(--spui-semantic-body-text,inherit))}[part~=control-group]{box-sizing:border-box;display:grid;grid-template-columns:minmax(0,1fr) 24px;grid-template-rows:1fr 1fr;min-height:var(--spui-control-height,32px);column-gap:4px;position:relative;isolation:isolate;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))));color:var(--spui-ui-text,var(--spui-semantic-body-text,inherit))}[part~=control-group]:focus-within{outline:2px solid var(--spui-focus-ring,var(--colorStrokeFocus2,#005a9e));outline-offset:2px}:host([size=small]) [part~=control-group]{min-height:24px}:host([appearance=underline]) [part~=control-group]{border-width:0 0 1px;border-radius:0}:host([appearance=filled-darker]) [part~=control-group]{background:var(--colorNeutralBackground3,var(--spui-ui-surface-sunken,#f0f0f0));border-color:transparent}:host([appearance=filled-lighter]) [part~=control-group]{background:var(--colorNeutralBackground2,var(--spui-ui-surface-hover,#f5f5f5));border-color:transparent}input{grid-column:1;grid-row:1/3;min-width:0;width:100%;padding-inline:var(--spui-control-padding-inline,10px);border:0;border-radius:inherit;background:transparent;color:inherit;font:inherit;outline:none}input::-webkit-inner-spin-button,input::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}span[part=buttons]{grid-column:2;grid-row:1/3;display:grid;grid-template-rows:1fr 1fr;min-height:0}button{display:grid;place-items:center;min-width:24px;padding:0;border:0;background:transparent;color:inherit;cursor:pointer}button:hover:not(:disabled){background:var(--spui-ui-surface-hover,#f5f5f5)}button:disabled{opacity:.42;cursor:not-allowed}button svg{width:12px;height:12px;fill:none;stroke:currentColor;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}input:disabled{opacity:.55}input:read-only{cursor:default}@media(forced-colors:active){[part~=control-group]{border-color:FieldText}button:focus-visible,input:focus-visible{outline:1px solid Highlight}}`;
492
+ style.textContent += "[part~=control-group]{grid-template-rows:24px 24px;min-height:50px}:host([size=small]) [part~=control-group]{min-height:50px}span[part=buttons]{grid-template-rows:24px 24px;min-height:48px}button{min-height:24px}";
493
+ style.textContent += "slot[name=input]{display:contents}slot[name=input]::slotted(input){grid-column:1;grid-row:1/3;min-width:0;width:100%;padding-inline:var(--spui-control-padding-inline,10px);border:0;border-radius:inherit;background:transparent;color:inherit;font:inherit;outline:none}slot[name=incrementButton],slot[name=decrementButton]{display:contents}slot[name=incrementButton]::slotted(button),slot[name=decrementButton]::slotted(button){display:grid;place-items:center;min-width:24px;min-height:24px;padding:0;border:0;background:transparent;color:inherit;cursor:pointer}slot[name=incrementButton]::slotted(button:disabled),slot[name=decrementButton]::slotted(button:disabled){opacity:.42;cursor:not-allowed}";
494
+ shadow.replaceChildren(style, label, control);
495
+ this.syncSlots();
496
+ }
497
+ }
498
+ customElements.define("spui-spin-button", SpuiSpinButton);
499
+ }
@@ -0,0 +1,4 @@
1
+ export type SpinnerSize = "extra-tiny" | "tiny" | "extra-small" | "small" | "medium" | "large" | "extra-large" | "huge";
2
+ export type SpinnerAppearance = "primary" | "inverted";
3
+ export type SpinnerLabelPosition = "above" | "below" | "before" | "after";
4
+ export declare function defineSpinner(): void;
@@ -0,0 +1,124 @@
1
+ import { base } from "./shared.js";
2
+ const sizes = ["extra-tiny", "tiny", "extra-small", "small", "medium", "large", "extra-large", "huge"];
3
+ const positions = ["above", "below", "before", "after"];
4
+ export function defineSpinner() {
5
+ if (customElements.get("spui-spinner"))
6
+ return;
7
+ class SpuiSpinner extends HTMLElement {
8
+ static observedAttributes = ["label", "size", "appearance", "label-position", "delay"];
9
+ delayTimer;
10
+ connectedCallback() {
11
+ if (!this.shadowRoot)
12
+ this.render();
13
+ this.sync();
14
+ this.syncDelay();
15
+ }
16
+ disconnectedCallback() { clearTimeout(this.delayTimer); this.delayTimer = undefined; }
17
+ attributeChangedCallback(name) {
18
+ if (!this.isConnected)
19
+ return;
20
+ if (name === "delay")
21
+ this.syncDelay();
22
+ this.sync();
23
+ }
24
+ sync() {
25
+ const ring = this.shadowRoot?.querySelector(".ring");
26
+ const label = this.shadowRoot?.querySelector("slot[name=label]");
27
+ if (!ring || !label)
28
+ return;
29
+ const size = this.getAttribute("size");
30
+ this.dataset.size = sizes.includes(size) ? size : "medium";
31
+ const appearance = this.getAttribute("appearance");
32
+ this.dataset.appearance = appearance === "inverted" ? "inverted" : "primary";
33
+ const position = this.getAttribute("label-position");
34
+ this.dataset.labelPosition = positions.includes(position) ? position : "after";
35
+ const text = this.getAttribute("label") ?? "Loading";
36
+ label.textContent = text;
37
+ if (!this.hasAttribute("aria-label"))
38
+ this.setAttribute("aria-label", text);
39
+ this.setAttribute("role", "progressbar");
40
+ this.setAttribute("aria-busy", "true");
41
+ ring.setAttribute("aria-hidden", "true");
42
+ }
43
+ syncDelay() {
44
+ clearTimeout(this.delayTimer);
45
+ this.delayTimer = undefined;
46
+ const delay = Number(this.getAttribute("delay") ?? 0);
47
+ if (!Number.isFinite(delay) || delay <= 0) {
48
+ this.removeAttribute("data-delay-pending");
49
+ return;
50
+ }
51
+ this.setAttribute("data-delay-pending", "");
52
+ this.delayTimer = setTimeout(() => { this.delayTimer = undefined; this.removeAttribute("data-delay-pending"); }, Math.min(delay, 60_000));
53
+ }
54
+ render() {
55
+ const shadow = this.attachShadow({ mode: "open" });
56
+ const ring = document.createElement("span");
57
+ ring.className = "ring";
58
+ ring.part.add("indicator");
59
+ ring.setAttribute("aria-hidden", "true");
60
+ const spinnerSlot = document.createElement("slot");
61
+ spinnerSlot.name = "spinner";
62
+ spinnerSlot.append(ring);
63
+ const spinnerTail = document.createElement("slot");
64
+ spinnerTail.name = "spinner-tail";
65
+ spinnerTail.setAttribute("aria-hidden", "true");
66
+ const defaultTail = document.createElement("span");
67
+ defaultTail.className = "tail";
68
+ defaultTail.setAttribute("aria-hidden", "true");
69
+ spinnerTail.append(defaultTail);
70
+ const spinner = document.createElement("span");
71
+ spinner.className = "spinner";
72
+ spinner.part.add("spinner");
73
+ spinner.append(spinnerSlot, spinnerTail);
74
+ const label = document.createElement("span");
75
+ label.className = "label";
76
+ label.part.add("label");
77
+ label.setAttribute("aria-hidden", "true");
78
+ const slot = document.createElement("slot");
79
+ slot.name = "label";
80
+ label.append(slot);
81
+ const style = document.createElement("style");
82
+ style.textContent = `${base}
83
+ :host{box-sizing:border-box;position:relative;display:inline-flex;align-items:center;justify-content:center;gap:8px;line-height:1;min-width:min-content;color:var(--spui-ui-text,var(--spui-semantic-body-text,#242424))}
84
+ :host([data-delay-pending]){display:none}
85
+ :host([data-label-position=above]),:host([data-label-position=below]){flex-direction:column}
86
+ .spinner{position:relative;display:block;flex:none;width:var(--spui-spinner-size,32px);height:var(--spui-spinner-size,32px);background:var(--spui-spinner-track,var(--colorBrandStroke2Contrast,var(--spui-ui-stroke,#edebe9)));color:var(--spui-spinner-active,var(--colorBrandStroke1,var(--spui-ui-accent,#005a9e)));mask-image:radial-gradient(closest-side,transparent calc(100% - var(--spui-spinner-stroke,2px) - 1px),white calc(100% - var(--spui-spinner-stroke,2px)),transparent 100%);-webkit-mask-image:radial-gradient(closest-side,transparent calc(100% - var(--spui-spinner-stroke,2px) - 1px),white calc(100% - var(--spui-spinner-stroke,2px)),transparent 100%);animation:spui-spinner-rotate 1.5s linear infinite}
87
+ .ring,.tail{position:absolute;inset:0;display:block;width:100%;height:100%}
88
+ .ring{background:transparent;color:inherit}
89
+ .tail{mask-image:conic-gradient(transparent 105deg,white 105deg);-webkit-mask-image:conic-gradient(transparent 105deg,white 105deg);animation:spui-spinner-tail 1.5s var(--spui-motion-easing-standard,cubic-bezier(.33,0,.67,1)) infinite}
90
+ .tail::before,.tail::after{content:"";position:absolute;inset:0;display:block;width:100%;height:100%;background-image:conic-gradient(currentColor 135deg,transparent 135deg);animation:inherit}
91
+ .tail::before{animation-name:spui-spinner-tail-start}
92
+ .tail::after{animation-name:spui-spinner-tail-end}
93
+ slot[name=spinner],slot[name=spinner-tail]{position:absolute;inset:0;display:block;width:100%;height:100%}
94
+ slot[name=spinner]::slotted(*),slot[name=spinner-tail]::slotted(*){display:block;width:100%;height:100%}
95
+ :host([data-size=extra-tiny]){--spui-spinner-size:16px;--spui-spinner-stroke:2px}
96
+ :host([data-size=tiny]){--spui-spinner-size:20px;--spui-spinner-stroke:2px}
97
+ :host([data-size=extra-small]){--spui-spinner-size:24px;--spui-spinner-stroke:2px}
98
+ :host([data-size=small]){--spui-spinner-size:28px;--spui-spinner-stroke:2px}
99
+ :host([data-size=medium]){--spui-spinner-size:32px;--spui-spinner-stroke:2.5px}
100
+ :host([data-size=large]){--spui-spinner-size:36px;--spui-spinner-stroke:2.5px}
101
+ :host([data-size=extra-large]){--spui-spinner-size:40px;--spui-spinner-stroke:2.5px}
102
+ :host([data-size=huge]){--spui-spinner-size:44px;--spui-spinner-stroke:3px}
103
+ :host([data-label-position=before]) .label,:host([data-label-position=above]) .label{order:0}
104
+ :host([data-label-position=before]) .spinner,:host([data-label-position=above]) .spinner{order:1}
105
+ :host([data-label-position=after]) .spinner,:host([data-label-position=below]) .spinner{order:0}
106
+ :host([data-label-position=after]) .label,:host([data-label-position=below]) .label{order:1}
107
+ .label{display:block;max-width:100%;font:var(--spui-spinner-label-font,400 14px/1.4 var(--spui-font-family,inherit));color:var(--spui-spinner-label,var(--spui-ui-text,var(--spui-semantic-body-text,inherit)))}
108
+ :host([data-size=extra-tiny]) .label,:host([data-size=tiny]) .label,:host([data-size=extra-small]) .label,:host([data-size=small]) .label{font-size:12px}
109
+ :host([data-size=huge]) .label{font-size:16px}
110
+ :host([appearance=inverted]),:host([data-appearance=inverted]){--spui-spinner-track:var(--spui-spinner-track-inverted,color-mix(in srgb,var(--spui-ui-accent-text,#fff) 38%,transparent));--spui-spinner-active:var(--spui-spinner-active-inverted,var(--spui-ui-accent-text,#fff));--spui-spinner-label:var(--spui-spinner-label-inverted,var(--spui-ui-accent-text,#fff))}
111
+ :host([dir=rtl]) .tail{mask-image:conic-gradient(white 255deg,transparent 255deg);-webkit-mask-image:conic-gradient(white 255deg,transparent 255deg)}
112
+ :host([dir=rtl]) .tail::before,:host([dir=rtl]) .tail::after{background-image:conic-gradient(transparent 225deg,currentColor 225deg)}
113
+ @keyframes spui-spinner-rotate{to{transform:rotate(360deg)}}
114
+ @keyframes spui-spinner-tail{0%{transform:rotate(-135deg)}50%{transform:rotate(0deg)}100%{transform:rotate(225deg)}}
115
+ @keyframes spui-spinner-tail-start{0%,100%{transform:rotate(0deg)}50%{transform:rotate(105deg)}}
116
+ @keyframes spui-spinner-tail-end{0%,100%{transform:rotate(0deg)}50%{transform:rotate(225deg)}}
117
+ @media(prefers-reduced-motion:reduce){.spinner,.tail,.tail::before,.tail::after{animation:none}.tail{background-image:conic-gradient(transparent 120deg,currentColor 360deg)}.tail::before,.tail::after{display:none}}
118
+ @media(forced-colors:active){.spinner{forced-color-adjust:none;color:Highlight}.ring{background:HighlightText;color:Highlight}.label{color:CanvasText}}
119
+ `;
120
+ shadow.append(style, spinner, label);
121
+ }
122
+ }
123
+ customElements.define("spui-spinner", SpuiSpinner);
124
+ }
@@ -0,0 +1,5 @@
1
+ export declare function defineColorSwatch(): void;
2
+ export declare function defineImageSwatch(): void;
3
+ export declare function defineEmptySwatch(): void;
4
+ export declare function defineSwatchPickerRow(): void;
5
+ export declare function defineSwatchPicker(): void;