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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/AGENTS.md +211 -0
  2. package/LICENSE +21 -0
  3. package/README.md +897 -2
  4. package/components.json +5811 -0
  5. package/dist/appearance.d.ts +6 -0
  6. package/dist/appearance.js +217 -0
  7. package/dist/elements/accordion.d.ts +9 -0
  8. package/dist/elements/accordion.js +338 -0
  9. package/dist/elements/alert.d.ts +2 -0
  10. package/dist/elements/alert.js +62 -0
  11. package/dist/elements/avatar-group-primitives.d.ts +3 -0
  12. package/dist/elements/avatar-group-primitives.js +189 -0
  13. package/dist/elements/avatar-group.d.ts +2 -0
  14. package/dist/elements/avatar-group.js +110 -0
  15. package/dist/elements/avatar.d.ts +4 -0
  16. package/dist/elements/avatar.js +251 -0
  17. package/dist/elements/badge.d.ts +8 -0
  18. package/dist/elements/badge.js +182 -0
  19. package/dist/elements/breadcrumb.d.ts +4 -0
  20. package/dist/elements/breadcrumb.js +233 -0
  21. package/dist/elements/button-group.d.ts +1 -0
  22. package/dist/elements/button-group.js +20 -0
  23. package/dist/elements/button-variants.d.ts +6 -0
  24. package/dist/elements/button-variants.js +339 -0
  25. package/dist/elements/button.d.ts +5 -0
  26. package/dist/elements/button.js +233 -0
  27. package/dist/elements/card-primitives.d.ts +3 -0
  28. package/dist/elements/card-primitives.js +83 -0
  29. package/dist/elements/card.d.ts +5 -0
  30. package/dist/elements/card.js +213 -0
  31. package/dist/elements/carousel.d.ts +92 -0
  32. package/dist/elements/carousel.js +1112 -0
  33. package/dist/elements/color-picker.d.ts +24 -0
  34. package/dist/elements/color-picker.js +1004 -0
  35. package/dist/elements/combobox.d.ts +1 -0
  36. package/dist/elements/combobox.js +1012 -0
  37. package/dist/elements/data-grid-model.d.ts +117 -0
  38. package/dist/elements/data-grid-model.js +294 -0
  39. package/dist/elements/data-grid.d.ts +29 -0
  40. package/dist/elements/data-grid.js +1567 -0
  41. package/dist/elements/date-picker.d.ts +1 -0
  42. package/dist/elements/date-picker.js +486 -0
  43. package/dist/elements/dialog.d.ts +23 -0
  44. package/dist/elements/dialog.js +409 -0
  45. package/dist/elements/divider.d.ts +2 -0
  46. package/dist/elements/divider.js +66 -0
  47. package/dist/elements/drawer.d.ts +3 -0
  48. package/dist/elements/drawer.js +308 -0
  49. package/dist/elements/field-composition.d.ts +26 -0
  50. package/dist/elements/field-composition.js +133 -0
  51. package/dist/elements/field.d.ts +1 -0
  52. package/dist/elements/field.js +67 -0
  53. package/dist/elements/file-upload.d.ts +6 -0
  54. package/dist/elements/file-upload.js +211 -0
  55. package/dist/elements/floating-position.d.ts +26 -0
  56. package/dist/elements/floating-position.js +39 -0
  57. package/dist/elements/image.d.ts +28 -0
  58. package/dist/elements/image.js +38 -0
  59. package/dist/elements/info.d.ts +9 -0
  60. package/dist/elements/info.js +102 -0
  61. package/dist/elements/input.d.ts +5 -0
  62. package/dist/elements/input.js +255 -0
  63. package/dist/elements/label.d.ts +15 -0
  64. package/dist/elements/label.js +56 -0
  65. package/dist/elements/link.d.ts +1 -0
  66. package/dist/elements/link.js +92 -0
  67. package/dist/elements/list.d.ts +19 -0
  68. package/dist/elements/list.js +297 -0
  69. package/dist/elements/menu.d.ts +33 -0
  70. package/dist/elements/menu.js +1103 -0
  71. package/dist/elements/message-bar.d.ts +10 -0
  72. package/dist/elements/message-bar.js +299 -0
  73. package/dist/elements/nav.d.ts +32 -0
  74. package/dist/elements/nav.js +791 -0
  75. package/dist/elements/overflow.d.ts +9 -0
  76. package/dist/elements/overflow.js +218 -0
  77. package/dist/elements/persona.d.ts +3 -0
  78. package/dist/elements/persona.js +125 -0
  79. package/dist/elements/popover.d.ts +77 -0
  80. package/dist/elements/popover.js +840 -0
  81. package/dist/elements/progress-bar.d.ts +4 -0
  82. package/dist/elements/progress-bar.js +82 -0
  83. package/dist/elements/radio-group.d.ts +1 -0
  84. package/dist/elements/radio-group.js +303 -0
  85. package/dist/elements/rating-display.d.ts +2 -0
  86. package/dist/elements/rating-display.js +125 -0
  87. package/dist/elements/rating.d.ts +2 -0
  88. package/dist/elements/rating.js +206 -0
  89. package/dist/elements/rich-text-editor.d.ts +18 -0
  90. package/dist/elements/rich-text-editor.js +508 -0
  91. package/dist/elements/search.d.ts +3 -0
  92. package/dist/elements/search.js +207 -0
  93. package/dist/elements/select.d.ts +6 -0
  94. package/dist/elements/select.js +291 -0
  95. package/dist/elements/shared.d.ts +19 -0
  96. package/dist/elements/shared.js +396 -0
  97. package/dist/elements/skeleton.d.ts +6 -0
  98. package/dist/elements/skeleton.js +91 -0
  99. package/dist/elements/slider.d.ts +1 -0
  100. package/dist/elements/slider.js +248 -0
  101. package/dist/elements/spin-button.d.ts +7 -0
  102. package/dist/elements/spin-button.js +499 -0
  103. package/dist/elements/spinner.d.ts +4 -0
  104. package/dist/elements/spinner.js +124 -0
  105. package/dist/elements/swatch-picker.d.ts +5 -0
  106. package/dist/elements/swatch-picker.js +363 -0
  107. package/dist/elements/table.d.ts +21 -0
  108. package/dist/elements/table.js +261 -0
  109. package/dist/elements/tabs.d.ts +1 -0
  110. package/dist/elements/tabs.js +276 -0
  111. package/dist/elements/tag-picker.d.ts +29 -0
  112. package/dist/elements/tag-picker.js +697 -0
  113. package/dist/elements/tags.d.ts +21 -0
  114. package/dist/elements/tags.js +421 -0
  115. package/dist/elements/teaching-popover.d.ts +32 -0
  116. package/dist/elements/teaching-popover.js +412 -0
  117. package/dist/elements/text.d.ts +9 -0
  118. package/dist/elements/text.js +75 -0
  119. package/dist/elements/textarea.d.ts +1 -0
  120. package/dist/elements/textarea.js +182 -0
  121. package/dist/elements/toast.d.ts +25 -0
  122. package/dist/elements/toast.js +337 -0
  123. package/dist/elements/toaster.d.ts +6 -0
  124. package/dist/elements/toaster.js +356 -0
  125. package/dist/elements/toggle.d.ts +2 -0
  126. package/dist/elements/toggle.js +246 -0
  127. package/dist/elements/toolbar.d.ts +12 -0
  128. package/dist/elements/toolbar.js +381 -0
  129. package/dist/elements/tooltip.d.ts +76 -0
  130. package/dist/elements/tooltip.js +510 -0
  131. package/dist/elements/tree-model.d.ts +19 -0
  132. package/dist/elements/tree-model.js +34 -0
  133. package/dist/elements/tree.d.ts +5 -0
  134. package/dist/elements/tree.js +273 -0
  135. package/dist/elements/view-card.d.ts +2 -0
  136. package/dist/elements/view-card.js +78 -0
  137. package/dist/elements/virtual-list.d.ts +41 -0
  138. package/dist/elements/virtual-list.js +369 -0
  139. package/dist/elements.d.ts +72 -0
  140. package/dist/elements.js +207 -0
  141. package/dist/index.d.ts +76 -0
  142. package/dist/index.js +66 -0
  143. package/dist/react.d.ts +4546 -0
  144. package/dist/react.js +6837 -0
  145. package/dist/theme.d.ts +169 -0
  146. package/dist/theme.js +167 -0
  147. package/dist/virtualizer-hooks.d.ts +80 -0
  148. package/dist/virtualizer-hooks.js +253 -0
  149. package/package.json +87 -4
  150. package/scripts/generate-token-index.mjs +43 -0
  151. package/scripts/sync-component-catalog-signatures.mjs +55 -0
  152. package/scripts/validate-component-catalog.mjs +44 -0
  153. package/skills/sharepoint-ui/SKILL.md +12 -0
  154. package/skills/sharepoint-ui/references/choosing-components.md +20 -0
  155. package/skills/sharepoint-ui/references/sharepoint-patterns.md +17 -0
  156. package/tokens.json +3777 -0
@@ -0,0 +1,6 @@
1
+ export type SharePointUIAppearance = "fluent" | "ios" | "windows" | "linux" | "glass";
2
+ export type SharePointUIDensity = "compact" | "comfortable" | "touch";
3
+ export type SharePointUIStyleTokens = Record<`--spui-${string}`, string | number>;
4
+ export declare const DEFAULT_ACCENT = "#78716c";
5
+ export type SharePointUIBaseAppearance = Exclude<SharePointUIAppearance, "glass">;
6
+ export declare function getSharePointUIStyleTokens(appearance?: SharePointUIAppearance, overrides?: Partial<SharePointUIStyleTokens>, baseAppearance?: SharePointUIBaseAppearance, density?: SharePointUIDensity): SharePointUIStyleTokens;
@@ -0,0 +1,217 @@
1
+ export const DEFAULT_ACCENT = "#78716c";
2
+ const presets = {
3
+ fluent: { "--spui-font-family": '"Segoe UI", sans-serif', "--spui-control-radius": "4px", "--spui-control-height": "32px", "--spui-card-radius": "8px", "--spui-base-card-padding": "16px", "--spui-card-padding": "16px", "--spui-base-control-gap": "8px", "--spui-control-gap": "8px", "--spui-control-padding-inline": "14px", "--spui-ui-text": "#242424", "--spui-ui-muted": "#616161", "--spui-ui-canvas": "#f5f5f5", "--spui-ui-surface": "#ffffff", "--spui-ui-surface-hover": "#f5f5f5", "--spui-ui-stroke": "#d1d1d1", "--spui-ui-accent": DEFAULT_ACCENT, "--spui-ui-accent-hover": "color-mix(in srgb, var(--spui-ui-accent) 84%, black)", "--spui-ui-accent-text": "#ffffff", "--spui-ui-success": "#107c10", "--spui-ui-success-surface": "#dff6dd", "--spui-ui-warning": "#835b00", "--spui-ui-warning-surface": "#fff4ce", "--spui-ui-danger": "#b10e1c", "--spui-ui-danger-surface": "#fde7e9", "--spui-focus-ring": "var(--spui-ui-accent)", "--spui-card-shadow": "0 1px 3px #0001", "--spui-color-scheme": "light" },
4
+ ios: { "--spui-font-family": '-apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif', "--spui-control-radius": "11px", "--spui-control-height": "44px", "--spui-card-radius": "18px", "--spui-base-card-padding": "20px", "--spui-card-padding": "20px", "--spui-base-control-gap": "10px", "--spui-control-gap": "10px", "--spui-control-padding-inline": "16px", "--spui-ui-text": "light-dark(#1c1c1e, #f2f2f7)", "--spui-ui-muted": "light-dark(#636366, #aeaeb2)", "--spui-ui-canvas": "light-dark(#f2f2f7, #000000)", "--spui-ui-surface": "light-dark(#ffffff, #1c1c1e)", "--spui-ui-surface-hover": "light-dark(#e5e5ea, #2c2c2e)", "--spui-ui-stroke": "light-dark(#c6c6c8, #38383a)", "--spui-ui-accent": DEFAULT_ACCENT, "--spui-ui-accent-hover": "color-mix(in srgb, var(--spui-ui-accent) 84%, black)", "--spui-ui-accent-text": "#ffffff", "--spui-ui-success": "light-dark(#248a3d, #30d158)", "--spui-ui-success-surface": "light-dark(#e4f5e9, #14321c)", "--spui-ui-warning": "light-dark(#9a6700, #ffd60a)", "--spui-ui-warning-surface": "light-dark(#fff3d6, #342900)", "--spui-ui-danger": "light-dark(#d70015, #ff453a)", "--spui-ui-danger-surface": "light-dark(#ffe5e8, #3a1114)", "--spui-focus-ring": "var(--spui-ui-accent)", "--spui-card-shadow": "light-dark(0 2px 10px #1c1c1e12, 0 2px 10px #0008)", "--spui-control-shadow": "light-dark(0 1px 2px #1c1c1e0d, none)", "--spui-color-scheme": "light dark" },
5
+ windows: { "--spui-font-family": '"Segoe UI Variable", "Segoe UI", sans-serif', "--spui-control-radius": "6px", "--spui-control-height": "32px", "--spui-card-radius": "8px", "--spui-base-card-padding": "16px", "--spui-card-padding": "16px", "--spui-base-control-gap": "8px", "--spui-control-gap": "8px", "--spui-ui-text": "#1a1a1a", "--spui-ui-muted": "#5d5d5d", "--spui-ui-canvas": "#f3f3f3", "--spui-ui-surface": "#ffffff", "--spui-ui-surface-hover": "#f5f5f5", "--spui-ui-stroke": "#d1d1d1", "--spui-ui-accent": DEFAULT_ACCENT, "--spui-ui-accent-hover": "color-mix(in srgb, var(--spui-ui-accent) 84%, black)", "--spui-ui-accent-text": "#ffffff", "--spui-ui-success": "#0f7b0f", "--spui-ui-success-surface": "#dff6dd", "--spui-ui-warning": "#835b00", "--spui-ui-warning-surface": "#fff4ce", "--spui-ui-danger": "#c42b1c", "--spui-ui-danger-surface": "#fde7e9", "--spui-focus-ring": "var(--spui-ui-accent)", "--spui-card-shadow": "0 2px 6px #00000012", "--spui-color-scheme": "light" },
6
+ linux: { "--spui-font-family": "system-ui, Ubuntu, Cantarell, sans-serif", "--spui-control-radius": "6px", "--spui-control-height": "36px", "--spui-card-radius": "10px", "--spui-base-card-padding": "18px", "--spui-card-padding": "18px", "--spui-base-control-gap": "8px", "--spui-control-gap": "8px", "--spui-ui-text": "#241f31", "--spui-ui-muted": "#5e5c64", "--spui-ui-canvas": "#f6f5f4", "--spui-ui-surface": "#ffffff", "--spui-ui-surface-hover": "#f1f0ef", "--spui-ui-stroke": "#c7c5c3", "--spui-ui-accent": DEFAULT_ACCENT, "--spui-ui-accent-hover": "color-mix(in srgb, var(--spui-ui-accent) 84%, black)", "--spui-ui-accent-text": "#ffffff", "--spui-ui-success": "#2ec27e", "--spui-ui-success-surface": "#e4f5ec", "--spui-ui-warning": "#9c6e03", "--spui-ui-warning-surface": "#fff3d6", "--spui-ui-danger": "#e01b24", "--spui-ui-danger-surface": "#ffe5e8", "--spui-focus-ring": "var(--spui-ui-accent)", "--spui-card-shadow": "0 2px 8px #00000012", "--spui-color-scheme": "light" },
7
+ glass: {}
8
+ };
9
+ function scalePresetLength(value, scale) {
10
+ if (typeof value !== "string")
11
+ return String(value ?? "0px");
12
+ const pixels = /^(-?\d*\.?\d+)px$/.exec(value.trim());
13
+ // ponytail: presets use px lengths; keep custom units intact instead of emitting unsupported CSS math.
14
+ return pixels ? `${Number(pixels[1]) * scale}px` : value;
15
+ }
16
+ export function getSharePointUIStyleTokens(appearance = "fluent", overrides, baseAppearance = "fluent", density = "comfortable") {
17
+ const selectedBase = appearance === "glass" ? baseAppearance : appearance;
18
+ const tokens = { ...presets[selectedBase] };
19
+ Object.assign(tokens, { "--spui-motion-duration-fast": "120ms", "--spui-motion-duration-normal": "150ms", "--spui-motion-easing-standard": "cubic-bezier(.2,0,0,1)" });
20
+ if (selectedBase === "fluent")
21
+ Object.assign(tokens, {
22
+ "--spui-ui-text": "var(--spui-semantic-body-text, var(--colorNeutralForeground1, #242424))",
23
+ "--spui-ui-muted": "var(--spui-semantic-body-subtext, var(--colorNeutralForeground2, #616161))",
24
+ "--spui-ui-canvas": "var(--spui-semantic-body-background, var(--colorNeutralBackground2, #f5f5f5))",
25
+ "--spui-ui-surface": "var(--spui-semantic-card-background, var(--spui-semantic-body-background, var(--colorNeutralBackground1, #ffffff)))",
26
+ "--spui-ui-surface-hover": "var(--spui-semantic-button-background-hovered, var(--colorNeutralBackground1Hover, #f5f5f5))",
27
+ "--spui-ui-stroke": "var(--spui-semantic-body-divider, var(--colorNeutralStroke2, #d1d1d1))",
28
+ "--spui-ui-accent": `var(--spui-semantic-primary-button-background, var(--colorBrandBackground, ${DEFAULT_ACCENT}))`,
29
+ "--spui-ui-accent-hover": "var(--spui-semantic-primary-button-background-hovered, color-mix(in srgb, var(--spui-ui-accent) 84%, black))",
30
+ "--spui-ui-accent-text": "var(--spui-semantic-primary-button-text, var(--colorNeutralForegroundOnBrand, #ffffff))",
31
+ "--spui-ui-success": "var(--spui-semantic-success-text, #107c10)",
32
+ "--spui-ui-success-surface": "var(--spui-semantic-success-background, #dff6dd)",
33
+ "--spui-ui-warning": "var(--spui-semantic-warning-text, #835b00)",
34
+ "--spui-ui-warning-surface": "var(--spui-semantic-warning-background, #fff4ce)",
35
+ "--spui-ui-danger": "var(--spui-semantic-error-text, #b10e1c)",
36
+ "--spui-ui-danger-surface": "var(--spui-semantic-error-background, #fde7e9)",
37
+ "--spui-focus-ring": "var(--spui-ui-border-focus)"
38
+ });
39
+ else {
40
+ const presetText = String(tokens["--spui-ui-text"]);
41
+ const presetMuted = String(tokens["--spui-ui-muted"]);
42
+ const presetCanvas = String(tokens["--spui-ui-canvas"]);
43
+ const presetSurface = String(tokens["--spui-ui-surface"]);
44
+ const presetStroke = String(tokens["--spui-ui-stroke"]);
45
+ const presetAccent = String(tokens["--spui-ui-accent"]);
46
+ const presetAccentHover = String(tokens["--spui-ui-accent-hover"]);
47
+ const presetAccentText = String(tokens["--spui-ui-accent-text"]);
48
+ Object.assign(tokens, {
49
+ "--spui-ui-text": `var(--spui-semantic-body-text, var(--colorNeutralForeground1, ${presetText}))`,
50
+ "--spui-ui-muted": `var(--spui-semantic-body-subtext, var(--colorNeutralForeground2, ${presetMuted}))`,
51
+ "--spui-ui-canvas": `color-mix(in srgb, var(--spui-semantic-body-background, ${presetCanvas}) 90%, var(--spui-semantic-primary-button-background, ${presetAccent}) 10%)`,
52
+ "--spui-ui-surface": `var(--spui-semantic-card-background, var(--spui-semantic-body-background, var(--colorNeutralBackground1, ${presetSurface})))`,
53
+ "--spui-ui-surface-hover": `color-mix(in srgb, var(--spui-ui-surface, ${presetSurface}) 90%, var(--spui-ui-accent, ${presetAccent}) 10%)`,
54
+ "--spui-ui-stroke": `var(--spui-semantic-body-divider, var(--colorNeutralStroke2, ${presetStroke}))`,
55
+ "--spui-ui-accent": `var(--spui-semantic-primary-button-background, var(--colorBrandBackground, ${presetAccent}))`,
56
+ "--spui-ui-accent-hover": `var(--spui-semantic-primary-button-background-hovered, color-mix(in srgb, var(--spui-ui-accent, ${presetAccent}) 84%, var(--spui-ui-text, ${presetText}) 16%))`,
57
+ "--spui-ui-accent-text": `var(--spui-semantic-primary-button-text, var(--colorNeutralForegroundOnBrand, ${presetAccentText}))`,
58
+ "--spui-ui-success": `var(--spui-semantic-success-text, ${tokens["--spui-ui-success"]})`,
59
+ "--spui-ui-success-surface": `var(--spui-semantic-success-background, ${tokens["--spui-ui-success-surface"]})`,
60
+ "--spui-ui-warning": `var(--spui-semantic-warning-text, ${tokens["--spui-ui-warning"]})`,
61
+ "--spui-ui-warning-surface": `var(--spui-semantic-warning-background, ${tokens["--spui-ui-warning-surface"]})`,
62
+ "--spui-ui-danger": `var(--spui-semantic-error-text, ${tokens["--spui-ui-danger"]})`,
63
+ "--spui-ui-danger-surface": `var(--spui-semantic-error-background, ${tokens["--spui-ui-danger-surface"]})`,
64
+ "--spui-focus-ring": "var(--spui-ui-border-focus)"
65
+ });
66
+ }
67
+ if (selectedBase === "ios") {
68
+ const tenantText = "var(--spui-semantic-body-text, var(--colorNeutralForeground1, #1c1c1e))";
69
+ const tenantMuted = "var(--spui-semantic-body-subtext, var(--colorNeutralForeground2, #636366))";
70
+ const tenantCanvas = "var(--spui-semantic-body-background, #f2f2f7)";
71
+ const tenantSurface = "var(--spui-semantic-card-background, var(--colorNeutralBackground1, #ffffff))";
72
+ const tenantAccent = `var(--spui-semantic-primary-button-background, var(--colorBrandBackground, ${DEFAULT_ACCENT}))`;
73
+ Object.assign(tokens, {
74
+ "--spui-ui-text": `light-dark(${tenantText}, color-mix(in srgb, ${tenantText} 14%, white 86%))`,
75
+ "--spui-ui-muted": `light-dark(${tenantMuted}, color-mix(in srgb, ${tenantMuted} 18%, white 82%))`,
76
+ "--spui-ui-canvas": `light-dark(color-mix(in srgb, ${tenantCanvas} 90%, ${tenantAccent} 10%), color-mix(in srgb, ${tenantCanvas} 18%, black 82%))`,
77
+ "--spui-ui-surface": `light-dark(${tenantSurface}, color-mix(in srgb, ${tenantSurface} 20%, black 80%))`,
78
+ "--spui-ui-surface-hover": `light-dark(color-mix(in srgb, var(--spui-ui-surface, #ffffff) 90%, ${tenantAccent} 10%), color-mix(in srgb, var(--spui-ui-surface, #1c1c1e) 90%, white 10%))`,
79
+ "--spui-ui-stroke": "light-dark(var(--spui-semantic-body-divider, var(--colorNeutralStroke2, #c6c6c8)), color-mix(in srgb, var(--spui-semantic-body-divider, #38383a) 25%, white 75%))",
80
+ "--spui-ui-accent": `light-dark(${tenantAccent}, ${tenantAccent})`,
81
+ "--spui-ui-accent-hover": `var(--spui-semantic-primary-button-background-hovered, color-mix(in srgb, ${tenantAccent} 84%, black))`,
82
+ "--spui-ui-success": "light-dark(var(--spui-semantic-success-text, #248a3d), color-mix(in srgb, var(--spui-semantic-success-text, #248a3d) 32%, white 68%))",
83
+ "--spui-ui-success-surface": "light-dark(var(--spui-semantic-success-background, #e4f5e9), color-mix(in srgb, var(--spui-semantic-success-background, #14321c) 28%, black 72%))",
84
+ "--spui-ui-warning": "light-dark(var(--spui-semantic-warning-text, #9a6700), color-mix(in srgb, var(--spui-semantic-warning-text, #9a6700) 30%, white 70%))",
85
+ "--spui-ui-warning-surface": "light-dark(var(--spui-semantic-warning-background, #fff3d6), color-mix(in srgb, var(--spui-semantic-warning-background, #342900) 28%, black 72%))",
86
+ "--spui-ui-danger": "light-dark(var(--spui-semantic-error-text, #d70015), color-mix(in srgb, var(--spui-semantic-error-text, #d70015) 30%, white 70%))",
87
+ "--spui-ui-danger-surface": "light-dark(var(--spui-semantic-error-background, #ffe5e8), color-mix(in srgb, var(--spui-semantic-error-background, #3a1114) 28%, black 72%))",
88
+ "--spui-focus-ring": "var(--spui-ui-border-focus)"
89
+ });
90
+ }
91
+ // Stable shared roles for applications that style slots or compose their own
92
+ // controls. These remain live aliases so SharePoint and appearance changes
93
+ // continue to flow through without copying resolved colors at render time.
94
+ Object.assign(tokens, {
95
+ "--spui-ui-foreground-subtle": "color-mix(in srgb, var(--spui-ui-text) 72%, var(--spui-ui-surface))",
96
+ "--spui-ui-foreground-disabled": "var(--spui-semantic-disabled-text, var(--colorNeutralForegroundDisabled, color-mix(in srgb, var(--spui-ui-muted) 68%, var(--spui-ui-surface))))",
97
+ "--spui-ui-text-muted": "var(--colorNeutralForeground3, var(--spui-ui-muted))",
98
+ "--spui-ui-surface-raised": "color-mix(in srgb, var(--spui-ui-surface) 96%, var(--spui-ui-text))",
99
+ "--spui-ui-surface-secondary": "var(--colorNeutralBackground3, var(--spui-ui-surface-raised))",
100
+ "--spui-ui-surface-subtle": "var(--colorNeutralBackground2, var(--spui-ui-canvas))",
101
+ "--spui-ui-surface-alt": "var(--colorNeutralBackground2, var(--spui-ui-surface-subtle))",
102
+ "--spui-ui-surface-sunken": "color-mix(in srgb, var(--spui-ui-canvas) 92%, var(--spui-ui-text))",
103
+ "--spui-ui-border-subtle": "color-mix(in srgb, var(--spui-ui-stroke) 55%, var(--spui-ui-surface))",
104
+ "--spui-ui-border-strong": "color-mix(in srgb, var(--spui-ui-stroke) 72%, var(--spui-ui-text))",
105
+ "--spui-ui-border-focus": "color-mix(in srgb, var(--spui-semantic-focus-border, var(--colorStrokeFocus2, var(--spui-ui-accent))) 30%, var(--spui-ui-text, #242424))",
106
+ "--spui-ui-focus": "var(--spui-ui-border-focus)",
107
+ "--spui-ui-border-disabled": "var(--colorNeutralStrokeDisabled, color-mix(in srgb, var(--spui-ui-stroke) 40%, var(--spui-ui-surface)))",
108
+ "--spui-ui-accent-pressed": "var(--colorBrandBackgroundPressed, color-mix(in srgb, var(--spui-ui-accent) 76%, black))",
109
+ "--spui-ui-accent-subtle": "var(--colorBrandBackground2, color-mix(in srgb, var(--spui-ui-accent) 14%, var(--spui-ui-surface)))",
110
+ "--spui-ui-accent-subtle-hover": "var(--colorBrandBackground2Hover, color-mix(in srgb, var(--spui-ui-accent) 20%, var(--spui-ui-surface)))",
111
+ "--spui-ui-accent-subtle-pressed": "var(--colorBrandBackground2Pressed, color-mix(in srgb, var(--spui-ui-accent) 26%, var(--spui-ui-surface)))",
112
+ "--spui-ui-hover": "var(--spui-ui-surface-hover)",
113
+ "--spui-selection-background": "color-mix(in srgb, var(--spui-ui-accent) 14%, var(--spui-ui-surface))",
114
+ "--spui-selection-background-hover": "var(--colorNeutralBackground1Selected, color-mix(in srgb, var(--spui-ui-accent) 20%, var(--spui-ui-surface)))",
115
+ "--spui-selection-foreground": "var(--spui-ui-text)",
116
+ "--spui-selection-border": "var(--spui-ui-accent)",
117
+ "--spui-link-color": "var(--spui-semantic-link, color-mix(in srgb, var(--spui-ui-accent) 55%, var(--spui-ui-text) 45%))",
118
+ "--spui-link-visited": "var(--spui-semantic-link-visited, var(--spui-link-color))",
119
+ "--spui-overlay-scrim": "rgb(0 0 0 / 40%)",
120
+ "--spui-ui-info": "var(--spui-ui-accent)",
121
+ "--spui-ui-info-surface": "color-mix(in srgb, var(--spui-ui-accent) 12%, var(--spui-ui-surface))",
122
+ "--spui-font-family-mono": "ui-monospace, SFMono-Regular, Consolas, monospace",
123
+ "--spui-text-font-size-small": "12px",
124
+ "--spui-text-font-size-large": "18px",
125
+ "--spui-text-font-size-heading": "24px",
126
+ "--spui-text-font-weight-strong": "600",
127
+ "--spui-text-line-height-heading": "1.25",
128
+ "--spui-text-letter-spacing": "normal",
129
+ "--spui-control-padding-block": "6px",
130
+ "--spui-section-gap": "24px",
131
+ "--spui-list-gap": "var(--spui-control-gap)",
132
+ "--spui-touch-target-min": "44px",
133
+ "--spui-surface-radius": "var(--spui-card-radius)",
134
+ "--spui-pill-radius": "999px",
135
+ "--spui-focus-ring-width": "2px",
136
+ "--spui-popover-shadow": "0 4px 16px #0002",
137
+ "--spui-overlay-shadow": "0 12px 40px #0003",
138
+ "--spui-focus-shadow": "0 0 0 2px var(--spui-focus-ring)",
139
+ "--spui-motion-duration-slow": "220ms",
140
+ "--spui-motion-easing-emphasized": "cubic-bezier(.2,0,0,1)"
141
+ });
142
+ const densityScale = density === "compact" ? 0.88 : density === "touch" ? 1.12 : 1;
143
+ if (densityScale !== 1) {
144
+ for (const name of ["--spui-control-height", "--spui-control-gap", "--spui-control-padding-inline", "--spui-control-padding-block", "--spui-card-padding", "--spui-field-gap", "--spui-section-gap", "--spui-list-gap", "--spui-base-card-padding", "--spui-base-control-gap", "--spui-base-control-padding", "--spui-base-control-height"]) {
145
+ if (tokens[name] !== undefined)
146
+ tokens[name] = scalePresetLength(tokens[name], densityScale);
147
+ }
148
+ }
149
+ // Treat incoming style tokens as the selected base theme. Glass derives its
150
+ // finish from those resolved colors and dimensions, then explicit glass
151
+ // token overrides can still opt out of an individual derived value.
152
+ for (const [name, value] of Object.entries(overrides ?? {}))
153
+ if (typeof value === "string" || typeof value === "number")
154
+ tokens[name] = value;
155
+ tokens["--spui-base-card-padding"] = overrides?.["--spui-base-card-padding"] ?? tokens["--spui-card-padding"] ?? "16px";
156
+ tokens["--spui-base-control-gap"] = overrides?.["--spui-base-control-gap"] ?? tokens["--spui-control-gap"] ?? "8px";
157
+ tokens["--spui-base-control-padding"] = overrides?.["--spui-base-control-padding"] ?? tokens["--spui-control-padding-inline"] ?? "14px";
158
+ tokens["--spui-base-control-height"] = overrides?.["--spui-base-control-height"] ?? tokens["--spui-control-height"] ?? "32px";
159
+ if (appearance === "glass")
160
+ Object.assign(tokens, {
161
+ "--spui-base-text": overrides?.["--spui-ui-text"] ?? tokens["--spui-ui-text"],
162
+ "--spui-base-muted": overrides?.["--spui-ui-muted"] ?? tokens["--spui-ui-muted"],
163
+ "--spui-base-canvas": overrides?.["--spui-ui-canvas"] ?? tokens["--spui-ui-canvas"],
164
+ "--spui-base-surface": overrides?.["--spui-ui-surface"] ?? tokens["--spui-ui-surface"],
165
+ "--spui-base-stroke": overrides?.["--spui-ui-stroke"] ?? tokens["--spui-ui-stroke"],
166
+ "--spui-base-accent": overrides?.["--spui-ui-accent"] ?? tokens["--spui-ui-accent"],
167
+ "--spui-base-accent-text": overrides?.["--spui-ui-accent-text"] ?? tokens["--spui-ui-accent-text"],
168
+ "--spui-base-success": overrides?.["--spui-ui-success-surface"] ?? tokens["--spui-ui-success-surface"],
169
+ "--spui-base-warning": overrides?.["--spui-ui-warning-surface"] ?? tokens["--spui-ui-warning-surface"],
170
+ "--spui-base-danger": overrides?.["--spui-ui-danger-surface"] ?? tokens["--spui-ui-danger-surface"],
171
+ "--spui-base-success-text": overrides?.["--spui-ui-success"] ?? tokens["--spui-ui-success"],
172
+ "--spui-base-warning-text": overrides?.["--spui-ui-warning"] ?? tokens["--spui-ui-warning"],
173
+ "--spui-base-danger-text": overrides?.["--spui-ui-danger"] ?? tokens["--spui-ui-danger"],
174
+ "--spui-ui-text": "var(--spui-base-text)",
175
+ "--spui-ui-muted": "var(--spui-base-muted)",
176
+ "--spui-ui-canvas": "color-mix(in srgb, var(--spui-base-canvas) var(--spui-glass-canvas-opacity,84%), transparent)",
177
+ "--spui-ui-surface": "color-mix(in srgb, var(--spui-base-surface) var(--spui-glass-surface-opacity,88%), transparent)",
178
+ "--spui-ui-surface-hover": "color-mix(in srgb, var(--spui-base-surface) 90%, var(--spui-base-accent) 10%)",
179
+ "--spui-ui-stroke": "color-mix(in srgb, var(--spui-base-stroke) var(--spui-glass-border-opacity,64%), transparent)",
180
+ "--spui-ui-accent": "var(--spui-base-accent)",
181
+ "--spui-ui-accent-hover": "color-mix(in srgb, var(--spui-base-accent) 84%, black)",
182
+ "--spui-ui-accent-text": "var(--spui-base-accent-text)",
183
+ "--spui-ui-success-surface": "color-mix(in srgb, var(--spui-base-success) 72%, transparent)",
184
+ "--spui-ui-warning-surface": "color-mix(in srgb, var(--spui-base-warning) 72%, transparent)",
185
+ "--spui-ui-danger-surface": "color-mix(in srgb, var(--spui-base-danger) 72%, transparent)",
186
+ "--spui-success-surface": "var(--spui-ui-success-surface)",
187
+ "--spui-warning-surface": "var(--spui-ui-warning-surface)",
188
+ "--spui-danger-surface": "var(--spui-ui-danger-surface)",
189
+ "--spui-success-text": "var(--spui-base-success-text)",
190
+ "--spui-warning-text": "var(--spui-base-warning-text)",
191
+ "--spui-danger-text": "var(--spui-base-danger-text)",
192
+ "--spui-control-background": "color-mix(in srgb, var(--spui-base-surface) 86%, transparent)",
193
+ "--spui-control-border": "color-mix(in srgb, var(--spui-base-stroke) 72%, transparent)",
194
+ "--spui-primary-background": "color-mix(in srgb, var(--spui-base-accent) 94%, var(--spui-base-surface) 6%)",
195
+ "--spui-primary-hover": "color-mix(in srgb, var(--spui-base-accent) 86%, black 14%)",
196
+ "--spui-card-background": "linear-gradient(145deg, color-mix(in srgb, var(--spui-base-surface) 76%, var(--spui-base-accent) 12%), color-mix(in srgb, var(--spui-base-surface) 84%, var(--spui-base-accent) 8%))",
197
+ "--spui-card-border": "var(--spui-ui-stroke)",
198
+ "--spui-card-backdrop": "blur(var(--spui-glass-blur,16px)) saturate(var(--spui-glass-saturation,150%))",
199
+ "--spui-card-shadow": "0 8px 28px color-mix(in srgb, var(--spui-base-text) 14%, transparent)",
200
+ "--spui-spacing-scale": "1.12",
201
+ "--spui-control-padding-inline": scalePresetLength(tokens["--spui-base-control-padding"], 1.12),
202
+ "--spui-control-height": scalePresetLength(tokens["--spui-base-control-height"], 1.04),
203
+ "--spui-card-padding": scalePresetLength(tokens["--spui-base-card-padding"], 1.12),
204
+ "--spui-control-gap": scalePresetLength(tokens["--spui-base-control-gap"], 1.12),
205
+ "--spui-color-scheme": "light"
206
+ });
207
+ if (appearance === "glass") {
208
+ // Directly specified finished values win; base values still flow through
209
+ // the glass transforms above.
210
+ for (const name of ["--spui-control-padding-inline", "--spui-control-height", "--spui-card-padding", "--spui-control-gap", "--spui-color-scheme"]) {
211
+ const value = overrides?.[name];
212
+ if (typeof value === "string" || typeof value === "number")
213
+ tokens[name] = value;
214
+ }
215
+ }
216
+ return tokens;
217
+ }
@@ -0,0 +1,9 @@
1
+ export type AccordionItemValue = string | number;
2
+ export type AccordionNavigation = "linear" | "circular";
3
+ export type AccordionToggleData<TValue extends AccordionItemValue = AccordionItemValue> = {
4
+ value: TValue;
5
+ openItems: TValue[];
6
+ };
7
+ export declare function defineAccordionHeader(): void;
8
+ export declare function defineAccordionPanel(): void;
9
+ export declare function defineAccordion(): void;
@@ -0,0 +1,338 @@
1
+ import { base } from "./shared.js";
2
+ const normalizeItems = (value) => (Array.isArray(value) ? value : value === undefined || value === null ? [] : [value]).map(String);
3
+ const directItems = (accordion) => [...accordion.children].filter((child) => child instanceof HTMLElement && child.localName === "spui-accordion-item");
4
+ export function defineAccordionHeader() {
5
+ if (customElements.get("spui-accordion-header"))
6
+ return;
7
+ class SpuiAccordionHeader extends HTMLElement {
8
+ static observedAttributes = ["expand-icon-position", "inline", "size"];
9
+ connectedCallback() {
10
+ if (this.shadowRoot)
11
+ return;
12
+ const shadow = this.attachShadow({ mode: "open" });
13
+ const style = document.createElement("style");
14
+ style.textContent = base + ':host{display:contents}.header{display:flex;align-items:center;gap:8px}.content{min-width:0}.icon,.expand{display:inline-flex;align-items:center;justify-content:center;flex:none}.icon[hidden],.expand[hidden]{display:none}:host([expand-icon-position=start]) .expand{order:-1}:host([open]) .expand{transform:rotate(90deg)}:host(:dir(rtl)) .expand{transform:rotate(180deg)}:host([open]:dir(rtl)) .expand{transform:rotate(90deg)}@media(prefers-reduced-motion:reduce){.expand{transition:none}}';
15
+ const header = document.createElement("div");
16
+ header.className = "header";
17
+ const icon = document.createElement("span");
18
+ icon.className = "icon";
19
+ icon.setAttribute("aria-hidden", "true");
20
+ const iconSlot = document.createElement("slot");
21
+ iconSlot.name = "icon";
22
+ icon.append(iconSlot);
23
+ const content = document.createElement("span");
24
+ content.className = "content";
25
+ const defaultSlot = document.createElement("slot");
26
+ content.append(defaultSlot);
27
+ const expand = document.createElement("span");
28
+ expand.className = "expand";
29
+ expand.setAttribute("aria-hidden", "true");
30
+ const expandSlot = document.createElement("slot");
31
+ expandSlot.name = "expand-icon";
32
+ expand.append(expandSlot);
33
+ header.append(icon, content, expand);
34
+ shadow.append(style, header);
35
+ const syncExpand = () => {
36
+ const hasExpand = expandSlot.assignedNodes({ flatten: true }).length > 0;
37
+ this.toggleAttribute("has-expand-icon", hasExpand);
38
+ this.dispatchEvent(new CustomEvent("spui-header-slots-change", { bubbles: true, composed: true }));
39
+ };
40
+ expandSlot.addEventListener("slotchange", syncExpand);
41
+ syncExpand();
42
+ }
43
+ }
44
+ customElements.define("spui-accordion-header", SpuiAccordionHeader);
45
+ }
46
+ export function defineAccordionPanel() {
47
+ if (customElements.get("spui-accordion-panel"))
48
+ return;
49
+ class SpuiAccordionPanel extends HTMLElement {
50
+ connectedCallback() { if (!this.shadowRoot) {
51
+ const shadow = this.attachShadow({ mode: "open" });
52
+ const style = document.createElement("style");
53
+ style.textContent = `${base}:host{display:block;min-width:0}`;
54
+ shadow.append(style, document.createElement("slot"));
55
+ } }
56
+ }
57
+ customElements.define("spui-accordion-panel", SpuiAccordionPanel);
58
+ }
59
+ export function defineAccordion() {
60
+ if (customElements.get("spui-accordion"))
61
+ return;
62
+ defineAccordionHeader();
63
+ defineAccordionPanel();
64
+ if (!customElements.get("spui-accordion-item")) {
65
+ class SpuiAccordionItem extends HTMLElement {
66
+ static observedAttributes = ["heading", "value", "open", "disabled", "disabled-focusable"];
67
+ panelId = `spui-accordion-panel-${crypto.randomUUID()}`;
68
+ triggerId = `spui-accordion-trigger-${crypto.randomUUID()}`;
69
+ wasOpen = false;
70
+ panelExitPending = false;
71
+ panelExitGeneration = 0;
72
+ buttonSlotAttributes = new Set();
73
+ connectedCallback() { if (!this.hasAttribute("value"))
74
+ this.setAttribute("value", this.id || crypto.randomUUID()); if (!this.shadowRoot)
75
+ this.render(); this.sync(); }
76
+ attributeChangedCallback() { if (this.isConnected)
77
+ this.sync(); }
78
+ get open() { return this.hasAttribute("open"); }
79
+ set open(value) { this.toggleAttribute("open", Boolean(value)); }
80
+ sync() {
81
+ const button = this.shadowRoot?.querySelector("button");
82
+ const panel = this.shadowRoot?.querySelector(".panel");
83
+ if (!button || !panel)
84
+ return;
85
+ const isOpen = this.open;
86
+ button.setAttribute("aria-expanded", String(isOpen));
87
+ button.disabled = this.hasAttribute("disabled");
88
+ if (button.disabled || this.hasAttribute("disabled-focusable"))
89
+ button.setAttribute("aria-disabled", "true");
90
+ else
91
+ button.removeAttribute("aria-disabled");
92
+ const motionPanel = this.shadowRoot?.querySelector("slot[name=panel]")?.assignedElements({ flatten: true }).some(element => element.localName === "spui-accordion-panel" && element.hasAttribute("motion")) ?? false;
93
+ if (isOpen) {
94
+ this.panelExitGeneration++;
95
+ this.panelExitPending = false;
96
+ panel.hidden = false;
97
+ panel.inert = false;
98
+ panel.removeAttribute("aria-hidden");
99
+ }
100
+ else if (motionPanel && this.wasOpen) {
101
+ this.panelExitPending = true;
102
+ panel.hidden = false;
103
+ panel.inert = true;
104
+ panel.setAttribute("aria-hidden", "true");
105
+ this.finishPanelExit(panel);
106
+ }
107
+ else if (!this.panelExitPending) {
108
+ panel.hidden = true;
109
+ panel.inert = false;
110
+ panel.removeAttribute("aria-hidden");
111
+ }
112
+ this.wasOpen = isOpen;
113
+ const heading = button.querySelector("slot[name=heading]");
114
+ const header = heading?.assignedElements({ flatten: true }).find(element => element.localName === "spui-accordion-header");
115
+ const icon = button.querySelector(".icon");
116
+ const buttonSlot = header?.button;
117
+ button.className = "trigger";
118
+ button.style.cssText = "";
119
+ for (const attribute of this.buttonSlotAttributes)
120
+ button.removeAttribute(attribute);
121
+ this.buttonSlotAttributes.clear();
122
+ if (buttonSlot) {
123
+ if (typeof buttonSlot.className === "string")
124
+ button.classList.add(...buttonSlot.className.split(/\s+/).filter(Boolean));
125
+ if (buttonSlot.style && typeof buttonSlot.style === "object")
126
+ Object.assign(button.style, buttonSlot.style);
127
+ for (const [name, value] of Object.entries(buttonSlot)) {
128
+ if (name === "className" || name === "style" || name === "children" || name === "disabled" || name === "type" || name === "tabIndex" || name === "onClick" || value == null || typeof value === "function")
129
+ continue;
130
+ const attribute = name.startsWith("aria-") || name.startsWith("data-") ? name : { title: "title", name: "name", value: "value", form: "form", accessKey: "accesskey", id: "id" }[name];
131
+ if (attribute) {
132
+ button.setAttribute(attribute, String(value));
133
+ this.buttonSlotAttributes.add(attribute);
134
+ }
135
+ }
136
+ }
137
+ if (icon && heading) {
138
+ const iconAtStart = header?.getAttribute("expand-icon-position") === "start";
139
+ button.insertBefore(icon, iconAtStart ? heading : null);
140
+ icon.hidden = header?.hasAttribute("has-expand-icon") ?? false;
141
+ }
142
+ header?.toggleAttribute("open", isOpen);
143
+ const fallback = heading?.firstChild;
144
+ if (fallback?.nodeType === Node.TEXT_NODE)
145
+ fallback.textContent = this.getAttribute("heading") || "Section";
146
+ }
147
+ finishPanelExit(panel) {
148
+ if (!this.panelExitPending)
149
+ return;
150
+ const generation = ++this.panelExitGeneration;
151
+ requestAnimationFrame(() => requestAnimationFrame(() => {
152
+ const animations = new Set();
153
+ const visit = (element) => {
154
+ for (const animation of element.getAnimations())
155
+ if (animation.playState === "running")
156
+ animations.add(animation);
157
+ if (element.shadowRoot)
158
+ for (const child of element.shadowRoot.children)
159
+ visit(child);
160
+ if (element instanceof HTMLSlotElement)
161
+ for (const child of element.assignedElements({ flatten: true }))
162
+ visit(child);
163
+ for (const child of element.children)
164
+ visit(child);
165
+ };
166
+ for (const assigned of this.shadowRoot?.querySelector("slot[name=panel]")?.assignedElements({ flatten: true }) ?? [])
167
+ visit(assigned);
168
+ const finish = () => {
169
+ if (generation !== this.panelExitGeneration || this.open || !this.panelExitPending)
170
+ return;
171
+ this.panelExitPending = false;
172
+ panel.hidden = true;
173
+ };
174
+ if (animations.size)
175
+ void Promise.allSettled([...animations].map(animation => animation.finished)).then(finish);
176
+ else
177
+ finish();
178
+ }));
179
+ }
180
+ render() {
181
+ const shadow = this.attachShadow({ mode: "open" });
182
+ const style = document.createElement("style");
183
+ style.textContent = `${base}:host{display:block;border-block-end:1px solid var(--spui-accordion-divider,var(--spui-ui-stroke,#d1d1d1))}.trigger{display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%;min-height:44px;padding:var(--spui-accordion-padding,12px 16px);border:0;background:transparent;color:var(--spui-ui-text,#242424);font:inherit;font-weight:600;text-align:start;cursor:pointer}.trigger:focus-visible{outline:2px solid var(--spui-focus-ring,var(--spui-semantic-focus-border,#78716c));outline-offset:-2px}.trigger:disabled{color:var(--spui-ui-muted,#616161);cursor:not-allowed}.icon{display:grid;place-items:center;flex:0 0 24px;width:24px;height:24px;transition:transform var(--spui-motion-duration-fast,120ms) var(--spui-motion-easing-standard,cubic-bezier(.2,0,0,1))}.icon:before{content:"";width:7px;height:7px;border-block-start:2px solid currentColor;border-inline-end:2px solid currentColor;transform:rotate(45deg)}.trigger[aria-expanded=true] .icon{transform:rotate(90deg)}:host(:dir(rtl)) .icon{transform:rotate(180deg)}:host(:dir(rtl)) .trigger[aria-expanded=true] .icon{transform:rotate(90deg)}.panel{padding:0 var(--spui-accordion-padding-inline,16px) 16px;color:var(--spui-ui-text,#242424)}.panel[hidden]{display:none}@media(prefers-reduced-motion:reduce){.icon{transition:none}}@media(forced-colors:active){.trigger:focus-visible{outline-color:Highlight}}`;
184
+ const button = document.createElement("button");
185
+ button.type = "button";
186
+ button.id = this.triggerId;
187
+ button.className = "trigger";
188
+ button.part.add("trigger");
189
+ button.setAttribute("aria-controls", this.panelId);
190
+ const heading = document.createElement("slot");
191
+ heading.name = "heading";
192
+ heading.append(document.createTextNode(this.getAttribute("heading") || "Section"));
193
+ const icon = document.createElement("span");
194
+ icon.className = "icon";
195
+ icon.part.add("icon");
196
+ icon.setAttribute("aria-hidden", "true");
197
+ button.append(heading, icon);
198
+ const panel = document.createElement("div");
199
+ panel.className = "panel";
200
+ panel.part.add("panel");
201
+ panel.id = this.panelId;
202
+ panel.setAttribute("role", "region");
203
+ panel.setAttribute("aria-labelledby", this.triggerId);
204
+ const panelSlot = document.createElement("slot");
205
+ panelSlot.name = "panel";
206
+ panelSlot.append(document.createElement("slot"));
207
+ panel.append(panelSlot);
208
+ button.addEventListener("click", event => {
209
+ const header = button.querySelector("slot[name=heading]")?.assignedElements({ flatten: true }).find(element => element.localName === "spui-accordion-header");
210
+ header?.button?.onClickCapture?.(event);
211
+ }, true);
212
+ button.addEventListener("click", event => {
213
+ const heading = button.querySelector("slot[name=heading]");
214
+ const header = heading?.assignedElements({ flatten: true }).find(element => element.localName === "spui-accordion-header");
215
+ header?.button?.onClick?.(event);
216
+ if (event.defaultPrevented)
217
+ return;
218
+ this.open = !this.open;
219
+ this.dispatchEvent(new CustomEvent("spui-item-toggle", { bubbles: true, composed: true, detail: { value: this.getAttribute("value") ?? "", open: this.open, sourceEvent: event } }));
220
+ });
221
+ heading.addEventListener("slotchange", () => this.sync());
222
+ button.addEventListener("spui-header-slots-change", () => this.sync());
223
+ shadow.append(style, button, panel);
224
+ }
225
+ }
226
+ customElements.define("spui-accordion-item", SpuiAccordionItem);
227
+ }
228
+ class SpuiAccordion extends HTMLElement {
229
+ static observedAttributes = ["mode", "multiple", "collapsible", "navigation"];
230
+ controlledItems;
231
+ uncontrolledItems = [];
232
+ hasInitialState = false;
233
+ hasDefaultItems = false;
234
+ syncing = false;
235
+ events;
236
+ get openItems() { return this.controlledItems; }
237
+ set openItems(value) { this.controlledItems = value === undefined ? undefined : normalizeItems(value); this.syncItems(); }
238
+ set defaultOpenItems(value) { if (!this.controlledItems && !this.hasDefaultItems && value !== undefined) {
239
+ this.uncontrolledItems = normalizeItems(value);
240
+ this.hasInitialState = true;
241
+ this.hasDefaultItems = true;
242
+ this.syncItems();
243
+ } }
244
+ get defaultOpenItems() { return this.uncontrolledItems; }
245
+ get multiple() { return this.hasAttribute("mode") ? this.getAttribute("mode") !== "single" : this.hasAttribute("multiple"); }
246
+ get collapsible() { return this.hasAttribute("collapsible"); }
247
+ connectedCallback() {
248
+ this.events?.abort();
249
+ this.events = new AbortController();
250
+ this.addEventListener("spui-item-toggle", this.onToggle, { signal: this.events.signal });
251
+ this.addEventListener("keydown", this.onKeyDown, { signal: this.events.signal });
252
+ if (!this.hasInitialState && !this.controlledItems) {
253
+ this.uncontrolledItems = directItems(this).filter(item => item.hasAttribute("open")).map(item => item.getAttribute("value") ?? "");
254
+ this.hasInitialState = true;
255
+ }
256
+ this.sync();
257
+ }
258
+ disconnectedCallback() { this.events?.abort(); }
259
+ attributeChangedCallback() { if (this.isConnected)
260
+ this.sync(); }
261
+ get selectedItems() { return this.controlledItems ?? this.uncontrolledItems; }
262
+ syncItems() {
263
+ if (!this.isConnected)
264
+ return;
265
+ const values = this.multiple ? this.selectedItems : this.selectedItems.slice(0, 1);
266
+ this.syncing = true;
267
+ const disabledFocusableValue = !this.multiple && !this.collapsible && values.length === 1 ? values[0] : undefined;
268
+ for (const item of directItems(this)) {
269
+ const value = item.getAttribute("value") ?? "";
270
+ item.toggleAttribute("open", values.includes(value));
271
+ item.toggleAttribute("disabled-focusable", value === disabledFocusableValue && item.hasAttribute("open"));
272
+ }
273
+ this.syncing = false;
274
+ this.updateTabStops();
275
+ }
276
+ sync() {
277
+ this.dataset.mode = this.multiple ? "multiple" : "single";
278
+ this.syncItems();
279
+ }
280
+ onToggle = (event) => {
281
+ if (event.target.closest("spui-accordion") !== this || this.syncing)
282
+ return;
283
+ const { value, open, sourceEvent } = event.detail;
284
+ const clickedItem = event.target;
285
+ const current = this.controlledItems ?? directItems(this).filter(item => item.hasAttribute("open") && !(item === clickedItem && open) || item === clickedItem && !open).map(item => item.getAttribute("value") ?? "");
286
+ let next = open ? (this.multiple ? [...current.filter(item => item !== value), value] : [value]) : current.filter(item => item !== value);
287
+ if (!this.multiple && !this.collapsible && current.includes(value) && next.length === 0) {
288
+ event.target.setAttribute("open", "");
289
+ return;
290
+ }
291
+ if (this.controlledItems)
292
+ this.syncItems();
293
+ else {
294
+ this.uncontrolledItems = next;
295
+ this.syncItems();
296
+ }
297
+ const detail = { value, openItems: next };
298
+ this.dispatchEvent(new CustomEvent("spui-toggle", { bubbles: true, composed: true, detail: { ...detail, sourceEvent } }));
299
+ };
300
+ enabledTriggers() { return directItems(this).filter(item => !item.hasAttribute("disabled")).map(item => item.shadowRoot?.querySelector("button")).filter((button) => Boolean(button)); }
301
+ updateTabStops() {
302
+ const triggers = this.enabledTriggers();
303
+ if (!triggers.length)
304
+ return;
305
+ if (!this.hasAttribute("navigation")) {
306
+ triggers.forEach(button => { button.tabIndex = 0; });
307
+ return;
308
+ }
309
+ const active = triggers.includes(this.ownerDocument.activeElement) ? this.ownerDocument.activeElement : triggers.find(button => button.tabIndex === 0) ?? triggers[0];
310
+ triggers.forEach(button => { button.tabIndex = button === active ? 0 : -1; });
311
+ }
312
+ onKeyDown = (event) => {
313
+ const navigation = this.getAttribute("navigation");
314
+ if (navigation !== "linear" && navigation !== "circular")
315
+ return;
316
+ const triggers = this.enabledTriggers();
317
+ const target = event.composedPath().find(node => node instanceof HTMLButtonElement && triggers.includes(node));
318
+ if (!target)
319
+ return;
320
+ const index = triggers.indexOf(target);
321
+ let next;
322
+ if (event.key === "ArrowDown" || event.key === "ArrowRight")
323
+ next = triggers[index + 1] ?? (navigation === "circular" ? triggers[0] : undefined);
324
+ else if (event.key === "ArrowUp" || event.key === "ArrowLeft")
325
+ next = triggers[index - 1] ?? (navigation === "circular" ? triggers.at(-1) : undefined);
326
+ else if (event.key === "Home")
327
+ next = triggers[0];
328
+ else if (event.key === "End")
329
+ next = triggers.at(-1);
330
+ if (!next)
331
+ return;
332
+ event.preventDefault();
333
+ triggers.forEach(button => { button.tabIndex = button === next ? 0 : -1; });
334
+ next.focus();
335
+ };
336
+ }
337
+ customElements.define("spui-accordion", SpuiAccordion);
338
+ }
@@ -0,0 +1,2 @@
1
+ export type AlertAppearance = "primary" | "inverted";
2
+ export declare function defineAlert(): void;