@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,255 @@
1
+ import { base, rememberNativeFormControlDefaults, stopHostAccessibleNameSync, syncHostAccessibleName } from "./shared.js";
2
+ /** Bare, form-associated input; pair it with Label or Field for an accessible name. */
3
+ export function defineInput() {
4
+ if (customElements.get("spui-input"))
5
+ return;
6
+ class SpuiInput extends HTMLElement {
7
+ static formAssociated = true;
8
+ static observedAttributes = ["value", "type", "placeholder", "disabled", "required", "read-only", "name", "size", "appearance", "autocomplete", "inputmode", "min", "max", "step", "minlength", "maxlength", "pattern", "multiple", "accept", "alt", "capture", "checked", "default-checked", "form", "formaction", "formenctype", "formmethod", "formnovalidate", "formtarget", "height", "list", "src", "width", "aria-label", "aria-labelledby", "aria-description", "aria-describedby", "aria-invalid"];
9
+ internals;
10
+ currentValue = "";
11
+ dirty = false;
12
+ formDisabled = false;
13
+ customError = "";
14
+ boundInput;
15
+ slotInputDefaults;
16
+ handleInput = (event) => {
17
+ const input = this.input();
18
+ if (!input)
19
+ return;
20
+ this.currentValue = input.value;
21
+ this.dirty = true;
22
+ this.syncForm(input);
23
+ this.dispatchEvent(new CustomEvent("spui-input", { detail: { value: input.value, checked: input.checked, sourceEvent: event }, bubbles: true, composed: true }));
24
+ };
25
+ handleChange = () => {
26
+ const input = this.input();
27
+ if (input)
28
+ this.dispatchEvent(new CustomEvent("spui-change", { detail: { value: input.value }, bubbles: true, composed: true }));
29
+ };
30
+ constructor() {
31
+ super();
32
+ try {
33
+ this.internals = this.attachInternals();
34
+ }
35
+ catch { /* Native input remains usable where form-associated custom elements are unavailable. */ }
36
+ }
37
+ get value() { return this.input()?.value ?? this.currentValue; }
38
+ set value(value) { this.currentValue = String(value ?? ""); this.dirty = true; const input = this.input(); if (input && (input.type !== "file" || !this.currentValue))
39
+ input.value = this.currentValue; this.syncForm(input); }
40
+ get defaultValue() { return this.getAttribute("value") ?? this.input()?.defaultValue ?? ""; }
41
+ set defaultValue(value) { this.setAttribute("value", String(value ?? "")); }
42
+ get name() { return this.getAttribute("name") ?? ""; }
43
+ set name(value) { this.setAttribute("name", value); }
44
+ get type() { return this.input()?.type ?? this.getAttribute("type") ?? "text"; }
45
+ set type(value) { this.setAttribute("type", value); }
46
+ get checked() { return Boolean(this.input()?.checked); }
47
+ set checked(value) { const input = this.input(); if (input)
48
+ input.checked = Boolean(value);
49
+ else
50
+ this.toggleAttribute("checked", Boolean(value)); this.syncForm(input); }
51
+ get disabled() { return this.hasAttribute("disabled"); }
52
+ set disabled(value) { this.toggleAttribute("disabled", Boolean(value)); }
53
+ get required() { return this.hasAttribute("required"); }
54
+ set required(value) { this.toggleAttribute("required", Boolean(value)); }
55
+ get readOnly() { return this.hasAttribute("read-only"); }
56
+ set readOnly(value) { this.toggleAttribute("read-only", Boolean(value)); }
57
+ get form() { return this.internals?.form ?? null; }
58
+ get validity() { return this.internals?.validity ?? this.input()?.validity; }
59
+ get validationMessage() { return this.internals?.validationMessage ?? this.input()?.validationMessage ?? ""; }
60
+ get willValidate() { return this.internals?.willValidate ?? Boolean(this.input() && !this.input().disabled && !this.input().readOnly); }
61
+ connectedCallback() {
62
+ if (!this.shadowRoot)
63
+ this.render();
64
+ const input = this.input();
65
+ if (!input)
66
+ return;
67
+ if (!this.dirty) {
68
+ if (this.hasAttribute("value")) {
69
+ this.currentValue = this.defaultValue;
70
+ if (input.type !== "file")
71
+ input.value = this.currentValue;
72
+ }
73
+ else
74
+ this.currentValue = input.type === "file" ? "" : input.value;
75
+ }
76
+ this.syncForm(input);
77
+ }
78
+ disconnectedCallback() { stopHostAccessibleNameSync(this); }
79
+ attributeChangedCallback(name, _oldValue, value) {
80
+ if (!this.isConnected || !this.shadowRoot)
81
+ return;
82
+ const input = this.input();
83
+ if (!input)
84
+ return;
85
+ if (name === "value") {
86
+ if (!this.dirty && input.type !== "file") {
87
+ this.currentValue = value ?? "";
88
+ input.value = this.currentValue;
89
+ this.syncForm(input);
90
+ }
91
+ return;
92
+ }
93
+ if (name === "size" || name === "appearance")
94
+ this.shadowRoot.querySelector(".control").dataset[name] = value || (name === "size" ? "medium" : "outline");
95
+ else if (name === "type") {
96
+ try {
97
+ input.type = value || "text";
98
+ }
99
+ catch {
100
+ input.type = "text";
101
+ }
102
+ }
103
+ else if (name === "checked")
104
+ input.checked = value !== null || (!this.dirty && Boolean(this.slotInputDefaults?.checked));
105
+ else if (name === "default-checked") {
106
+ input.defaultChecked = value !== null || Boolean(this.slotInputDefaults?.defaultChecked);
107
+ if (!this.dirty)
108
+ input.checked = value !== null || Boolean(this.slotInputDefaults?.checked);
109
+ }
110
+ else if (["placeholder", "autocomplete", "inputmode", "min", "max", "step", "minlength", "maxlength", "pattern", "multiple"].includes(name)) {
111
+ const target = name === "read-only" ? "readonly" : name;
112
+ if (value === null)
113
+ input.removeAttribute(target);
114
+ else
115
+ input.setAttribute(target, value);
116
+ }
117
+ else if (["accept", "alt", "capture", "form", "formaction", "formenctype", "formmethod", "formnovalidate", "formtarget", "height", "list", "src", "width"].includes(name)) {
118
+ if (value === null)
119
+ input.removeAttribute(name);
120
+ else
121
+ input.setAttribute(name, value);
122
+ }
123
+ else if (["aria-label", "aria-labelledby", "aria-description", "aria-describedby", "aria-invalid"].includes(name)) {
124
+ if (value === null)
125
+ input.removeAttribute(name);
126
+ else
127
+ input.setAttribute(name, value);
128
+ }
129
+ this.syncInputSlot();
130
+ }
131
+ formDisabledCallback(disabled) { this.formDisabled = disabled; this.syncInputSlot(); }
132
+ formResetCallback() { this.dirty = false; const input = this.input(); this.currentValue = input?.type === "file" ? "" : this.defaultValue; if (input) {
133
+ input.value = this.currentValue;
134
+ input.checked = input.defaultChecked;
135
+ this.syncForm(input);
136
+ } }
137
+ formStateRestoreCallback(state) { if (typeof state === "string")
138
+ this.value = state; }
139
+ focus(options) { this.input()?.focus(options); }
140
+ select() { this.input()?.select(); }
141
+ checkValidity() { return this.internals?.checkValidity() ?? this.input()?.checkValidity() ?? true; }
142
+ reportValidity() { return this.internals?.reportValidity() ?? this.input()?.reportValidity() ?? true; }
143
+ setCustomValidity(message) { this.customError = message; this.syncForm(this.input()); }
144
+ input() {
145
+ const slotted = Array.from(this.children).find((element) => element instanceof HTMLInputElement && element.slot === "input");
146
+ return slotted ?? this.shadowRoot?.querySelector(".input-fallback") ?? undefined;
147
+ }
148
+ syncInputSlot() {
149
+ const input = this.input();
150
+ if (!input)
151
+ return;
152
+ const fallback = this.shadowRoot?.querySelector(".input-fallback");
153
+ if (fallback)
154
+ fallback.hidden = input !== fallback;
155
+ if (input !== fallback)
156
+ this.slotInputDefaults = rememberNativeFormControlDefaults(this.slotInputDefaults, input);
157
+ if (this.boundInput !== input) {
158
+ this.boundInput?.removeEventListener("input", this.handleInput);
159
+ this.boundInput?.removeEventListener("change", this.handleChange);
160
+ input.addEventListener("input", this.handleInput);
161
+ input.addEventListener("change", this.handleChange);
162
+ this.boundInput = input;
163
+ }
164
+ if (this.hasAttribute("name"))
165
+ input.removeAttribute("name");
166
+ for (const name of ["type", "placeholder", "autocomplete", "inputmode", "min", "max", "step", "minlength", "maxlength", "pattern", "multiple", "accept", "alt", "capture", "form", "formaction", "formenctype", "formmethod", "formnovalidate", "formtarget", "height", "list", "src", "width", "aria-label", "aria-labelledby", "aria-description", "aria-describedby", "aria-invalid"]) {
167
+ if (input.hasAttribute(name))
168
+ continue;
169
+ const value = this.getAttribute(name);
170
+ if (value !== null)
171
+ input.setAttribute(name, value);
172
+ }
173
+ const defaults = input === fallback ? undefined : this.slotInputDefaults;
174
+ input.disabled = this.disabled || this.formDisabled || Boolean(defaults?.disabled);
175
+ input.required = this.required || Boolean(defaults?.required);
176
+ input.readOnly = this.readOnly || Boolean(defaults?.readOnly);
177
+ if (this.hasAttribute("default-checked"))
178
+ input.defaultChecked = true;
179
+ if (this.hasAttribute("checked"))
180
+ input.checked = true;
181
+ if (!this.dirty && input.type !== "file" && this.hasAttribute("value"))
182
+ input.value = this.currentValue || this.defaultValue;
183
+ syncHostAccessibleName(this, input);
184
+ this.syncForm(input);
185
+ }
186
+ syncForm(input) {
187
+ if (!input)
188
+ return;
189
+ let value = input.disabled || this.formDisabled || ((input.type === "checkbox" || input.type === "radio") && !input.checked) ? null : input.value;
190
+ if (input.type === "file") {
191
+ value = null;
192
+ const files = input.disabled || this.formDisabled ? null : input.files;
193
+ if (files?.length && this.name) {
194
+ const formData = new FormData();
195
+ for (const file of Array.from(files))
196
+ formData.append(this.name, file);
197
+ value = formData;
198
+ }
199
+ }
200
+ this.internals?.setFormValue(value, input.type === "file" ? null : input.value);
201
+ const v = input.validity;
202
+ const flags = { badInput: v.badInput, patternMismatch: v.patternMismatch, rangeOverflow: v.rangeOverflow, rangeUnderflow: v.rangeUnderflow, stepMismatch: v.stepMismatch, tooLong: v.tooLong, tooShort: v.tooShort, typeMismatch: v.typeMismatch, valueMissing: v.valueMissing, customError: Boolean(this.customError) };
203
+ const message = this.customError || input.validationMessage;
204
+ try {
205
+ this.internals?.setValidity(input.validity.valid && !this.customError ? {} : flags, message, input);
206
+ }
207
+ catch { /* Native validation remains available inside the shadow root. */ }
208
+ }
209
+ render() {
210
+ const shadow = this.attachShadow({ mode: "open" });
211
+ const style = document.createElement("style");
212
+ style.textContent = `${base}:host{display:inline-block;min-width:0;vertical-align:middle}.control{box-sizing:border-box;display:flex;align-items:center;gap:var(--spui-input-content-gap,8px);min-width:0;width:100%;border:1px solid var(--spui-input-border,var(--spui-control-border,var(--spui-ui-stroke,var(--spui-semantic-input-border,#8a8886))));border-radius:var(--spui-input-radius,var(--spui-control-radius,4px));background:var(--spui-input-background,var(--spui-control-background,var(--spui-ui-surface,var(--spui-semantic-input-background,#fff))));box-shadow:var(--spui-input-shadow,var(--spui-control-shadow,none));color:var(--spui-ui-text,var(--spui-semantic-body-text,inherit));font-family:var(--spui-font-family,inherit);font-size:var(--fontSizeBase200,14px);transition:border-color var(--spui-motion-duration-fast,120ms) var(--spui-motion-easing-standard,cubic-bezier(.2,0,0,1)),box-shadow var(--spui-motion-duration-fast,120ms) var(--spui-motion-easing-standard,cubic-bezier(.2,0,0,1))}.control:focus-within{border-color:var(--spui-focus-ring,var(--spui-ui-accent,#0078d4));box-shadow:0 0 0 1px var(--spui-focus-ring,var(--spui-ui-accent,#0078d4))}.control[data-size=small]{min-height:24px;font-size:var(--fontSizeBase100,12px)}.control[data-size=medium]{min-height:32px}.control[data-size=large]{min-height:40px;font-size:var(--fontSizeBase200,14px)}.control[data-appearance=underline]{border-width:0 0 1px;border-radius:0;box-shadow:none}.control[data-appearance=filled-darker],.control[data-appearance=filled-darker-shadow]{background:var(--spui-input-filled-darker,var(--spui-ui-surface-hover,var(--colorNeutralBackground3,#f0f0f0)));border-color:transparent}.control[data-appearance=filled-lighter],.control[data-appearance=filled-lighter-shadow]{background:var(--spui-input-filled-lighter,var(--spui-ui-canvas,var(--colorNeutralBackground1,#fafafa)));border-color:transparent}.control[data-appearance=filled-darker-shadow],.control[data-appearance=filled-lighter-shadow]{box-shadow:var(--spui-input-filled-shadow,var(--shadow2,0 1px 2px rgb(0 0 0 / 20%)))}input{box-sizing:border-box;flex:1;min-width:0;width:100%;height:100%;padding-inline:var(--spui-input-padding-inline,var(--spui-control-padding-inline,12px));border:0;outline:0;background:transparent;color:inherit;font:var(--spui-input-font,var(--spui-font-family,inherit));font-size:inherit}input::placeholder{color:var(--spui-ui-muted,var(--spui-semantic-body-subtext,#616161));opacity:1}input:disabled{cursor:not-allowed;opacity:.55}.control:has(input:disabled){opacity:.65}slot[name=before],slot[name=after]{display:inline-flex;align-items:center;flex:none;color:var(--spui-input-content-color,var(--spui-ui-muted,var(--spui-semantic-body-subtext,#616161)))}slot[name=before]{padding-inline-start:12px}slot[name=after]{padding-inline-end:12px}:host([data-spui-appearance=glass]) .control{backdrop-filter:blur(12px) saturate(1.2)}@media(prefers-reduced-motion:reduce){.control{transition:none}}@media(forced-colors:active){.control{border-color:FieldText;background:Field;color:FieldText}.control:focus-within{outline:2px solid Highlight;outline-offset:2px}input::placeholder{color:GrayText}}`;
213
+ style.textContent += `slot[name=input]{display:contents}slot[name=input]::slotted(input){box-sizing:border-box;flex:1;min-width:0;width:100%;height:100%;padding-inline:var(--spui-input-padding-inline,var(--spui-control-padding-inline,12px));border:0;outline:0;background:transparent;color:inherit;font:var(--spui-input-font,var(--spui-font-family,inherit));font-size:inherit}slot[name=input]::slotted(input:disabled){cursor:not-allowed;opacity:.55}`;
214
+ style.textContent += `input[type=checkbox],input[type=radio],slot[name=input]::slotted(input[type=checkbox]),slot[name=input]::slotted(input[type=radio]){box-sizing:border-box;flex:none;width:16px;height:16px;padding:0}`;
215
+ const control = document.createElement("div");
216
+ control.className = "control";
217
+ control.part.add("control");
218
+ control.dataset.size = this.getAttribute("size") || "medium";
219
+ control.dataset.appearance = this.getAttribute("appearance") || "outline";
220
+ const before = document.createElement("slot");
221
+ before.name = "before";
222
+ before.setAttribute("aria-hidden", "true");
223
+ const input = document.createElement("input");
224
+ input.className = "input-fallback";
225
+ input.type = this.getAttribute("type") || "text";
226
+ if (input.type !== "file")
227
+ input.value = this.defaultValue;
228
+ input.placeholder = this.getAttribute("placeholder") || "";
229
+ input.disabled = this.disabled || this.formDisabled;
230
+ input.required = this.required;
231
+ input.readOnly = this.readOnly;
232
+ input.part.add("input");
233
+ for (const name of ["autocomplete", "inputmode", "min", "max", "step", "minlength", "maxlength", "pattern", "accept", "alt", "capture", "form", "formaction", "formenctype", "formmethod", "formnovalidate", "formtarget", "height", "list", "src", "width", "aria-label", "aria-labelledby", "aria-description", "aria-describedby", "aria-invalid"]) {
234
+ const value = this.getAttribute(name);
235
+ if (value !== null)
236
+ input.setAttribute(name, value);
237
+ }
238
+ if (this.hasAttribute("multiple"))
239
+ input.multiple = true;
240
+ input.defaultChecked = this.hasAttribute("default-checked");
241
+ input.checked = this.hasAttribute("checked") || input.defaultChecked;
242
+ const inputSlot = document.createElement("slot");
243
+ inputSlot.name = "input";
244
+ inputSlot.part.add("input");
245
+ inputSlot.addEventListener("slotchange", () => this.syncInputSlot());
246
+ const after = document.createElement("slot");
247
+ after.name = "after";
248
+ after.setAttribute("aria-hidden", "true");
249
+ control.append(before, input, inputSlot, after);
250
+ shadow.append(style, control);
251
+ this.syncInputSlot();
252
+ }
253
+ }
254
+ customElements.define("spui-input", SpuiInput);
255
+ }
@@ -0,0 +1,15 @@
1
+ export type LabelSize = "small" | "medium" | "large";
2
+ export type LabelWeight = "regular" | "semibold";
3
+ export type LabelOptions = {
4
+ htmlFor?: string;
5
+ size?: LabelSize;
6
+ weight?: LabelWeight;
7
+ required?: boolean | string | Node;
8
+ disabled?: boolean;
9
+ id?: string;
10
+ title?: string;
11
+ };
12
+ export declare function ensureLabelStyles(document: Document): void;
13
+ export declare function getLabelStyle(size?: LabelSize, disabled?: boolean, weight?: LabelWeight): Record<string, string | number>;
14
+ /** Create a real native label so `for` association and click-to-focus work across frameworks. */
15
+ export declare function createLabel(content: string | Node, options?: LabelOptions): HTMLLabelElement;
@@ -0,0 +1,56 @@
1
+ const styledDocuments = new WeakSet();
2
+ export function ensureLabelStyles(document) {
3
+ if (styledDocuments.has(document))
4
+ return;
5
+ if (document.querySelector("style[data-spui-label-styles]")) {
6
+ styledDocuments.add(document);
7
+ return;
8
+ }
9
+ const style = document.createElement("style");
10
+ style.dataset.spuiLabelStyles = "";
11
+ style.textContent = "@media(forced-colors:active){.spui-label[data-disabled=true],.spui-label[data-disabled=true] .spui-label-required{color:GrayText!important}}";
12
+ (document.head ?? document.documentElement).append(style);
13
+ styledDocuments.add(document);
14
+ }
15
+ const sizes = {
16
+ small: ["200", "12px", "16px"], medium: ["300", "14px", "20px"], large: ["400", "16px", "22px"],
17
+ };
18
+ export function getLabelStyle(size = "medium", disabled = false, weight = "regular") {
19
+ const [token, fallback, line] = sizes[size];
20
+ return {
21
+ display: "inline-flex", alignItems: "baseline", gap: "0.25em",
22
+ fontFamily: "var(--spui-font-family,var(--fontFamilyBase,inherit))", fontSize: `var(--fontSizeBase${token},${fallback})`,
23
+ fontWeight: weight === "semibold" || size === "large" ? "var(--fontWeightSemibold,600)" : "var(--fontWeightRegular,400)", lineHeight: `var(--lineHeightBase${token},${line})`, cursor: disabled ? "default" : "pointer",
24
+ color: disabled ? "var(--spui-ui-foreground-disabled,var(--spui-semantic-disabled-text,var(--colorNeutralForegroundDisabled,#bdbdbd)))" : "var(--spui-ui-text,var(--spui-semantic-body-text,inherit))",
25
+ };
26
+ }
27
+ /** Create a real native label so `for` association and click-to-focus work across frameworks. */
28
+ export function createLabel(content, options = {}) {
29
+ const label = document.createElement("label");
30
+ ensureLabelStyles(label.ownerDocument);
31
+ label.className = "spui-label";
32
+ if (options.htmlFor)
33
+ label.htmlFor = options.htmlFor;
34
+ if (options.id)
35
+ label.id = options.id;
36
+ if (options.title)
37
+ label.title = options.title;
38
+ label.dataset.size = options.size ?? "medium";
39
+ label.dataset.disabled = String(Boolean(options.disabled));
40
+ Object.assign(label.style, getLabelStyle(options.size, options.disabled, options.weight));
41
+ label.append(typeof content === "string" ? document.createTextNode(content) : content);
42
+ if (options.required) {
43
+ const marker = document.createElement("span");
44
+ marker.className = "spui-label-required";
45
+ marker.setAttribute("aria-hidden", "true");
46
+ if (options.required === true)
47
+ marker.textContent = "*";
48
+ else if (typeof options.required === "string")
49
+ marker.textContent = options.required;
50
+ else
51
+ marker.append(options.required);
52
+ Object.assign(marker.style, { color: options.disabled ? "var(--spui-ui-foreground-disabled,var(--spui-semantic-disabled-text,var(--colorNeutralForegroundDisabled,#bdbdbd)))" : "var(--spui-label-required-color,var(--colorPaletteRedForeground3,var(--spui-semantic-error-text,#c50f1f)))", paddingInlineStart: "var(--spui-label-required-gap,var(--spacingHorizontalXS,4px))" });
53
+ label.append(marker);
54
+ }
55
+ return label;
56
+ }
@@ -0,0 +1 @@
1
+ export declare function defineLink(): void;
@@ -0,0 +1,92 @@
1
+ import { base } from "./shared.js";
2
+ const forwarded = ["href", "target", "rel", "download", "hreflang", "referrerpolicy", "title", "type", "media", "ping", "role", "aria-label", "aria-describedby", "aria-current", "tabindex"];
3
+ const unsafeSchemes = /^(?:javascript|data|vbscript):/i;
4
+ export function defineLink() {
5
+ if (customElements.get("spui-link"))
6
+ return;
7
+ class SpuiLink extends HTMLElement {
8
+ static observedAttributes = [...forwarded, "disabled", "disabled-focusable", "appearance", "underline", "inline", "as"];
9
+ connectedCallback() { if (!this.shadowRoot)
10
+ this.render(); this.sync(); }
11
+ attributeChangedCallback() { if (this.isConnected)
12
+ this.sync(); }
13
+ sync() {
14
+ const root = this.shadowRoot?.querySelector("a,button,span");
15
+ if (!root)
16
+ return;
17
+ const href = this.getAttribute("href")?.trim() ?? "";
18
+ const as = this.getAttribute("as") || (href ? "a" : "button");
19
+ if (root.localName !== as) {
20
+ this.renderRoot(as);
21
+ return;
22
+ }
23
+ for (const name of forwarded) {
24
+ const value = this.getAttribute(name);
25
+ if (value === null)
26
+ root.removeAttribute(name);
27
+ else
28
+ root.setAttribute(name, value);
29
+ }
30
+ if (as !== "a")
31
+ root.removeAttribute("href");
32
+ const disabled = this.hasAttribute("disabled") || (as === "a" && (!href || unsafeSchemes.test(href)));
33
+ if (root instanceof HTMLAnchorElement && (disabled || as !== "a"))
34
+ root.removeAttribute("href");
35
+ if (as === "span")
36
+ root.setAttribute("role", this.getAttribute("role") || "button");
37
+ if (as === "a" && (disabled || this.hasAttribute("disabled-focusable")))
38
+ root.setAttribute("role", this.getAttribute("role") || "link");
39
+ const disabledFocusable = this.hasAttribute("disabled") && this.hasAttribute("disabled-focusable") && (as !== "a" || (!!href && !unsafeSchemes.test(href)));
40
+ if (as === "span" || as === "a")
41
+ root.tabIndex = this.hasAttribute("tabindex") ? Number(this.getAttribute("tabindex")) : disabled && !disabledFocusable ? -1 : 0;
42
+ else if (this.hasAttribute("tabindex"))
43
+ root.tabIndex = Number(this.getAttribute("tabindex"));
44
+ if (disabled || this.hasAttribute("disabled-focusable"))
45
+ root.setAttribute("aria-disabled", "true");
46
+ else
47
+ root.removeAttribute("aria-disabled");
48
+ if (root instanceof HTMLButtonElement)
49
+ root.disabled = disabled && !disabledFocusable;
50
+ if (root instanceof HTMLButtonElement && !root.hasAttribute("type"))
51
+ root.type = "button";
52
+ if (disabled)
53
+ root.removeAttribute("target");
54
+ if (!disabled && this.getAttribute("target")?.toLowerCase() === "_blank") {
55
+ const rel = new Set((this.getAttribute("rel") ?? "").split(/\s+/).filter(Boolean));
56
+ rel.add("noopener");
57
+ rel.add("noreferrer");
58
+ root.setAttribute("rel", [...rel].join(" "));
59
+ }
60
+ }
61
+ render() {
62
+ const shadow = this.attachShadow({ mode: "open" });
63
+ const style = document.createElement("style");
64
+ style.textContent = `${base}:host{display:inline;color:var(--spui-link-color,var(--spui-semantic-link,var(--colorBrandForeground1,#115ea3)))}a,button,span{box-sizing:border-box;background:transparent;border:0;color:inherit;font:inherit;text-align:inherit;text-decoration:none;cursor:pointer;padding:0;margin:0}a,span{display:inline}button{display:inline}a:visited{color:var(--spui-link-visited,var(--spui-semantic-link-visited,var(--colorBrandForeground2,#4f52b2))))}a:hover,button:hover,span:hover{text-decoration:underline}a:focus-visible,button:focus-visible,span:focus-visible{outline:2px solid var(--spui-focus-ring,var(--spui-semantic-focus-border,#005a9e));outline-offset:2px} [aria-disabled=true]{color:var(--spui-ui-foreground-disabled,var(--spui-ui-muted,#777));cursor:not-allowed;text-decoration:none}[aria-disabled=true]:hover,[aria-disabled=true]:active{text-decoration:none}@media(forced-colors:active){[aria-disabled=true]{color:GrayText}}:host([underline=always]) a,:host([underline=always]) button,:host([underline=always]) span{text-decoration:underline;text-decoration-thickness:from-font;text-underline-offset:2px}:host([underline=hover]) a,:host([underline=hover]) button,:host([underline=hover]) span{text-decoration:none}:host([underline=hover]) a:hover,:host([underline=hover]) button:hover,:host([underline=hover]) span:hover,:host([underline=hover]) a:focus-visible,:host([underline=hover]) button:focus-visible,:host([underline=hover]) span:focus-visible,:host([inline]) a,:host([inline]) button,:host([inline]) span{text-decoration:underline}:host([appearance=subtle]){color:var(--spui-ui-text,var(--spui-semantic-body-text,#323130))}:host([appearance=subtle]) a:visited{color:inherit}`;
65
+ shadow.append(style);
66
+ this.renderRoot(this.getAttribute("as") || (this.getAttribute("href") ? "a" : "button"));
67
+ this.addEventListener("click", event => { if (this.isDisabled()) {
68
+ event.preventDefault();
69
+ event.stopImmediatePropagation();
70
+ } }, true);
71
+ this.addEventListener("keydown", event => { if (this.isDisabled() && (event.key === "Enter" || event.key === " ")) {
72
+ event.preventDefault();
73
+ event.stopImmediatePropagation();
74
+ }
75
+ else if (this.shadowRoot?.querySelector("span") && (event.key === "Enter" || event.key === " ")) {
76
+ event.preventDefault();
77
+ this.shadowRoot.querySelector("span")?.click();
78
+ } }, true);
79
+ }
80
+ isDisabled() { const href = this.getAttribute("href")?.trim() ?? ""; return this.hasAttribute("disabled") || this.hasAttribute("disabled-focusable") || (this.getAttribute("as") === "a" || (!this.hasAttribute("as") && !!href)) && (!href || unsafeSchemes.test(href)); }
81
+ renderRoot(as) {
82
+ if (!this.shadowRoot)
83
+ return;
84
+ const root = document.createElement(["a", "button", "span"].includes(as) ? as : "span");
85
+ root.part.add("link");
86
+ root.append(document.createElement("slot"));
87
+ this.shadowRoot.querySelector("a,button,span")?.replaceWith(root) ?? this.shadowRoot.append(root);
88
+ this.sync();
89
+ }
90
+ }
91
+ customElements.define("spui-link", SpuiLink);
92
+ }
@@ -0,0 +1,19 @@
1
+ export type ListItemValue = string | number;
2
+ export type ListSelectionMode = "single" | "multiselect";
3
+ export type ListNavigationMode = "items" | "composite";
4
+ export type ListSelectionChangeData = {
5
+ event: Event;
6
+ type: "change";
7
+ selectedItems: ListItemValue[];
8
+ };
9
+ export type ListItemActionEvent = CustomEvent<{
10
+ originalEvent: Event;
11
+ value: ListItemValue;
12
+ }>;
13
+ export type ListItemActionData = {
14
+ event: ListItemActionEvent;
15
+ type: "ListItemAction";
16
+ value: ListItemValue;
17
+ };
18
+ export declare function defineListItem(): void;
19
+ export declare function defineList(): void;