@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,4 @@
1
+ export type ProgressBarColor = "brand" | "success" | "warning" | "error";
2
+ export type ProgressBarShape = "rounded" | "square";
3
+ export type ProgressBarThickness = "medium" | "large";
4
+ export declare function defineProgressBar(): void;
@@ -0,0 +1,82 @@
1
+ import { base } from "./shared.js";
2
+ export function defineProgressBar() {
3
+ if (customElements.get("spui-progress-bar"))
4
+ return;
5
+ class SpuiProgressBar extends HTMLElement {
6
+ static observedAttributes = ["value", "max", "label", "aria-label", "aria-labelledby", "aria-describedby", "aria-valuetext", "color", "shape", "thickness"];
7
+ generatedLabel;
8
+ connectedCallback() {
9
+ if (!this.shadowRoot) {
10
+ const shadow = this.attachShadow({ mode: "open" });
11
+ const progress = document.createElement("div");
12
+ progress.className = "track";
13
+ progress.setAttribute("aria-hidden", "true");
14
+ progress.part.add("track");
15
+ const bar = document.createElement("slot");
16
+ bar.name = "bar";
17
+ bar.append(Object.assign(document.createElement("span"), { className: "indicator" }));
18
+ progress.append(bar);
19
+ const motion = document.createElement("slot");
20
+ motion.name = "indeterminateMotion";
21
+ motion.setAttribute("aria-hidden", "true");
22
+ progress.append(motion);
23
+ const style = document.createElement("style");
24
+ style.textContent = `${base}:host{display:block;min-width:0}.track{position:relative;display:block;width:100%;height:2px;overflow:hidden;border-radius:var(--spui-progress-radius,999px);background:var(--spui-progress-track,color-mix(in srgb,currentColor 10%,transparent))}.indicator{position:absolute;inset-block:0;inset-inline-start:0;width:100%;border-radius:inherit;background:var(--spui-progress-color,var(--spui-ui-accent,var(--spui-semantic-focus-border,#0f6cbd)));transform-origin:left center}:host([dir=rtl]) .indicator{transform-origin:right center}:host([data-determinate-transition]) .indicator,:host([data-determinate-transition]) slot[name=bar]::slotted([slot=bar]){transition:transform 300ms ease,width 300ms ease}:host([thickness=large]) .track{height:4px}:host([shape=square]){--spui-progress-radius:0px}:host([color=success]){--spui-progress-color:var(--spui-ui-success,#107c10)}:host([color=warning]){--spui-progress-color:var(--spui-ui-warning,#835b00)}:host([color=error]){--spui-progress-color:var(--spui-ui-danger,#b10e1c)}:host([data-indeterminate]) .indicator{width:33%;animation:spui-progress-indeterminate 3s linear infinite}:host([dir=rtl][data-indeterminate]) .indicator{animation-name:spui-progress-indeterminate-rtl}:host([data-indeterminate][data-custom-motion]) .indicator{display:none}slot[name=bar]::slotted([slot=bar]){width:var(--spui-progress-value,100%);height:100%;border-radius:inherit;background:var(--spui-progress-color,var(--spui-ui-accent,var(--spui-semantic-focus-border,#0f6cbd)))}:host([data-indeterminate]) slot[name=bar]::slotted([slot=bar]){width:33%;animation:spui-progress-indeterminate 3s linear infinite}:host([dir=rtl][data-indeterminate]) slot[name=bar]::slotted([slot=bar]){animation-name:spui-progress-indeterminate-rtl}slot[name=indeterminateMotion]::slotted(*){position:absolute;inset-block:0;inset-inline-start:0;width:33%;height:100%}:host([dir=rtl]) slot[name=indeterminateMotion]::slotted(*){inset-inline-start:auto;inset-inline-end:0}@keyframes spui-progress-indeterminate{from{transform:translateX(-100%)}to{transform:translateX(300%)}}@keyframes spui-progress-indeterminate-rtl{from{transform:translateX(100%)}to{transform:translateX(-300%)}}@media(prefers-reduced-motion:reduce){.indicator{transition:none}:host([data-indeterminate]) .indicator,:host([dir=rtl][data-indeterminate]) .indicator{width:100%;animation:spui-progress-pulse 3s linear infinite}:host([data-indeterminate]) slot[name=bar]::slotted([slot=bar]),:host([dir=rtl][data-indeterminate]) slot[name=bar]::slotted([slot=bar]){width:100%;animation:spui-progress-pulse 3s linear infinite}}@keyframes spui-progress-pulse{0%,100%{opacity:.2}50%{opacity:1}}@media(forced-colors:active){.track{forced-color-adjust:none;background:Canvas;border:1px solid CanvasText}.indicator,slot[name=bar]::slotted([slot=bar]){background:Highlight}}`;
25
+ style.textContent += `:host(:not([color])),:host([color=brand]){--spui-progress-color:color-mix(in srgb,var(--spui-ui-accent,#0f6cbd) 45%,currentColor 55%)}:host([color=success]){--spui-progress-color:color-mix(in srgb,var(--spui-ui-success,#107c10) 45%,currentColor 55%)}:host([color=warning]){--spui-progress-color:color-mix(in srgb,var(--spui-ui-warning,#835b00) 45%,currentColor 55%)}:host([color=error]){--spui-progress-color:color-mix(in srgb,var(--spui-ui-danger,#b10e1c) 45%,currentColor 55%)}`;
26
+ shadow.append(style, progress);
27
+ progress.querySelectorAll("slot").forEach(slot => slot.addEventListener("slotchange", () => this.sync()));
28
+ }
29
+ this.sync();
30
+ }
31
+ attributeChangedCallback() { if (this.isConnected)
32
+ this.sync(); }
33
+ sync() {
34
+ const progress = this.shadowRoot?.querySelector(".track");
35
+ if (!progress)
36
+ return;
37
+ this.toggleAttribute("data-custom-motion", (this.shadowRoot?.querySelector('slot[name="indeterminateMotion"]')?.assignedElements().length ?? 0) > 0);
38
+ this.setAttribute("role", "progressbar");
39
+ const max = Number(this.getAttribute("max"));
40
+ const normalizedMax = Number.isFinite(max) && max > 0 ? max : 1;
41
+ const raw = this.getAttribute("value");
42
+ const value = raw === null ? null : Number(raw);
43
+ this.setAttribute("aria-valuemin", "0");
44
+ this.setAttribute("aria-valuemax", String(normalizedMax));
45
+ if (value === null || !Number.isFinite(value)) {
46
+ this.setAttribute("data-indeterminate", "");
47
+ this.removeAttribute("data-determinate-transition");
48
+ this.removeAttribute("aria-valuenow");
49
+ const indicator = this.shadowRoot?.querySelector(".indicator");
50
+ if (indicator)
51
+ indicator.style.removeProperty("transform");
52
+ const slottedBar = this.shadowRoot?.querySelector("slot[name=bar]")?.assignedElements()[0];
53
+ slottedBar?.style.removeProperty("--spui-progress-value");
54
+ }
55
+ else {
56
+ this.removeAttribute("data-indeterminate");
57
+ const bounded = Math.min(normalizedMax, Math.max(0, value));
58
+ this.toggleAttribute("data-determinate-transition", bounded / normalizedMax > 0.01);
59
+ this.setAttribute("aria-valuenow", String(bounded));
60
+ const indicator = this.shadowRoot?.querySelector(".indicator");
61
+ if (indicator)
62
+ indicator.style.transform = `scaleX(${bounded / normalizedMax})`;
63
+ const slottedBar = this.shadowRoot?.querySelector("slot[name=bar]")?.assignedElements()[0];
64
+ slottedBar?.style.setProperty("--spui-progress-value", `${bounded / normalizedMax * 100}%`);
65
+ }
66
+ if (this.hasAttribute("aria-labelledby")) {
67
+ if (this.generatedLabel !== undefined && this.getAttribute("aria-label") === this.generatedLabel)
68
+ this.removeAttribute("aria-label");
69
+ this.generatedLabel = undefined;
70
+ }
71
+ else if (this.getAttribute("aria-label") === null || this.getAttribute("aria-label") === this.generatedLabel) {
72
+ const label = this.getAttribute("label") ?? "Loading";
73
+ this.generatedLabel = label;
74
+ if (this.getAttribute("aria-label") !== label)
75
+ this.setAttribute("aria-label", label);
76
+ }
77
+ else
78
+ this.generatedLabel = undefined;
79
+ }
80
+ }
81
+ customElements.define("spui-progress-bar", SpuiProgressBar);
82
+ }
@@ -0,0 +1 @@
1
+ export declare function defineRadioGroup(): void;
@@ -0,0 +1,303 @@
1
+ import { base, stopHostAccessibleNameSync, syncHostAccessibleName } from "./shared.js";
2
+ function radioOptionLabel(option, fallback) {
3
+ const text = [...option.childNodes].filter(node => !(node instanceof Element && ["input", "indicator", "label"].includes(node.getAttribute("slot") ?? ""))).map(node => node.textContent ?? "").join("").trim();
4
+ return option.getAttribute("label") ?? (text || fallback);
5
+ }
6
+ const radioRootInputAttributes = new WeakMap();
7
+ const radioManagedInputAttributes = new Set(["checked", "disabled", "hidden", "label", "label-position", "name", "required", "slot", "type", "value"]);
8
+ function isForwardableRadioInputAttribute(name) {
9
+ return !radioManagedInputAttributes.has(name) && !name.startsWith("on");
10
+ }
11
+ function syncRadioRootProps(option, input, label) {
12
+ label.className = option.getAttribute("class") ?? option.getAttribute("classname") ?? "";
13
+ label.style.cssText = option.getAttribute("style") ?? "";
14
+ for (const name of radioRootInputAttributes.get(input) ?? [])
15
+ input.removeAttribute(name);
16
+ const applied = new Set();
17
+ for (const attribute of [...option.attributes]) {
18
+ if (attribute.name !== "class" && attribute.name !== "style" && isForwardableRadioInputAttribute(attribute.name)) {
19
+ input.setAttribute(attribute.name, attribute.value);
20
+ applied.add(attribute.name);
21
+ }
22
+ }
23
+ radioRootInputAttributes.set(input, applied);
24
+ }
25
+ const radioInputSlotAttributes = new WeakMap();
26
+ function syncRadioInputSlot(source, input) {
27
+ for (const name of radioInputSlotAttributes.get(input) ?? [])
28
+ input.removeAttribute(name);
29
+ const applied = new Set();
30
+ if (!source) {
31
+ radioInputSlotAttributes.set(input, applied);
32
+ return;
33
+ }
34
+ for (const attribute of [...source.attributes]) {
35
+ if (isForwardableRadioInputAttribute(attribute.name)) {
36
+ input.setAttribute(attribute.name, attribute.value);
37
+ applied.add(attribute.name);
38
+ }
39
+ }
40
+ radioInputSlotAttributes.set(input, applied);
41
+ }
42
+ function createRadioIndicator(source) {
43
+ const indicator = source?.cloneNode(true) ?? document.createElement("span");
44
+ indicator.removeAttribute("slot");
45
+ indicator.removeAttribute("hidden");
46
+ indicator.setAttribute("aria-hidden", "true");
47
+ indicator.classList.add("radio-indicator");
48
+ indicator.part.add("indicator");
49
+ return indicator;
50
+ }
51
+ function createRadioLabel(source, text) {
52
+ const label = document.createElement("span");
53
+ label.dataset.radioLabel = "";
54
+ if (source) {
55
+ const content = source.cloneNode(true);
56
+ content.removeAttribute("slot");
57
+ content.removeAttribute("hidden");
58
+ label.append(content);
59
+ }
60
+ else
61
+ label.textContent = text;
62
+ return label;
63
+ }
64
+ export function defineRadioGroup() {
65
+ if (customElements.get("spui-radio-group"))
66
+ return;
67
+ class SpuiRadioGroup extends HTMLElement {
68
+ static formAssociated = true;
69
+ static observedAttributes = ["label", "value", "default-value", "disabled", "required", "name", "orientation", "layout", "aria-label", "aria-labelledby", "aria-description", "aria-describedby", "aria-invalid"];
70
+ internals;
71
+ observer;
72
+ currentValue;
73
+ formDisabledValue = false;
74
+ constructor() {
75
+ super();
76
+ try {
77
+ this.internals = this.attachInternals();
78
+ }
79
+ catch { /* Keep the controls usable where custom form association is unavailable. */ }
80
+ }
81
+ get form() { return this.internals?.form ?? null; }
82
+ get validity() { return this.internals?.validity; }
83
+ get validationMessage() { return this.internals?.validationMessage ?? ""; }
84
+ get willValidate() { return this.internals?.willValidate ?? false; }
85
+ checkValidity() { return this.internals?.checkValidity() ?? true; }
86
+ reportValidity() { return this.internals?.reportValidity() ?? true; }
87
+ connectedCallback() {
88
+ this.render();
89
+ this.observer = new MutationObserver(records => {
90
+ let optionsChanged = false;
91
+ const textChanged = new Set();
92
+ for (const record of records) {
93
+ if (record.type === "attributes") {
94
+ const option = record.target instanceof Element ? record.target.closest("spui-radio-option") : null;
95
+ if (option?.parentElement === this && record.target === option && ["value", "disabled", "checked"].includes(record.attributeName ?? ""))
96
+ optionsChanged = true;
97
+ else if (option?.parentElement === this)
98
+ textChanged.add(option);
99
+ else if (record.target === this)
100
+ optionsChanged = true;
101
+ continue;
102
+ }
103
+ const target = record.target instanceof Element ? record.target : record.target.parentElement;
104
+ const option = target?.closest("spui-radio-option");
105
+ if (option?.parentElement === this)
106
+ textChanged.add(option);
107
+ else if (record.target === this)
108
+ optionsChanged = true;
109
+ }
110
+ if (optionsChanged)
111
+ this.syncOptions();
112
+ else
113
+ for (const option of textChanged)
114
+ this.syncOptionLabel(option);
115
+ });
116
+ this.observer.observe(this, { childList: true, subtree: true, characterData: true, attributes: true });
117
+ }
118
+ disconnectedCallback() { this.observer?.disconnect(); stopHostAccessibleNameSync(this); }
119
+ attributeChangedCallback(name, _oldValue, value) {
120
+ if (!this.isConnected)
121
+ return;
122
+ const fieldset = this.shadowRoot?.querySelector("fieldset");
123
+ if (!fieldset)
124
+ return;
125
+ if (name === "label") {
126
+ const legend = this.shadowRoot?.querySelector("legend");
127
+ if (legend)
128
+ legend.textContent = value ?? "";
129
+ }
130
+ if (name === "disabled")
131
+ fieldset.disabled = this.formDisabledValue || this.hasAttribute("disabled");
132
+ if (name === "orientation" || name === "layout")
133
+ fieldset.dataset.layout = this.getLayout();
134
+ if (name === "aria-label" || name === "aria-labelledby" || name === "aria-description" || name === "aria-describedby" || name === "aria-invalid") {
135
+ const value = this.getAttribute(name);
136
+ if (value === null)
137
+ fieldset.removeAttribute(name);
138
+ else
139
+ fieldset.setAttribute(name, value);
140
+ syncHostAccessibleName(this, fieldset);
141
+ }
142
+ if (name === "value")
143
+ this.currentValue = value ?? "";
144
+ for (const radio of fieldset.querySelectorAll('input[type="radio"]')) {
145
+ if (name === "name")
146
+ radio.name = value ?? (this.id || "spui-radio-group");
147
+ if (name === "required")
148
+ radio.required = this.hasAttribute("required");
149
+ if (name === "value")
150
+ radio.checked = radio.value === (this.currentValue ?? "");
151
+ if (name === "disabled")
152
+ radio.disabled = this.hasAttribute("disabled") || radio.dataset.optionDisabled === "true";
153
+ }
154
+ this.syncFormValue();
155
+ }
156
+ get value() { return this.currentValue ?? this.getAttribute("value") ?? this.shadowRoot?.querySelector('input[type="radio"]:checked')?.value ?? ""; }
157
+ set value(value) {
158
+ this.currentValue = value;
159
+ const fieldset = this.shadowRoot?.querySelector("fieldset");
160
+ if (fieldset) {
161
+ for (const radio of fieldset.querySelectorAll('input[type="radio"]'))
162
+ radio.checked = radio.value === value;
163
+ this.syncFormValue();
164
+ }
165
+ }
166
+ get defaultValue() { return this.getAttribute("default-value") ?? this.querySelector(':scope > spui-radio-option[checked]')?.getAttribute("value") ?? ""; }
167
+ set defaultValue(value) { this.setAttribute("default-value", value); }
168
+ formDisabledCallback(disabled) {
169
+ this.formDisabledValue = disabled;
170
+ for (const radio of this.shadowRoot?.querySelectorAll('input[type="radio"]') ?? [])
171
+ radio.disabled = disabled || this.hasAttribute("disabled") || radio.dataset.optionDisabled === "true";
172
+ this.syncFormValue();
173
+ }
174
+ formResetCallback() {
175
+ const options = [...this.querySelectorAll(":scope > spui-radio-option")];
176
+ const initial = options.find(option => option.hasAttribute("checked"))?.getAttribute("value") ?? "";
177
+ this.value = this.getAttribute("value") ?? this.getAttribute("default-value") ?? initial;
178
+ }
179
+ formStateRestoreCallback(state) { if (typeof state === "string")
180
+ this.value = state; }
181
+ syncFormValue() {
182
+ const checked = this.shadowRoot?.querySelector('input[type="radio"]:checked');
183
+ const disabled = this.formDisabledValue || this.hasAttribute("disabled");
184
+ const value = checked?.value ?? "";
185
+ for (const label of this.shadowRoot?.querySelectorAll("fieldset > label") ?? [])
186
+ label.dataset.checked = String(Boolean(label.querySelector("input")?.checked));
187
+ this.internals?.setFormValue(checked && !disabled ? value : null);
188
+ try {
189
+ const anchor = checked ?? this.shadowRoot?.querySelector('input[type="radio"]');
190
+ const missing = !disabled && this.hasAttribute("required") && !checked;
191
+ this.internals?.setValidity(missing ? { valueMissing: true } : {}, missing ? anchor?.validationMessage ?? "" : "", missing ? anchor ?? undefined : undefined);
192
+ }
193
+ catch { /* Some engines do not accept shadow descendants as validity anchors. */ }
194
+ }
195
+ syncOptionLabel(option) {
196
+ const index = [...this.querySelectorAll(":scope > spui-radio-option")].indexOf(option);
197
+ const label = this.shadowRoot?.querySelectorAll("fieldset > label")[index];
198
+ const input = label?.querySelector("input");
199
+ const text = label?.querySelector(":scope > [data-radio-label]");
200
+ if (input) {
201
+ syncRadioRootProps(option, input, label);
202
+ syncRadioInputSlot(option.querySelector(":scope > input[slot=input]"), input);
203
+ const sourceIndicator = option.querySelector(":scope > [slot=indicator]");
204
+ const currentIndicator = label?.querySelector(".radio-indicator");
205
+ if (currentIndicator && sourceIndicator)
206
+ currentIndicator.replaceWith(createRadioIndicator(sourceIndicator));
207
+ else if (currentIndicator && !sourceIndicator)
208
+ currentIndicator.remove();
209
+ else if (label && sourceIndicator)
210
+ label.insertBefore(createRadioIndicator(sourceIndicator), text ?? null);
211
+ label?.toggleAttribute("data-custom-indicator", Boolean(sourceIndicator));
212
+ input.dataset.customIndicator = String(Boolean(sourceIndicator));
213
+ if (label)
214
+ label.dataset.labelPosition = option.getAttribute("label-position") ?? (this.getLayout() === "horizontal-stacked" ? "below" : "after");
215
+ }
216
+ if (label && input) {
217
+ const nextText = createRadioLabel(option.querySelector(":scope > [slot=label]"), radioOptionLabel(option, input.value));
218
+ if (text)
219
+ text.replaceWith(nextText);
220
+ else
221
+ label.append(nextText);
222
+ }
223
+ }
224
+ syncOptions() {
225
+ const fieldset = this.shadowRoot?.querySelector("fieldset");
226
+ if (!fieldset)
227
+ return;
228
+ const current = this.currentValue ?? this.getAttribute("value") ?? this.getAttribute("default-value") ?? fieldset.querySelector('input[type="radio"]:checked')?.value ?? "";
229
+ const options = [...this.querySelectorAll(":scope > spui-radio-option")];
230
+ const radios = options.map((option, index) => this.createRadio(option, index));
231
+ fieldset.replaceChildren(fieldset.querySelector("legend"), ...radios);
232
+ const checked = fieldset.querySelector(`input[type="radio"][value="${CSS.escape(current)}"]`);
233
+ if (checked)
234
+ checked.checked = true;
235
+ this.syncFormValue();
236
+ }
237
+ createRadio(option, index) {
238
+ const label = document.createElement("label");
239
+ label.part.add("option");
240
+ label.dataset.labelPosition = option.getAttribute("label-position") ?? (this.getLayout() === "horizontal-stacked" ? "below" : "after");
241
+ const inputSource = option.querySelector(":scope > input[slot=input]");
242
+ const input = (inputSource?.cloneNode(false) ?? document.createElement("input"));
243
+ input.removeAttribute("slot");
244
+ input.removeAttribute("hidden");
245
+ input.type = "radio";
246
+ input.name = this.getAttribute("name") ?? (this.id || "spui-radio-group");
247
+ input.value = option.getAttribute("value") ?? String(index);
248
+ input.disabled = this.formDisabledValue || this.hasAttribute("disabled") || option.hasAttribute("disabled");
249
+ input.required = this.hasAttribute("required");
250
+ input.dataset.optionDisabled = String(option.hasAttribute("disabled"));
251
+ input.part.add("control");
252
+ syncRadioRootProps(option, input, label);
253
+ syncRadioInputSlot(inputSource, input);
254
+ const text = createRadioLabel(option.querySelector(":scope > [slot=label]"), radioOptionLabel(option, input.value));
255
+ const indicatorSource = option.querySelector(":scope > [slot=indicator]");
256
+ label.toggleAttribute("data-custom-indicator", Boolean(indicatorSource));
257
+ input.dataset.customIndicator = String(Boolean(indicatorSource));
258
+ label.append(input);
259
+ if (indicatorSource)
260
+ label.append(createRadioIndicator(indicatorSource));
261
+ label.append(text);
262
+ input.addEventListener("change", event => {
263
+ if (!input.checked)
264
+ return;
265
+ this.currentValue = input.value;
266
+ this.syncFormValue();
267
+ option.dispatchEvent(new CustomEvent("spui-radio-change", { detail: { value: input.value, sourceEvent: event }, bubbles: true, composed: true }));
268
+ this.dispatchEvent(new CustomEvent("spui-change", { detail: { value: input.value, sourceEvent: event }, bubbles: true, composed: true }));
269
+ });
270
+ return label;
271
+ }
272
+ render() {
273
+ const shadow = this.shadowRoot ?? this.attachShadow({ mode: "open" });
274
+ const fieldset = document.createElement("fieldset");
275
+ fieldset.disabled = this.formDisabledValue || this.hasAttribute("disabled");
276
+ fieldset.dataset.layout = this.getLayout();
277
+ for (const name of ["aria-label", "aria-labelledby", "aria-description", "aria-describedby", "aria-invalid"]) {
278
+ const value = this.getAttribute(name);
279
+ if (value !== null)
280
+ fieldset.setAttribute(name, value);
281
+ }
282
+ const legend = document.createElement("legend");
283
+ legend.textContent = this.getAttribute("label") ?? "";
284
+ legend.part.add("label");
285
+ const options = [...this.querySelectorAll(":scope > spui-radio-option")];
286
+ const radios = options.map((option, index) => this.createRadio(option, index));
287
+ const initial = this.currentValue ?? this.getAttribute("value") ?? this.getAttribute("default-value") ?? options.find(option => option.hasAttribute("checked"))?.getAttribute("value") ?? "";
288
+ const selected = radios.find(label => label.querySelector("input")?.value === initial)?.querySelector("input");
289
+ if (selected)
290
+ selected.checked = true;
291
+ const style = document.createElement("style");
292
+ style.textContent = `${base}:host{display:block}fieldset{display:flex;flex-direction:column;gap:var(--spui-control-gap,8px);min-width:0;margin:0;padding:0;border:0}fieldset[data-layout="horizontal"],fieldset[data-layout="horizontal-stacked"]{flex-direction:row;flex-wrap:wrap;column-gap:var(--spui-control-gap,16px)}label[data-label-position="below"]{flex-direction:column;align-items:flex-start;justify-content:center}legend{margin-bottom:var(--spui-control-gap,8px);padding:0;color:var(--spui-ui-muted,var(--spui-semantic-body-text,inherit));font-size:14px;font-weight:600}label{display:inline-flex;align-items:center;gap:8px;min-height:var(--spui-control-height,32px);color:var(--spui-ui-text,var(--spui-semantic-body-text,inherit));cursor:pointer}input{width:18px;height:18px;margin:0;accent-color:var(--spui-ui-accent,var(--spui-semantic-input-background-checked,var(--colorBrandBackground,#0f6cbd)));cursor:pointer}input:focus-visible{outline:2px solid var(--spui-focus-ring,#005a9e);outline-offset:2px}input:disabled{cursor:not-allowed}label:has(input:disabled){opacity:.55;cursor:not-allowed}@media(forced-colors:active){input{accent-color:Highlight}input:focus-visible{outline-color:Highlight}}`;
293
+ style.textContent += `.radio-indicator[hidden]{display:none!important}.radio-indicator{box-sizing:border-box;display:inline-grid;place-items:center;flex:none;width:18px;height:18px;border:1px solid var(--spui-ui-stroke,var(--spui-semantic-body-divider,#8a8886));border-radius:50%;color:var(--spui-ui-accent,var(--spui-semantic-accent,#0f6cbd));background:var(--spui-ui-surface,var(--spui-semantic-body-background,#fff))}.radio-indicator::after{content:"";width:10px;height:10px;border-radius:50%;background:currentColor;transform:scale(0);transition:transform var(--spui-motion-duration-fast,120ms) var(--spui-motion-easing-standard,ease)}label[data-checked=true] .radio-indicator{border-color:currentColor}label[data-checked=true] .radio-indicator::after{transform:scale(1)}input[data-custom-indicator=true]{position:absolute;inset-inline-start:0;inset-block-start:calc(50% - 9px);z-index:1;opacity:0}input[data-custom-indicator=true]:focus-visible{outline:0}input[data-custom-indicator=true]:focus-visible+.radio-indicator{outline:2px solid var(--spui-focus-ring,#005a9e);outline-offset:2px}@media(prefers-reduced-motion:reduce){.radio-indicator::after{transition:none}}@media(forced-colors:active){.radio-indicator{border-color:ButtonText;background:Canvas;color:Highlight}.radio-indicator::after{background:Highlight}}`;
294
+ fieldset.append(legend, ...radios);
295
+ fieldset.addEventListener("change", () => this.syncFormValue());
296
+ shadow.replaceChildren(style, fieldset);
297
+ syncHostAccessibleName(this, fieldset);
298
+ this.syncFormValue();
299
+ }
300
+ getLayout() { return this.getAttribute("layout") ?? this.getAttribute("orientation") ?? "vertical"; }
301
+ }
302
+ customElements.define("spui-radio-group", SpuiRadioGroup);
303
+ }
@@ -0,0 +1,2 @@
1
+ export declare function defineRatingItem(): void;
2
+ export declare function defineRatingDisplay(): void;
@@ -0,0 +1,125 @@
1
+ import { base } from "./shared.js";
2
+ function ratingValue(element) {
3
+ const parent = element.closest("spui-rating-display");
4
+ const maxRaw = Number(parent?.getAttribute("max") ?? element.getAttribute("max") ?? 5);
5
+ const max = Number.isInteger(maxRaw) && maxRaw > 1 ? Math.min(20, maxRaw) : 5;
6
+ const valueRaw = Number(parent?.getAttribute("value") ?? element.getAttribute("rating") ?? 0);
7
+ return { value: Number.isFinite(valueRaw) ? Math.max(0, Math.min(max, valueRaw)) : 0, max };
8
+ }
9
+ export function defineRatingItem() {
10
+ if (customElements.get("spui-rating-item"))
11
+ return;
12
+ class SpuiRatingItem extends HTMLElement {
13
+ static observedAttributes = ["value", "rating", "max", "size", "color"];
14
+ connectedCallback() { this.render(); }
15
+ attributeChangedCallback() { if (this.isConnected)
16
+ this.render(); }
17
+ render() {
18
+ const { value: rating } = ratingValue(this);
19
+ const raw = Number(this.getAttribute("value") ?? 1);
20
+ const itemValue = Number.isInteger(raw) && raw > 0 ? raw : 1;
21
+ const amount = Math.max(0, Math.min(1, rating - itemValue + 1));
22
+ this.setAttribute("aria-hidden", "true");
23
+ const shadow = this.shadowRoot ?? this.attachShadow({ mode: "open" });
24
+ const style = document.createElement("style");
25
+ style.textContent = `${base}:host{position:relative;display:inline-block;flex:none;width:var(--spui-rating-size,24px);height:var(--spui-rating-size,24px);font-size:var(--spui-rating-size,24px);line-height:1;color:var(--spui-rating-empty,color-mix(in srgb,var(--spui-ui-text,var(--spui-semantic-body-text,#242424)) 65%,var(--spui-ui-surface,var(--spui-semantic-body-background,#ffffff)) 35%));user-select:none}.outline,.filled{position:absolute;inset:0;display:block;overflow:hidden;white-space:nowrap}.outline slot::slotted(*),.filled slot::slotted(*){font:inherit}.outline slot::slotted(*){color:var(--spui-rating-empty,color-mix(in srgb,var(--spui-ui-text,var(--spui-semantic-body-text,#242424)) 65%,var(--spui-ui-surface,var(--spui-semantic-body-background,#ffffff)) 35%))}.filled{inset-inline-start:0;width:${amount * 100}%;color:var(--spui-rating-color,var(--spui-ui-accent,#0078d4))}.filled slot::slotted(*){color:var(--spui-rating-color,var(--spui-ui-accent,#0078d4))}:host(:not([color])),:host([color=neutral]){--spui-rating-color:var(--spui-ui-text,var(--spui-semantic-body-text,currentColor))}:host([color=brand]){--spui-rating-color:color-mix(in srgb,var(--spui-ui-accent,#0078d4) 65%,var(--spui-ui-text,var(--spui-semantic-body-text,#242424)) 35%)}:host([color=marigold]){--spui-rating-color:color-mix(in srgb,var(--spui-rating-marigold,#c58b00) 65%,var(--spui-ui-text,var(--spui-semantic-body-text,#242424)) 35%)}:host([size=small]){--spui-rating-size:18px}:host([size=medium]){--spui-rating-size:24px}:host([size=large]){--spui-rating-size:30px}:host([size=extra-large]){--spui-rating-size:36px}@media(forced-colors:active){:host{color:CanvasText}.filled{color:Highlight}}`;
26
+ const outline = document.createElement("span");
27
+ outline.className = "outline";
28
+ outline.part.add("unselected-icon");
29
+ const outlineSlot = document.createElement("slot");
30
+ outlineSlot.name = "unselected-icon";
31
+ outlineSlot.textContent = "☆";
32
+ outline.append(outlineSlot);
33
+ const filled = document.createElement("span");
34
+ filled.className = "filled";
35
+ filled.part.add("selected-icon");
36
+ const filledSlot = document.createElement("slot");
37
+ filledSlot.name = "selected-icon";
38
+ filledSlot.textContent = "★";
39
+ filled.append(filledSlot);
40
+ shadow.replaceChildren(style, outline, filled);
41
+ }
42
+ }
43
+ customElements.define("spui-rating-item", SpuiRatingItem);
44
+ }
45
+ export function defineRatingDisplay() {
46
+ defineRatingItem();
47
+ if (customElements.get("spui-rating-display"))
48
+ return;
49
+ class SpuiRatingDisplay extends HTMLElement {
50
+ static observedAttributes = ["value", "max", "count", "compact", "size", "color", "aria-label", "lang"];
51
+ connectedCallback() { this.render(); }
52
+ attributeChangedCallback() { if (this.isConnected)
53
+ this.render(); }
54
+ render() {
55
+ const maxRaw = Number(this.getAttribute("max") ?? 5);
56
+ const max = Number.isInteger(maxRaw) && maxRaw > 1 ? Math.min(20, maxRaw) : 5;
57
+ const hasValue = this.hasAttribute("value");
58
+ const valueRaw = Number(this.getAttribute("value") ?? 0);
59
+ const value = Number.isFinite(valueRaw) ? Math.max(0, Math.min(max, valueRaw)) : 0;
60
+ const countRaw = this.getAttribute("count");
61
+ const explicitLabel = this.getAttribute("aria-label");
62
+ const shadow = this.shadowRoot ?? this.attachShadow({ mode: "open" });
63
+ const style = document.createElement("style");
64
+ style.textContent = `${base}:host{display:inline-flex;align-items:center;gap:6px;min-width:0;color:var(--spui-ui-text,var(--spui-semantic-body-text,inherit));font-family:var(--spui-font-family,inherit)}.root{display:inline-flex;align-items:center;gap:6px}.items{display:inline-flex;align-items:center;gap:2px}.value-text,.count-text{color:var(--spui-ui-muted,var(--spui-semantic-body-subtext,#616161));font-size:var(--spui-rating-label-size,14px);font-variant-numeric:tabular-nums}.count-text:before{content:"("}.count-text:after{content:")"}:host([compact]) .items{gap:0}@media(forced-colors:active){:host{color:CanvasText}}`;
65
+ const root = document.createElement("span");
66
+ root.className = "root";
67
+ root.setAttribute("role", "img");
68
+ root.setAttribute("aria-label", explicitLabel || `${value} of ${max}${countRaw === null ? "" : `, ${Number(countRaw).toLocaleString(this.lang || undefined)} ratings`}`);
69
+ const items = document.createElement("span");
70
+ items.className = "items";
71
+ items.part.add("items");
72
+ const slot = document.createElement("slot");
73
+ const hasItems = [...this.children].some(child => child.localName === "spui-rating-item");
74
+ if (!hasItems) {
75
+ const itemCount = this.hasAttribute("compact") ? 1 : max;
76
+ for (let index = 1; index <= itemCount; index++) {
77
+ const item = document.createElement("spui-rating-item");
78
+ item.setAttribute("value", String(index));
79
+ item.setAttribute("rating", String(value));
80
+ item.setAttribute("max", String(max));
81
+ if (this.hasAttribute("size"))
82
+ item.setAttribute("size", this.getAttribute("size"));
83
+ if (this.hasAttribute("color"))
84
+ item.setAttribute("color", this.getAttribute("color"));
85
+ items.append(item);
86
+ }
87
+ }
88
+ else {
89
+ slot.addEventListener("slotchange", () => this.syncItems(slot, value, max));
90
+ items.append(slot);
91
+ }
92
+ const valueText = document.createElement("span");
93
+ valueText.className = "value-text";
94
+ valueText.part.add("value-text");
95
+ const valueSlot = document.createElement("slot");
96
+ valueSlot.name = "value-text";
97
+ valueSlot.textContent = String(value);
98
+ valueText.append(valueSlot);
99
+ const countText = document.createElement("span");
100
+ countText.className = "count-text";
101
+ countText.part.add("count-text");
102
+ const countSlot = document.createElement("slot");
103
+ countSlot.name = "count-text";
104
+ countSlot.textContent = countRaw === null ? "" : Number(countRaw).toLocaleString(this.lang || undefined);
105
+ countText.append(countSlot);
106
+ if (!hasValue)
107
+ valueText.hidden = true;
108
+ if (countRaw === null)
109
+ countText.hidden = true;
110
+ root.append(items, valueText, countText);
111
+ shadow.replaceChildren(style, root);
112
+ if (hasItems)
113
+ this.syncItems(slot, value, max);
114
+ }
115
+ syncItems(slot, value, max) {
116
+ for (const item of slot.assignedElements().filter((node) => node.localName === "spui-rating-item")) {
117
+ item.setAttribute("rating", String(value));
118
+ item.setAttribute("max", String(max));
119
+ for (const name of ["size", "color"])
120
+ this.hasAttribute(name) ? item.setAttribute(name, this.getAttribute(name)) : item.removeAttribute(name);
121
+ }
122
+ }
123
+ }
124
+ customElements.define("spui-rating-display", SpuiRatingDisplay);
125
+ }
@@ -0,0 +1,2 @@
1
+ /** Selectable, form-associated star rating with optional half steps. */
2
+ export declare function defineRating(): void;