@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,409 @@
1
+ import { base } from "./shared.js";
2
+ let dialogId = 0;
3
+ const dialogParts = [
4
+ ["spui-dialog-surface", "surface"],
5
+ ["spui-dialog-title", "title"],
6
+ ["spui-dialog-content", "content"],
7
+ ["spui-dialog-body", "body"],
8
+ ["spui-dialog-actions", "actions"],
9
+ ["spui-dialog-trigger", "trigger"],
10
+ ];
11
+ const documentScrollLocks = new WeakMap();
12
+ function acquireDocumentScrollLock(doc) {
13
+ const active = documentScrollLocks.get(doc);
14
+ if (active) {
15
+ active.count++;
16
+ return true;
17
+ }
18
+ const body = doc.body;
19
+ const view = doc.defaultView;
20
+ if (!body || !view || Math.floor(body.getBoundingClientRect().height) <= view.innerHeight)
21
+ return false;
22
+ const lock = { count: 1, htmlOverflowY: doc.documentElement.style.overflowY, bodyOverflowY: body.style.overflowY, scrollbarGutter: doc.documentElement.style.scrollbarGutter };
23
+ documentScrollLocks.set(doc, lock);
24
+ doc.documentElement.style.overflowY = "clip";
25
+ doc.documentElement.style.scrollbarGutter = "stable";
26
+ body.style.overflowY = "hidden";
27
+ return true;
28
+ }
29
+ function releaseDocumentScrollLock(doc) {
30
+ const lock = documentScrollLocks.get(doc);
31
+ if (!lock || --lock.count > 0)
32
+ return;
33
+ if (doc.documentElement.style.overflowY === "clip")
34
+ doc.documentElement.style.overflowY = lock.htmlOverflowY;
35
+ if (doc.documentElement.style.scrollbarGutter === "stable")
36
+ doc.documentElement.style.scrollbarGutter = lock.scrollbarGutter;
37
+ if (doc.body?.style.overflowY === "hidden")
38
+ doc.body.style.overflowY = lock.bodyOverflowY;
39
+ documentScrollLocks.delete(doc);
40
+ }
41
+ function defineDialogPart(tag, styles, namedSlots = []) {
42
+ if (customElements.get(tag))
43
+ return;
44
+ class SpuiDialogPart extends HTMLElement {
45
+ connectedCallback() {
46
+ if (this.shadowRoot)
47
+ return;
48
+ const shadow = this.attachShadow({ mode: "open" });
49
+ const style = document.createElement("style");
50
+ style.textContent = `${base}:host{${styles}}${tag === "spui-dialog-actions" ? ":host([position=start]){justify-content:flex-start}:host([fluid])::slotted(*){flex:1}" : tag === "spui-dialog-title" ? "slot:not([name]){flex:1;min-width:0}slot[name=action]{margin-inline-start:auto}" : ""}`;
51
+ if (tag === "spui-dialog-title") {
52
+ this.setAttribute("role", "heading");
53
+ this.setAttribute("aria-level", "2");
54
+ }
55
+ const slot = document.createElement("slot");
56
+ shadow.append(style, slot, ...namedSlots.map(name => { const named = document.createElement("slot"); named.name = name; return named; }));
57
+ }
58
+ }
59
+ customElements.define(tag, SpuiDialogPart);
60
+ }
61
+ export function defineDialogSurface() { defineDialogPart("spui-dialog-surface", "display:block;min-width:0;color:inherit;background:inherit"); }
62
+ export function defineDialogTitle() { defineDialogPart("spui-dialog-title", "display:flex;align-items:center;gap:12px;margin:0;padding:20px 20px 0;font-size:20px;line-height:1.3;font-weight:650", ["action"]); }
63
+ export function defineDialogContent() { defineDialogPart("spui-dialog-content", "display:block"); }
64
+ export function defineDialogBody() { defineDialogPart("spui-dialog-body", "display:block;padding:16px 20px 20px;line-height:1.5"); }
65
+ export function defineDialogActions() {
66
+ defineDialogPart("spui-dialog-actions", "display:flex;justify-content:flex-end;flex-wrap:wrap;gap:8px;padding:12px 20px 20px;border-top:1px solid var(--spui-ui-stroke,var(--spui-semantic-body-divider,#d1d1d1))");
67
+ }
68
+ export function defineDialogTrigger() {
69
+ const tag = "spui-dialog-trigger";
70
+ if (customElements.get(tag))
71
+ return;
72
+ class SpuiDialogTrigger extends HTMLElement {
73
+ constructor() {
74
+ super();
75
+ this.addEventListener("click", event => this.dispatchEvent(new CustomEvent("spui-dialog-trigger", { bubbles: true, composed: true, detail: { action: this.getAttribute("action") ?? (this.closest('[slot="surface"],spui-dialog-surface') ? "close" : "open"), trigger: event.composedPath().find(node => node instanceof HTMLElement && node.matches("button,a[href],[role=button]")), originalEvent: event } })));
76
+ }
77
+ connectedCallback() {
78
+ if (!this.hasAttribute("slot") && !this.closest('[slot="surface"],spui-dialog-surface'))
79
+ this.slot = "trigger";
80
+ if (!this.shadowRoot) {
81
+ const shadow = this.attachShadow({ mode: "open" });
82
+ const style = document.createElement("style");
83
+ style.textContent = `${base}:host{display:contents}`;
84
+ const slot = document.createElement("slot");
85
+ shadow.append(style, slot);
86
+ }
87
+ }
88
+ }
89
+ customElements.define(tag, SpuiDialogTrigger);
90
+ }
91
+ export function defineDialog() {
92
+ defineDialogSurface();
93
+ defineDialogTitle();
94
+ defineDialogContent();
95
+ defineDialogBody();
96
+ defineDialogActions();
97
+ defineDialogTrigger();
98
+ if (customElements.get("spui-dialog"))
99
+ return;
100
+ class SpuiDialog extends HTMLElement {
101
+ static observedAttributes = ["open", "default-open", "motion", "motion-visible", "motion-exit-complete", "title", "dialog-label", "modal", "modal-type", "inert-trap-focus", "dismissible", "close-label", "aria-label"];
102
+ titleObserver;
103
+ previousFocus;
104
+ fallbackTrapActive = false;
105
+ refocusing = false;
106
+ bodyScrollLockHeld = false;
107
+ focusGuard;
108
+ get open() { return this.hasAttribute("open"); }
109
+ set open(value) { this.toggleAttribute("open", Boolean(value)); }
110
+ connectedCallback() {
111
+ if (!this.shadowRoot)
112
+ this.render();
113
+ if (!this.hasAttribute("open") && this.hasAttribute("default-open"))
114
+ this.open = true;
115
+ this.observeTitle();
116
+ this.syncOpen();
117
+ }
118
+ disconnectedCallback() { this.titleObserver?.disconnect(); this.releaseBodyScrollLock(); }
119
+ attributeChangedCallback() { if (this.isConnected)
120
+ this.syncOpen(); }
121
+ syncOpen() {
122
+ const dialog = this.shadowRoot?.querySelector("dialog");
123
+ if (!dialog)
124
+ return;
125
+ const motionExiting = this.hasAttribute("motion") && this.getAttribute("motion-visible") === "false" && !this.hasAttribute("motion-exit-complete");
126
+ const shouldOpen = this.open || motionExiting;
127
+ this.syncBodyScrollLock(shouldOpen);
128
+ if (shouldOpen && !dialog.open) {
129
+ this.previousFocus ??= document.activeElement instanceof HTMLElement ? document.activeElement : undefined;
130
+ const modalType = this.getAttribute("modal-type");
131
+ const nonModal = modalType === "non-modal" || (modalType == null && this.hasAttribute("modal") && this.getAttribute("modal") === "false");
132
+ if (nonModal || this.getAttribute("inert-trap-focus") === "false" || this.hasAttribute("data-has-custom-backdrop")) {
133
+ dialog.show();
134
+ this.fallbackTrapActive = !nonModal;
135
+ const backdrop = this.shadowRoot?.querySelector(".modal-backdrop");
136
+ if (backdrop)
137
+ backdrop.hidden = !this.fallbackTrapActive;
138
+ if (this.fallbackTrapActive) {
139
+ dialog.setAttribute("aria-modal", "true");
140
+ queueMicrotask(() => (this.focusableElements()[0] ?? dialog).focus());
141
+ this.focusGuard = event => {
142
+ if (this.refocusing || event.composedPath().includes(dialog))
143
+ return;
144
+ this.refocusing = true;
145
+ (this.focusableElements()[0] ?? dialog).focus();
146
+ queueMicrotask(() => { this.refocusing = false; });
147
+ };
148
+ document.addEventListener("focus", this.focusGuard, true);
149
+ }
150
+ }
151
+ else
152
+ dialog.showModal();
153
+ }
154
+ else if (!shouldOpen) {
155
+ if (dialog.open)
156
+ dialog.close();
157
+ const restoreFocus = this.fallbackTrapActive ? this.previousFocus : undefined;
158
+ this.fallbackTrapActive = false;
159
+ const backdrop = this.shadowRoot?.querySelector(".modal-backdrop");
160
+ if (backdrop)
161
+ backdrop.hidden = true;
162
+ if (this.focusGuard)
163
+ document.removeEventListener("focus", this.focusGuard, true);
164
+ this.focusGuard = undefined;
165
+ this.previousFocus = undefined;
166
+ if (restoreFocus)
167
+ window.setTimeout(() => { if (restoreFocus.isConnected)
168
+ restoreFocus.focus(); }, 0);
169
+ }
170
+ dialog.inert = motionExiting;
171
+ if (motionExiting)
172
+ dialog.setAttribute("aria-hidden", "true");
173
+ else
174
+ dialog.removeAttribute("aria-hidden");
175
+ if (this.getAttribute("modal-type") === "alert")
176
+ dialog.setAttribute("role", "alertdialog");
177
+ else
178
+ dialog.removeAttribute("role");
179
+ const title = this.getAttribute("title");
180
+ const fallbackTitle = this.shadowRoot?.querySelector(".fallback-title");
181
+ if (fallbackTitle)
182
+ fallbackTitle.textContent = title ?? this.getAttribute("dialog-label") ?? "Dialog";
183
+ const titleSlot = this.shadowRoot?.querySelector("slot[name=title]");
184
+ if (fallbackTitle && titleSlot)
185
+ fallbackTitle.hidden = titleSlot.assignedNodes().length > 0;
186
+ const close = this.shadowRoot?.querySelector(".close");
187
+ if (close) {
188
+ close.setAttribute("aria-label", this.getAttribute("close-label") ?? "Close");
189
+ close.hidden = this.getAttribute("modal-type") === "alert" || (this.hasAttribute("dismissible") && this.getAttribute("dismissible") === "false");
190
+ }
191
+ if (dialog)
192
+ this.syncAccessibleName(dialog);
193
+ }
194
+ syncAccessibleName(dialog) {
195
+ const explicitLabel = this.getAttribute("aria-label");
196
+ const composedTitle = this.querySelector("[data-spui-dialog-title],spui-dialog-title");
197
+ if (explicitLabel) {
198
+ dialog.setAttribute("aria-label", explicitLabel);
199
+ dialog.removeAttribute("aria-labelledby");
200
+ }
201
+ else if (composedTitle) {
202
+ const titleText = composedTitle.matches("[data-spui-dialog-title]")
203
+ ? [...composedTitle.childNodes].filter(node => !(node instanceof Element && node.hasAttribute("data-spui-dialog-title-action"))).map(node => node.textContent ?? "").join("").trim()
204
+ : composedTitle.shadowRoot?.querySelector("slot:not([name])")?.assignedNodes({ flatten: true }).map(node => node.textContent ?? "").join("").trim();
205
+ dialog.setAttribute("aria-label", titleText || this.getAttribute("title") || "Dialog");
206
+ dialog.removeAttribute("aria-labelledby");
207
+ }
208
+ else {
209
+ dialog.removeAttribute("aria-label");
210
+ dialog.setAttribute("aria-labelledby", "title");
211
+ }
212
+ }
213
+ emitOpenChange(data) {
214
+ this.dispatchEvent(new CustomEvent("spui-open-change", { bubbles: true, composed: true, detail: data }));
215
+ }
216
+ focusableElements() {
217
+ const selector = 'button,a[href],input,select,textarea,[tabindex]';
218
+ const controls = [];
219
+ const seen = new Set();
220
+ const visit = (root) => {
221
+ for (const element of root.querySelectorAll("*")) {
222
+ if (element.closest('[slot="trigger"]'))
223
+ continue;
224
+ if (element.matches(selector) && !element.matches(":disabled") && element.tabIndex >= 0 && element.getClientRects().length > 0 && !seen.has(element)) {
225
+ controls.push(element);
226
+ seen.add(element);
227
+ }
228
+ if (element.shadowRoot)
229
+ visit(element.shadowRoot);
230
+ if (element instanceof HTMLSlotElement)
231
+ for (const assigned of element.assignedElements({ flatten: true }))
232
+ visit(assigned);
233
+ }
234
+ };
235
+ visit(this);
236
+ return controls;
237
+ }
238
+ closeDialog(type, event, trigger) {
239
+ if (!this.open)
240
+ return;
241
+ if (this.hasAttribute("motion")) {
242
+ if (this.getAttribute("motion-visible") === "false")
243
+ return;
244
+ this.emitOpenChange({ open: false, type, event, trigger });
245
+ return;
246
+ }
247
+ this.removeAttribute("open");
248
+ const dialog = this.shadowRoot?.querySelector("dialog");
249
+ if (dialog?.open)
250
+ dialog.close();
251
+ this.emitOpenChange({ open: false, type, event, trigger });
252
+ }
253
+ isDismissible() { return !(this.hasAttribute("dismissible") && this.getAttribute("dismissible") === "false"); }
254
+ isModal() {
255
+ const modalType = this.getAttribute("modal-type");
256
+ return modalType === "modal" || (modalType == null && this.getAttribute("modal") !== "false");
257
+ }
258
+ syncBodyScrollLock(shouldOpen) {
259
+ const modalType = this.getAttribute("modal-type");
260
+ const shouldLock = shouldOpen && modalType !== "non-modal" && (modalType != null || this.getAttribute("modal") !== "false");
261
+ if (shouldLock && !this.bodyScrollLockHeld)
262
+ this.bodyScrollLockHeld = acquireDocumentScrollLock(this.ownerDocument);
263
+ else if (!shouldLock)
264
+ this.releaseBodyScrollLock();
265
+ }
266
+ releaseBodyScrollLock() {
267
+ if (!this.bodyScrollLockHeld)
268
+ return;
269
+ this.bodyScrollLockHeld = false;
270
+ releaseDocumentScrollLock(this.ownerDocument);
271
+ }
272
+ render() {
273
+ const shadow = this.attachShadow({ mode: "open" });
274
+ const dialog = document.createElement("dialog");
275
+ dialog.id = `spui-dialog-${++dialogId}`;
276
+ dialog.part.add("surface");
277
+ dialog.setAttribute("aria-labelledby", "title");
278
+ const header = document.createElement("header");
279
+ const heading = document.createElement("h2");
280
+ heading.id = "title";
281
+ const fallbackTitle = document.createElement("span");
282
+ fallbackTitle.className = "fallback-title";
283
+ fallbackTitle.textContent = this.getAttribute("title") ?? this.getAttribute("dialog-label") ?? "Dialog";
284
+ const titleSlot = document.createElement("slot");
285
+ titleSlot.name = "title";
286
+ titleSlot.addEventListener("slotchange", () => { fallbackTitle.hidden = titleSlot.assignedNodes().length > 0; });
287
+ heading.append(fallbackTitle, titleSlot);
288
+ const close = document.createElement("button");
289
+ close.type = "button";
290
+ close.className = "close";
291
+ close.part.add("close-button");
292
+ const closeMark = document.createElement("span");
293
+ closeMark.setAttribute("aria-hidden", "true");
294
+ closeMark.textContent = "×";
295
+ close.append(closeMark);
296
+ close.addEventListener("click", event => this.closeDialog("triggerClick", event));
297
+ header.append(heading, close);
298
+ const body = document.createElement("div");
299
+ body.className = "body";
300
+ body.part.add("body");
301
+ body.append(document.createElement("slot"));
302
+ const footer = document.createElement("footer");
303
+ footer.part.add("actions");
304
+ footer.hidden = true;
305
+ const actions = document.createElement("slot");
306
+ actions.name = "actions";
307
+ actions.addEventListener("slotchange", () => { footer.hidden = actions.assignedElements().length === 0; });
308
+ footer.append(actions);
309
+ dialog.append(header, body, footer);
310
+ const surface = document.createElement("slot");
311
+ surface.name = "surface";
312
+ surface.addEventListener("slotchange", () => {
313
+ const composed = surface.assignedElements().length > 0;
314
+ header.hidden = false;
315
+ heading.hidden = composed;
316
+ body.hidden = composed;
317
+ footer.hidden = composed;
318
+ this.syncAccessibleName(dialog);
319
+ if (this.fallbackTrapActive)
320
+ requestAnimationFrame(() => (this.focusableElements()[0] ?? dialog).focus());
321
+ });
322
+ this.titleObserver = new MutationObserver(() => this.syncAccessibleName(dialog));
323
+ dialog.insertBefore(surface, body);
324
+ dialog.addEventListener("cancel", event => event.preventDefault());
325
+ dialog.addEventListener("keydown", event => {
326
+ if (event.key === "Escape") {
327
+ if (event.defaultPrevented)
328
+ return;
329
+ window.setTimeout(() => {
330
+ if (event.defaultPrevented || !this.open)
331
+ return;
332
+ event.preventDefault();
333
+ if (this.isDismissible())
334
+ this.closeDialog("escapeKeyDown", event);
335
+ }, 0);
336
+ return;
337
+ }
338
+ if (!this.fallbackTrapActive)
339
+ return;
340
+ if (event.key !== "Tab")
341
+ return;
342
+ const controls = this.focusableElements();
343
+ if (!controls.length) {
344
+ event.preventDefault();
345
+ dialog.focus();
346
+ return;
347
+ }
348
+ const target = event.composedPath()[0];
349
+ const index = controls.indexOf(target instanceof HTMLElement ? target : document.activeElement);
350
+ if (event.shiftKey && index <= 0) {
351
+ event.preventDefault();
352
+ controls.at(-1)?.focus();
353
+ }
354
+ else if (!event.shiftKey && index === controls.length - 1) {
355
+ event.preventDefault();
356
+ controls[0]?.focus();
357
+ }
358
+ });
359
+ dialog.addEventListener("click", event => {
360
+ if (event.target !== dialog || !this.isModal() || !this.isDismissible())
361
+ return;
362
+ const rect = dialog.getBoundingClientRect();
363
+ if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom)
364
+ this.closeDialog("backdropClick", event);
365
+ });
366
+ const style = document.createElement("style");
367
+ style.textContent = `${base}:host{display:contents}dialog{position:relative;z-index:2147483647;box-sizing:border-box;width:min(520px,calc(100vw - 32px));max-width:calc(100vw - 32px);max-height:calc(100dvh - 32px);overflow:auto;padding:0;border:1px solid var(--spui-ui-stroke,var(--spui-semantic-body-divider,#d1d1d1));border-radius:var(--spui-card-radius,12px);background:var(--spui-card-background,var(--spui-ui-surface,#fff));box-shadow:var(--spui-card-shadow,0 16px 48px #0004);color:var(--spui-ui-text,var(--spui-semantic-body-text,#242424))}dialog::backdrop{background:color-mix(in srgb,var(--spui-ui-text,#242424) 42%,transparent);backdrop-filter:blur(2px)}:host([backdrop-appearance=transparent]) dialog::backdrop{background:transparent;backdrop-filter:none}header{display:flex;align-items:start;justify-content:space-between;gap:16px;padding:20px 20px 0}h2{margin:0;font-size:20px;line-height:1.3;font-weight:650}.close{display:inline-grid;place-items:center;flex:none;width:var(--spui-control-height,32px);height:var(--spui-control-height,32px);margin:-5px -6px 0 0;margin-left:auto;border:0;border-radius:var(--spui-control-radius,6px);background:transparent;color:inherit;font-size:24px;line-height:1;cursor:pointer}.close:hover{background:var(--spui-ui-surface-hover,#f5f5f5)}.close:focus-visible{outline:2px solid var(--spui-focus-ring,#005a9e);outline-offset:2px}.body{padding:16px 20px 20px;line-height:1.5}footer{display:flex;justify-content:flex-end;flex-wrap:wrap;gap:8px;padding:12px 20px 20px;border-top:1px solid var(--spui-ui-stroke,var(--spui-semantic-body-divider,#d1d1d1))}footer[hidden]{display:none}@media(forced-colors:active){dialog{border-color:CanvasText;box-shadow:none}dialog::backdrop{background:Canvas} .close:focus-visible{outline-color:Highlight}}`;
368
+ const overlayStyle = document.createElement("style");
369
+ overlayStyle.textContent = ".modal-backdrop{position:fixed;inset:0;z-index:2147483000;background:color-mix(in srgb,var(--spui-ui-text,#242424) 42%,transparent);backdrop-filter:blur(2px)}.modal-backdrop[hidden]{display:none}.modal-backdrop slot{position:absolute;inset:0}:host([data-has-custom-backdrop]) .modal-backdrop{background:transparent;backdrop-filter:none}:host([backdrop-appearance=transparent]) .modal-backdrop{background:transparent;backdrop-filter:none}::slotted([slot=backdrop]){position:absolute;inset:0;display:block;background:color-mix(in srgb,var(--spui-ui-text,#242424) 42%,transparent)}::slotted([slot=backdrop][data-appearance=transparent]){background:transparent}";
370
+ const backdrop = document.createElement("div");
371
+ backdrop.className = "modal-backdrop";
372
+ backdrop.part.add("backdrop");
373
+ backdrop.hidden = true;
374
+ backdrop.setAttribute("aria-hidden", "true");
375
+ const backdropSlot = document.createElement("slot");
376
+ backdropSlot.name = "backdrop";
377
+ backdrop.append(backdropSlot);
378
+ backdrop.addEventListener("click", event => { if (this.isModal() && this.isDismissible())
379
+ this.closeDialog("backdropClick", event); });
380
+ const triggerSlot = document.createElement("slot");
381
+ triggerSlot.name = "trigger";
382
+ triggerSlot.addEventListener("slotchange", () => {
383
+ for (const trigger of triggerSlot.assignedElements()) {
384
+ const innerSlot = trigger.shadowRoot?.querySelector("slot");
385
+ for (const target of innerSlot?.assignedElements({ flatten: true }) ?? []) {
386
+ if (!trigger.hasAttribute("disable-button-enhancement") && !target.hasAttribute("aria-haspopup"))
387
+ target.setAttribute("aria-haspopup", "dialog");
388
+ }
389
+ }
390
+ });
391
+ shadow.append(style, overlayStyle, triggerSlot, backdrop, dialog);
392
+ this.addEventListener("spui-dialog-trigger", event => {
393
+ const detail = event.detail;
394
+ if (detail?.action === "close")
395
+ this.closeDialog("triggerClick", detail.originalEvent, detail.trigger);
396
+ else if (!this.open) {
397
+ this.previousFocus = detail?.trigger ?? this.querySelector('spui-dialog-trigger[slot="trigger"] button,spui-dialog-trigger[slot="trigger"] a[href],[slot="trigger"] [role="button"]') ?? undefined;
398
+ this.open = true;
399
+ if (detail?.originalEvent)
400
+ this.emitOpenChange({ open: true, type: "triggerClick", event: detail.originalEvent, trigger: detail.trigger });
401
+ }
402
+ });
403
+ }
404
+ observeTitle() {
405
+ this.titleObserver?.observe(this, { subtree: true, childList: true, characterData: true });
406
+ }
407
+ }
408
+ customElements.define("spui-dialog", SpuiDialog);
409
+ }
@@ -0,0 +1,2 @@
1
+ export type DividerAppearance = "brand" | "default" | "strong" | "subtle";
2
+ export declare function defineDivider(): void;
@@ -0,0 +1,66 @@
1
+ import { base } from "./shared.js";
2
+ export function defineDivider() {
3
+ if (customElements.get("spui-divider"))
4
+ return;
5
+ class SpuiDivider extends HTMLElement {
6
+ static observedAttributes = ["orientation", "vertical", "align", "align-content", "appearance", "inset"];
7
+ generatedAriaLabel;
8
+ connectedCallback() {
9
+ if (!this.shadowRoot)
10
+ this.render();
11
+ this.sync();
12
+ }
13
+ attributeChangedCallback() { if (this.isConnected)
14
+ this.sync(); }
15
+ sync() {
16
+ const orientation = this.hasAttribute("vertical") || this.getAttribute("orientation") === "vertical" ? "vertical" : "horizontal";
17
+ this.dataset.orientation = orientation;
18
+ const wrapper = this.shadowRoot?.querySelector("slot[name=wrapper]");
19
+ const slot = this.shadowRoot?.querySelector("slot:not([name])");
20
+ const hasLabel = Boolean(wrapper?.assignedNodes().length
21
+ ? wrapper.assignedNodes().some(node => node.nodeType === Node.ELEMENT_NODE || Boolean(node.textContent?.trim()))
22
+ : slot?.assignedNodes().some(node => node.nodeType === Node.ELEMENT_NODE || Boolean(node.textContent?.trim())));
23
+ this.dataset.hasLabel = String(hasLabel);
24
+ this.dataset.alignContent = this.getAttribute("align-content") ?? this.getAttribute("align") ?? "center";
25
+ this.dataset.dividerAppearance = this.getAttribute("appearance") ?? "default";
26
+ this.setAttribute("role", "separator");
27
+ this.setAttribute("aria-orientation", orientation);
28
+ if (hasLabel && !this.hasAttribute("aria-labelledby")) {
29
+ const label = this.textContent?.trim();
30
+ if (label && (this.getAttribute("aria-label") === null || this.getAttribute("aria-label") === this.generatedAriaLabel)) {
31
+ this.generatedAriaLabel = label;
32
+ if (this.getAttribute("aria-label") !== label)
33
+ this.setAttribute("aria-label", label);
34
+ }
35
+ }
36
+ else if (this.generatedAriaLabel !== undefined && this.getAttribute("aria-label") === this.generatedAriaLabel) {
37
+ this.removeAttribute("aria-label");
38
+ this.generatedAriaLabel = undefined;
39
+ }
40
+ }
41
+ render() {
42
+ const shadow = this.attachShadow({ mode: "open" });
43
+ const before = document.createElement("span");
44
+ before.className = "line";
45
+ before.part.add("line");
46
+ before.setAttribute("aria-hidden", "true");
47
+ const after = document.createElement("span");
48
+ after.className = "line";
49
+ after.part.add("line");
50
+ after.setAttribute("aria-hidden", "true");
51
+ const wrapper = document.createElement("div");
52
+ wrapper.className = "wrapper";
53
+ const wrapperSlot = document.createElement("slot");
54
+ wrapperSlot.name = "wrapper";
55
+ wrapperSlot.addEventListener("slotchange", () => this.sync());
56
+ const slot = document.createElement("slot");
57
+ slot.addEventListener("slotchange", () => this.sync());
58
+ wrapperSlot.append(slot);
59
+ wrapper.append(wrapperSlot);
60
+ const style = document.createElement("style");
61
+ style.textContent = `${base}:host{display:flex;align-items:center;gap:var(--spui-divider-gap,12px);min-width:0;margin-block:var(--spui-divider-margin-block,12px);color:var(--spui-ui-muted,var(--spui-semantic-body-subtext,#616161));font:inherit}:host([data-orientation=vertical]){display:inline-flex;flex-direction:column;align-self:stretch;min-height:1em;width:1px;margin-inline:var(--spui-divider-margin-inline,12px);margin-block:0}:host([data-has-label=false]) .wrapper{display:none}:host([data-has-label=false]) .line:last-of-type{display:none}:host([data-has-label=true]) .wrapper{font-size:.875em;line-height:1.25;text-align:center}.wrapper{min-width:0}.line{flex:1;min-height:1px;background:var(--spui-divider-color,var(--spui-ui-stroke,var(--spui-semantic-body-divider,#d1d1d1)))}:host([data-orientation=vertical]) .line{min-height:0;width:1px;height:auto;min-width:1px;background:var(--spui-divider-color,var(--spui-ui-stroke,var(--spui-semantic-body-divider,#d1d1d1)))}:host([data-divider-appearance=brand]) .line{background:var(--spui-divider-color,var(--spui-base-accent,var(--spui-ui-accent,#0078d4)))}:host([data-divider-appearance=strong]) .line{background:var(--spui-divider-color,color-mix(in srgb,var(--spui-ui-text,#242424) 44%,var(--spui-ui-stroke,#d1d1d1)))}:host([data-divider-appearance=subtle]) .line{background:var(--spui-divider-color,color-mix(in srgb,var(--spui-ui-stroke,#d1d1d1) 56%,transparent))}:host([data-align-content=start]) .line:first-of-type{flex:0}:host([data-align-content=end]) .line:last-of-type{flex:0}:host([data-align-content=center]) .line{flex:1}:host([inset]){margin-inline-start:var(--spui-divider-inset,16px)}@media(forced-colors:active){.line{background:CanvasText}}`;
62
+ shadow.append(style, before, wrapper, after);
63
+ }
64
+ }
65
+ customElements.define("spui-divider", SpuiDivider);
66
+ }
@@ -0,0 +1,3 @@
1
+ export declare function defineDrawer(): void;
2
+ /** Non-modal, in-flow companion surface for navigation and supporting work. */
3
+ export declare function defineInlineDrawer(): void;