@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.
- package/AGENTS.md +211 -0
- package/LICENSE +21 -0
- package/README.md +897 -2
- package/components.json +5810 -0
- package/dist/appearance.d.ts +6 -0
- package/dist/appearance.js +217 -0
- package/dist/elements/accordion.d.ts +9 -0
- package/dist/elements/accordion.js +338 -0
- package/dist/elements/alert.d.ts +2 -0
- package/dist/elements/alert.js +62 -0
- package/dist/elements/avatar-group-primitives.d.ts +3 -0
- package/dist/elements/avatar-group-primitives.js +189 -0
- package/dist/elements/avatar-group.d.ts +2 -0
- package/dist/elements/avatar-group.js +110 -0
- package/dist/elements/avatar.d.ts +4 -0
- package/dist/elements/avatar.js +251 -0
- package/dist/elements/badge.d.ts +8 -0
- package/dist/elements/badge.js +182 -0
- package/dist/elements/breadcrumb.d.ts +4 -0
- package/dist/elements/breadcrumb.js +233 -0
- package/dist/elements/button-group.d.ts +1 -0
- package/dist/elements/button-group.js +20 -0
- package/dist/elements/button-variants.d.ts +6 -0
- package/dist/elements/button-variants.js +339 -0
- package/dist/elements/button.d.ts +5 -0
- package/dist/elements/button.js +233 -0
- package/dist/elements/card-primitives.d.ts +3 -0
- package/dist/elements/card-primitives.js +83 -0
- package/dist/elements/card.d.ts +5 -0
- package/dist/elements/card.js +213 -0
- package/dist/elements/carousel.d.ts +92 -0
- package/dist/elements/carousel.js +1112 -0
- package/dist/elements/color-picker.d.ts +24 -0
- package/dist/elements/color-picker.js +1004 -0
- package/dist/elements/combobox.d.ts +1 -0
- package/dist/elements/combobox.js +1012 -0
- package/dist/elements/data-grid-model.d.ts +117 -0
- package/dist/elements/data-grid-model.js +294 -0
- package/dist/elements/data-grid.d.ts +29 -0
- package/dist/elements/data-grid.js +1567 -0
- package/dist/elements/date-picker.d.ts +1 -0
- package/dist/elements/date-picker.js +486 -0
- package/dist/elements/dialog.d.ts +23 -0
- package/dist/elements/dialog.js +409 -0
- package/dist/elements/divider.d.ts +2 -0
- package/dist/elements/divider.js +66 -0
- package/dist/elements/drawer.d.ts +3 -0
- package/dist/elements/drawer.js +308 -0
- package/dist/elements/field-composition.d.ts +26 -0
- package/dist/elements/field-composition.js +133 -0
- package/dist/elements/field.d.ts +1 -0
- package/dist/elements/field.js +67 -0
- package/dist/elements/file-upload.d.ts +6 -0
- package/dist/elements/file-upload.js +211 -0
- package/dist/elements/floating-position.d.ts +26 -0
- package/dist/elements/floating-position.js +39 -0
- package/dist/elements/image.d.ts +28 -0
- package/dist/elements/image.js +38 -0
- package/dist/elements/info.d.ts +9 -0
- package/dist/elements/info.js +102 -0
- package/dist/elements/input.d.ts +5 -0
- package/dist/elements/input.js +255 -0
- package/dist/elements/label.d.ts +15 -0
- package/dist/elements/label.js +56 -0
- package/dist/elements/link.d.ts +1 -0
- package/dist/elements/link.js +92 -0
- package/dist/elements/list.d.ts +19 -0
- package/dist/elements/list.js +297 -0
- package/dist/elements/menu.d.ts +33 -0
- package/dist/elements/menu.js +1103 -0
- package/dist/elements/message-bar.d.ts +10 -0
- package/dist/elements/message-bar.js +299 -0
- package/dist/elements/nav.d.ts +32 -0
- package/dist/elements/nav.js +791 -0
- package/dist/elements/overflow.d.ts +9 -0
- package/dist/elements/overflow.js +218 -0
- package/dist/elements/persona.d.ts +3 -0
- package/dist/elements/persona.js +124 -0
- package/dist/elements/popover.d.ts +77 -0
- package/dist/elements/popover.js +840 -0
- package/dist/elements/progress-bar.d.ts +4 -0
- package/dist/elements/progress-bar.js +82 -0
- package/dist/elements/radio-group.d.ts +1 -0
- package/dist/elements/radio-group.js +303 -0
- package/dist/elements/rating-display.d.ts +2 -0
- package/dist/elements/rating-display.js +125 -0
- package/dist/elements/rating.d.ts +2 -0
- package/dist/elements/rating.js +206 -0
- package/dist/elements/rich-text-editor.d.ts +18 -0
- package/dist/elements/rich-text-editor.js +508 -0
- package/dist/elements/search.d.ts +3 -0
- package/dist/elements/search.js +207 -0
- package/dist/elements/select.d.ts +6 -0
- package/dist/elements/select.js +291 -0
- package/dist/elements/shared.d.ts +19 -0
- package/dist/elements/shared.js +396 -0
- package/dist/elements/skeleton.d.ts +6 -0
- package/dist/elements/skeleton.js +91 -0
- package/dist/elements/slider.d.ts +1 -0
- package/dist/elements/slider.js +248 -0
- package/dist/elements/spin-button.d.ts +7 -0
- package/dist/elements/spin-button.js +499 -0
- package/dist/elements/spinner.d.ts +4 -0
- package/dist/elements/spinner.js +124 -0
- package/dist/elements/swatch-picker.d.ts +5 -0
- package/dist/elements/swatch-picker.js +363 -0
- package/dist/elements/table.d.ts +21 -0
- package/dist/elements/table.js +261 -0
- package/dist/elements/tabs.d.ts +1 -0
- package/dist/elements/tabs.js +276 -0
- package/dist/elements/tag-picker.d.ts +29 -0
- package/dist/elements/tag-picker.js +697 -0
- package/dist/elements/tags.d.ts +21 -0
- package/dist/elements/tags.js +421 -0
- package/dist/elements/teaching-popover.d.ts +32 -0
- package/dist/elements/teaching-popover.js +412 -0
- package/dist/elements/text.d.ts +5 -0
- package/dist/elements/text.js +69 -0
- package/dist/elements/textarea.d.ts +1 -0
- package/dist/elements/textarea.js +182 -0
- package/dist/elements/toast.d.ts +25 -0
- package/dist/elements/toast.js +337 -0
- package/dist/elements/toaster.d.ts +6 -0
- package/dist/elements/toaster.js +356 -0
- package/dist/elements/toggle.d.ts +2 -0
- package/dist/elements/toggle.js +246 -0
- package/dist/elements/toolbar.d.ts +12 -0
- package/dist/elements/toolbar.js +381 -0
- package/dist/elements/tooltip.d.ts +76 -0
- package/dist/elements/tooltip.js +510 -0
- package/dist/elements/tree-model.d.ts +19 -0
- package/dist/elements/tree-model.js +34 -0
- package/dist/elements/tree.d.ts +5 -0
- package/dist/elements/tree.js +273 -0
- package/dist/elements/view-card.d.ts +2 -0
- package/dist/elements/view-card.js +78 -0
- package/dist/elements/virtual-list.d.ts +41 -0
- package/dist/elements/virtual-list.js +369 -0
- package/dist/elements.d.ts +72 -0
- package/dist/elements.js +207 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +66 -0
- package/dist/react-theme-styles.d.ts +2 -0
- package/dist/react-theme-styles.js +75 -0
- package/dist/react.d.ts +4540 -0
- package/dist/react.js +6799 -0
- package/dist/theme.d.ts +169 -0
- package/dist/theme.js +167 -0
- package/dist/virtualizer-hooks.d.ts +80 -0
- package/dist/virtualizer-hooks.js +253 -0
- package/package.json +87 -4
- package/scripts/generate-token-index.mjs +43 -0
- package/scripts/sync-component-catalog-signatures.mjs +55 -0
- package/scripts/validate-component-catalog.mjs +44 -0
- package/skills/sharepoint-ui/SKILL.md +12 -0
- package/skills/sharepoint-ui/references/choosing-components.md +20 -0
- package/skills/sharepoint-ui/references/sharepoint-patterns.md +17 -0
- 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
|
+
}
|