zdp-design-system 0.43.8

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 (110) hide show
  1. package/CHANGELOG.md +449 -0
  2. package/LICENSE +21 -0
  3. package/README.md +568 -0
  4. package/THIRD_PARTY_NOTICES.md +34 -0
  5. package/dist/code.ts +2 -0
  6. package/dist/combobox.ts +9 -0
  7. package/dist/command.ts +1 -0
  8. package/dist/components/Accordion.svelte +97 -0
  9. package/dist/components/Avatar.svelte +90 -0
  10. package/dist/components/Badge.svelte +61 -0
  11. package/dist/components/Breadcrumb.svelte +97 -0
  12. package/dist/components/Button.svelte +163 -0
  13. package/dist/components/Callout.svelte +81 -0
  14. package/dist/components/Card.svelte +151 -0
  15. package/dist/components/CardHeader.svelte +58 -0
  16. package/dist/components/Checkbox.svelte +135 -0
  17. package/dist/components/CodeBlock.svelte +247 -0
  18. package/dist/components/Combobox.svelte +552 -0
  19. package/dist/components/CommandField.svelte +230 -0
  20. package/dist/components/ConfirmAction.svelte +307 -0
  21. package/dist/components/Container.svelte +63 -0
  22. package/dist/components/Dialog.svelte +303 -0
  23. package/dist/components/Disclosure.svelte +176 -0
  24. package/dist/components/Divider.svelte +41 -0
  25. package/dist/components/EmptyState.svelte +79 -0
  26. package/dist/components/ErrorText.svelte +18 -0
  27. package/dist/components/Field.svelte +38 -0
  28. package/dist/components/Grid.svelte +76 -0
  29. package/dist/components/HelpText.svelte +17 -0
  30. package/dist/components/Icon.svelte +45 -0
  31. package/dist/components/IconButton.svelte +162 -0
  32. package/dist/components/IdentityChip.svelte +130 -0
  33. package/dist/components/Inline.svelte +85 -0
  34. package/dist/components/InlineCode.svelte +27 -0
  35. package/dist/components/Input.svelte +109 -0
  36. package/dist/components/Kbd.svelte +63 -0
  37. package/dist/components/KeyValue.svelte +73 -0
  38. package/dist/components/Label.svelte +43 -0
  39. package/dist/components/Link.svelte +70 -0
  40. package/dist/components/LocaleSwitcher.svelte +209 -0
  41. package/dist/components/Menu.svelte +491 -0
  42. package/dist/components/Page.svelte +36 -0
  43. package/dist/components/PageHeader.svelte +93 -0
  44. package/dist/components/Pagination.svelte +297 -0
  45. package/dist/components/Popover.svelte +208 -0
  46. package/dist/components/Progress.svelte +111 -0
  47. package/dist/components/Radio.svelte +132 -0
  48. package/dist/components/Section.svelte +52 -0
  49. package/dist/components/SegmentedControl.svelte +190 -0
  50. package/dist/components/Select.svelte +88 -0
  51. package/dist/components/ShareDock.svelte +304 -0
  52. package/dist/components/Sheet.svelte +332 -0
  53. package/dist/components/ShortcutHint.svelte +52 -0
  54. package/dist/components/Skeleton.svelte +82 -0
  55. package/dist/components/SkipLink.svelte +40 -0
  56. package/dist/components/SortHeader.svelte +138 -0
  57. package/dist/components/Spinner.svelte +82 -0
  58. package/dist/components/Stack.svelte +62 -0
  59. package/dist/components/StatusToast.svelte +133 -0
  60. package/dist/components/Surface.svelte +53 -0
  61. package/dist/components/Switch.svelte +152 -0
  62. package/dist/components/Table.svelte +94 -0
  63. package/dist/components/TableToolbar.svelte +195 -0
  64. package/dist/components/Tabs.svelte +205 -0
  65. package/dist/components/TermSheet.svelte +392 -0
  66. package/dist/components/TermTrigger.svelte +70 -0
  67. package/dist/components/TextScaleControl.svelte +219 -0
  68. package/dist/components/Textarea.svelte +106 -0
  69. package/dist/components/ThemeToggle.svelte +148 -0
  70. package/dist/components/Toast.svelte +180 -0
  71. package/dist/components/Toolbar.svelte +83 -0
  72. package/dist/components/Tooltip.svelte +199 -0
  73. package/dist/components/VisuallyHidden.svelte +18 -0
  74. package/dist/disclosure.ts +11 -0
  75. package/dist/focusable.ts +36 -0
  76. package/dist/identity.ts +5 -0
  77. package/dist/index.d.ts +106 -0
  78. package/dist/index.js +76 -0
  79. package/dist/index.ts +106 -0
  80. package/dist/menu.ts +12 -0
  81. package/dist/modal-layer.ts +108 -0
  82. package/dist/pagination.ts +10 -0
  83. package/dist/preferences.js +14 -0
  84. package/dist/preferences.ts +36 -0
  85. package/dist/progress.ts +4 -0
  86. package/dist/schemas/design-tokens.schema.json +119 -0
  87. package/dist/segmented.ts +8 -0
  88. package/dist/share.d.ts +48 -0
  89. package/dist/share.js +115 -0
  90. package/dist/share.ts +99 -0
  91. package/dist/sheet.ts +3 -0
  92. package/dist/shortcuts.js +125 -0
  93. package/dist/shortcuts.ts +153 -0
  94. package/dist/styles/brand-fonts.css +10 -0
  95. package/dist/styles/components.css +4686 -0
  96. package/dist/styles/expressive-fonts.css +2 -0
  97. package/dist/styles/index.css +2 -0
  98. package/dist/styles/locale-fonts.css +4 -0
  99. package/dist/styles/tokens.css +413 -0
  100. package/dist/table-tools.ts +10 -0
  101. package/dist/term.ts +16 -0
  102. package/dist/theme.ts +2 -0
  103. package/dist/toast.ts +14 -0
  104. package/dist/tokens/zdp.tokens.json +241 -0
  105. package/dist/tokens.js +122 -0
  106. package/dist/tokens.ts +123 -0
  107. package/docs/CONSUMER_CONTRACT.md +482 -0
  108. package/docs/EXTERNAL_UI_ADOPTION.md +141 -0
  109. package/docs/INTERACTIVE_PRIMITIVE_AUDIT.md +127 -0
  110. package/package.json +78 -0
@@ -0,0 +1,392 @@
1
+ <script lang="ts">
2
+ import { onDestroy, tick } from 'svelte';
3
+ import { isZdpFocusableElement, zdpFocusableSelector } from '../focusable';
4
+ import { createZdpModalLayer } from '../modal-layer';
5
+ import type { ZdpTermSheetPlacement, ZdpTermSheetTerm } from '../term';
6
+
7
+ export let open = false;
8
+ export let id = 'zdp-term-sheet';
9
+ export let term: ZdpTermSheetTerm | null = null;
10
+ export let placement: ZdpTermSheetPlacement = 'right';
11
+ export let closeLabel = 'Close';
12
+ export let eyebrow = 'Term';
13
+ export let detailLabel = 'View details';
14
+ export let relatedLabel = 'Related terms';
15
+ export let exampleLabel = 'Example';
16
+ export let closeOnEscape = true;
17
+ export let closeOnBackdrop = true;
18
+ export let onClose: (() => void) | null = null;
19
+ export let onRelatedTerm: ((termId: string) => void) | null = null;
20
+
21
+ let panelElement: HTMLElement | null = null;
22
+ let layerElement: HTMLElement | null = null;
23
+ let previousFocusElement: HTMLElement | null = null;
24
+ let knownOpenState = false;
25
+ const modalLayer = createZdpModalLayer();
26
+
27
+ $: modalLayer.setActive(open && term !== null, layerElement);
28
+
29
+ onDestroy(() => {
30
+ modalLayer.destroy();
31
+ });
32
+
33
+ $: titleId = `${id}-title`;
34
+ $: descriptionId = `${id}-description`;
35
+ $: resolvedPlacement = placement;
36
+
37
+ $: if (open !== knownOpenState) {
38
+ knownOpenState = open;
39
+
40
+ if (open) {
41
+ void handleSheetOpened();
42
+ } else {
43
+ restorePreviousFocus();
44
+ }
45
+ }
46
+
47
+ async function handleSheetOpened(): Promise<void> {
48
+ if (typeof document === 'undefined') {
49
+ return;
50
+ }
51
+
52
+ const activeElement = document.activeElement;
53
+ previousFocusElement = activeElement instanceof HTMLElement ? activeElement : null;
54
+
55
+ await tick();
56
+
57
+ const firstElement = getFocusableElements()[0] ?? panelElement;
58
+ firstElement?.focus();
59
+ }
60
+
61
+ function restorePreviousFocus(): void {
62
+ if (typeof document === 'undefined') {
63
+ return;
64
+ }
65
+
66
+ if (previousFocusElement !== null && document.contains(previousFocusElement)) {
67
+ previousFocusElement.focus();
68
+ }
69
+
70
+ previousFocusElement = null;
71
+ }
72
+
73
+ function requestClose(): void {
74
+ open = false;
75
+ onClose?.();
76
+ }
77
+
78
+ function handleBackdropClick(): void {
79
+ if (closeOnBackdrop) {
80
+ requestClose();
81
+ }
82
+ }
83
+
84
+ function handleKeydown(event: KeyboardEvent): void {
85
+ if (event.key === 'Escape' && closeOnEscape) {
86
+ event.preventDefault();
87
+ requestClose();
88
+ return;
89
+ }
90
+
91
+ if (event.key !== 'Tab') {
92
+ return;
93
+ }
94
+
95
+ const focusableElements = getFocusableElements();
96
+
97
+ if (focusableElements.length === 0) {
98
+ event.preventDefault();
99
+ panelElement?.focus();
100
+ return;
101
+ }
102
+
103
+ const firstElement = focusableElements[0];
104
+ const lastElement = focusableElements[focusableElements.length - 1];
105
+
106
+ if (event.shiftKey && document.activeElement === firstElement) {
107
+ event.preventDefault();
108
+ lastElement.focus();
109
+ return;
110
+ }
111
+
112
+ if (!event.shiftKey && document.activeElement === lastElement) {
113
+ event.preventDefault();
114
+ firstElement.focus();
115
+ }
116
+ }
117
+
118
+ function getFocusableElements(): HTMLElement[] {
119
+ if (panelElement === null) {
120
+ return [];
121
+ }
122
+
123
+ return Array.from(panelElement.querySelectorAll<HTMLElement>(zdpFocusableSelector)).filter(
124
+ isZdpFocusableElement
125
+ );
126
+ }
127
+ </script>
128
+
129
+ {#if open && term !== null}
130
+ <div class="zdp-term-layer" bind:this={layerElement}>
131
+ <button
132
+ class="zdp-term-sheet__backdrop"
133
+ type="button"
134
+ aria-label={closeLabel}
135
+ tabindex="-1"
136
+ onclick={handleBackdropClick}
137
+ ></button>
138
+ <div
139
+ class={`zdp-term-sheet zdp-term-sheet--${resolvedPlacement}`}
140
+ id={id}
141
+ bind:this={panelElement}
142
+ role="dialog"
143
+ aria-modal="true"
144
+ aria-labelledby={titleId}
145
+ aria-describedby={descriptionId}
146
+ tabindex="-1"
147
+ data-term-id={term.id}
148
+ data-zdp-ad-exclude="true"
149
+ data-zdp-term-id={term.id}
150
+ data-zdp-term-placement={resolvedPlacement}
151
+ data-zdp-term-surface="sheet"
152
+ onkeydown={handleKeydown}
153
+ >
154
+ <header class="zdp-term-sheet__header">
155
+ <div class="zdp-term-sheet__heading">
156
+ <p class="zdp-term-sheet__eyebrow">{eyebrow}</p>
157
+ <h2 id={titleId} class="zdp-term-sheet__title">{term.label}</h2>
158
+ </div>
159
+ <button class="zdp-term-sheet__close" type="button" aria-label={closeLabel} onclick={requestClose}>
160
+ <span aria-hidden="true">×</span>
161
+ </button>
162
+ </header>
163
+
164
+ <div class="zdp-term-sheet__body">
165
+ <p id={descriptionId} class="zdp-term-sheet__short">{term.short}</p>
166
+
167
+ {#if term.long}
168
+ <p>{term.long}</p>
169
+ {/if}
170
+
171
+ {#if term.example}
172
+ <section class="zdp-term-sheet__section" aria-label={exampleLabel}>
173
+ <h3>{exampleLabel}</h3>
174
+ <p>{term.example}</p>
175
+ </section>
176
+ {/if}
177
+
178
+ {#if term.relatedTerms && term.relatedTerms.length > 0}
179
+ <section class="zdp-term-sheet__section" aria-label={relatedLabel}>
180
+ <h3>{relatedLabel}</h3>
181
+ <div class="zdp-term-sheet__related">
182
+ {#each term.relatedTerms as relatedTerm}
183
+ <button
184
+ class="zdp-term-sheet__related-button"
185
+ type="button"
186
+ data-term-id={relatedTerm.id}
187
+ data-zdp-term-id={relatedTerm.id}
188
+ onclick={() => onRelatedTerm?.(relatedTerm.id)}
189
+ >
190
+ {relatedTerm.label}
191
+ </button>
192
+ {/each}
193
+ </div>
194
+ </section>
195
+ {/if}
196
+ </div>
197
+
198
+ {#if term.canonicalPath}
199
+ <footer class="zdp-term-sheet__footer">
200
+ <a class="zdp-term-sheet__detail-link" href={term.canonicalPath}>{detailLabel}</a>
201
+ </footer>
202
+ {/if}
203
+ </div>
204
+ </div>
205
+ {/if}
206
+
207
+ <style>
208
+ .zdp-term-layer {
209
+ display: contents;
210
+ }
211
+
212
+ .zdp-term-sheet {
213
+ background: var(--zdp-color-surface-panel);
214
+ border: var(--zdp-control-border-width) solid var(--zdp-color-line-strong);
215
+ box-sizing: border-box;
216
+ color: var(--zdp-color-ink-normal);
217
+ display: grid;
218
+ font-family: var(--zdp-font-family-sans);
219
+ gap: var(--zdp-space-4);
220
+ max-block-size: calc(100vh - var(--zdp-space-6));
221
+ overflow: auto;
222
+ padding: var(--zdp-space-5);
223
+ position: fixed;
224
+ z-index: 901;
225
+ }
226
+
227
+ .zdp-term-sheet__backdrop {
228
+ background: rgb(47 36 24 / 0.28);
229
+ border: 0;
230
+ cursor: pointer;
231
+ inset: 0;
232
+ margin: 0;
233
+ padding: 0;
234
+ position: fixed;
235
+ z-index: 900;
236
+ }
237
+
238
+ :global([data-zdp-theme="dark"]) .zdp-term-sheet__backdrop {
239
+ background: rgb(10 8 5 / 0.64);
240
+ }
241
+
242
+ .zdp-term-sheet--right {
243
+ block-size: calc(100vh - var(--zdp-space-6));
244
+ border-radius: var(--zdp-control-radius);
245
+ inline-size: min(28rem, calc(100vw - var(--zdp-space-6)));
246
+ inset-block: var(--zdp-space-3);
247
+ inset-inline-end: var(--zdp-space-3);
248
+ }
249
+
250
+ .zdp-term-sheet--bottom {
251
+ border-end-end-radius: 0;
252
+ border-end-start-radius: 0;
253
+ border-radius: var(--zdp-control-radius) var(--zdp-control-radius) 0 0;
254
+ inset-block-end: 0;
255
+ inset-inline: 0;
256
+ max-block-size: min(34rem, calc(100vh - var(--zdp-space-6)));
257
+ }
258
+
259
+ .zdp-term-sheet:focus-visible {
260
+ border-color: var(--zdp-color-focus-line);
261
+ outline: var(--zdp-control-focus-outline-width) solid var(--zdp-color-focus-surface);
262
+ outline-offset: var(--zdp-control-focus-outline-offset);
263
+ }
264
+
265
+ .zdp-term-sheet__header {
266
+ align-items: start;
267
+ display: grid;
268
+ gap: var(--zdp-space-3);
269
+ grid-template-columns: minmax(0, 1fr) auto;
270
+ }
271
+
272
+ .zdp-term-sheet__heading {
273
+ display: grid;
274
+ gap: var(--zdp-space-1);
275
+ min-width: 0;
276
+ }
277
+
278
+ .zdp-term-sheet__eyebrow {
279
+ color: var(--zdp-color-ink-muted);
280
+ font-size: var(--zdp-type-caption-size);
281
+ line-height: var(--zdp-type-caption-line-height);
282
+ margin: 0;
283
+ }
284
+
285
+ .zdp-term-sheet__title {
286
+ color: var(--zdp-color-ink-strong);
287
+ font-size: var(--zdp-type-title-size);
288
+ line-height: var(--zdp-type-title-line-height);
289
+ margin: 0;
290
+ overflow-wrap: anywhere;
291
+ }
292
+
293
+ .zdp-term-sheet__close {
294
+ align-items: center;
295
+ background: var(--zdp-color-surface-panel);
296
+ border: var(--zdp-control-border-width) solid var(--zdp-color-line-strong);
297
+ border-radius: var(--zdp-control-radius);
298
+ color: var(--zdp-color-ink-normal);
299
+ cursor: pointer;
300
+ display: inline-flex;
301
+ font-family: var(--zdp-font-family-sans);
302
+ font-size: var(--zdp-type-control-size);
303
+ height: var(--zdp-control-icon-sm);
304
+ justify-content: center;
305
+ line-height: 1;
306
+ -webkit-user-select: none;
307
+ user-select: none;
308
+ width: var(--zdp-control-icon-sm);
309
+ }
310
+
311
+ .zdp-term-sheet__close:focus-visible,
312
+ .zdp-term-sheet__related-button:focus-visible,
313
+ .zdp-term-sheet__detail-link:focus-visible {
314
+ border-color: var(--zdp-color-focus-line);
315
+ outline: var(--zdp-control-focus-outline-width) solid var(--zdp-color-focus-surface);
316
+ outline-offset: var(--zdp-control-focus-outline-offset);
317
+ }
318
+
319
+ .zdp-term-sheet__body {
320
+ display: grid;
321
+ gap: var(--zdp-space-3);
322
+ line-height: var(--zdp-type-body-line-height);
323
+ min-width: 0;
324
+ }
325
+
326
+ .zdp-term-sheet__body p,
327
+ .zdp-term-sheet__section h3 {
328
+ margin: 0;
329
+ }
330
+
331
+ .zdp-term-sheet__short {
332
+ color: var(--zdp-color-ink-strong);
333
+ font-weight: var(--zdp-font-weight-semibold);
334
+ }
335
+
336
+ .zdp-term-sheet__section {
337
+ display: grid;
338
+ gap: var(--zdp-space-2);
339
+ }
340
+
341
+ .zdp-term-sheet__section h3 {
342
+ color: var(--zdp-color-ink-strong);
343
+ font-size: var(--zdp-type-control-size);
344
+ line-height: var(--zdp-type-control-line-height);
345
+ }
346
+
347
+ .zdp-term-sheet__related {
348
+ display: flex;
349
+ flex-wrap: wrap;
350
+ gap: var(--zdp-space-2);
351
+ }
352
+
353
+ .zdp-term-sheet__related-button {
354
+ background: var(--zdp-color-surface-raised);
355
+ border: var(--zdp-control-border-width) solid var(--zdp-color-line-normal);
356
+ border-radius: var(--zdp-control-radius);
357
+ color: var(--zdp-color-ink-normal);
358
+ cursor: pointer;
359
+ font: inherit;
360
+ min-height: var(--zdp-control-height-sm);
361
+ padding: 0 var(--zdp-space-2);
362
+ -webkit-user-select: none;
363
+ user-select: none;
364
+ }
365
+
366
+ .zdp-term-sheet__footer {
367
+ border-block-start: var(--zdp-control-border-width) solid var(--zdp-color-line-subtle);
368
+ padding-block-start: var(--zdp-space-3);
369
+ }
370
+
371
+ .zdp-term-sheet__detail-link {
372
+ color: var(--zdp-color-ink-strong);
373
+ font-weight: var(--zdp-font-weight-semibold);
374
+ text-decoration: underline;
375
+ text-underline-offset: 0.16em;
376
+ }
377
+
378
+ @media (max-width: 720px) {
379
+ .zdp-term-sheet,
380
+ .zdp-term-sheet--right {
381
+ block-size: auto;
382
+ border-end-end-radius: 0;
383
+ border-end-start-radius: 0;
384
+ border-radius: var(--zdp-control-radius) var(--zdp-control-radius) 0 0;
385
+ inline-size: auto;
386
+ inset-block: auto 0;
387
+ inset-inline: 0;
388
+ max-block-size: min(34rem, calc(100vh - var(--zdp-space-6)));
389
+ padding: var(--zdp-space-4);
390
+ }
391
+ }
392
+ </style>
@@ -0,0 +1,70 @@
1
+ <script lang="ts">
2
+ export let termId: string;
3
+ export let controls: string | null = null;
4
+ export let expanded = false;
5
+ export let disabled = false;
6
+ export let ariaLabel: string | null = null;
7
+ export let onopen: ((termId: string) => void) | null = null;
8
+
9
+ $: resolvedControls = controls !== null && expanded ? controls : null;
10
+
11
+ function handleClick(): void {
12
+ if (disabled) {
13
+ return;
14
+ }
15
+
16
+ onopen?.(termId);
17
+ }
18
+ </script>
19
+
20
+ <button
21
+ class="zdp-term-trigger"
22
+ type="button"
23
+ data-term-id={termId}
24
+ aria-label={ariaLabel ?? undefined}
25
+ aria-controls={resolvedControls ?? undefined}
26
+ aria-expanded={controls === null ? undefined : expanded}
27
+ aria-haspopup="dialog"
28
+ disabled={disabled}
29
+ onclick={handleClick}
30
+ >
31
+ <slot />
32
+ </button>
33
+
34
+ <style>
35
+ .zdp-term-trigger {
36
+ align-items: baseline;
37
+ appearance: none;
38
+ background: transparent;
39
+ border: 0;
40
+ border-block-end: var(--zdp-control-border-width) solid var(--zdp-color-focus-line);
41
+ border-radius: var(--zdp-radius-sm);
42
+ color: inherit;
43
+ cursor: pointer;
44
+ display: inline;
45
+ font: inherit;
46
+ line-height: inherit;
47
+ margin: 0;
48
+ padding: 0 0.2rem;
49
+ text-align: inherit;
50
+ text-decoration: none;
51
+ text-underline-offset: 0.14em;
52
+ }
53
+
54
+ .zdp-term-trigger:hover:not(:disabled) {
55
+ background: var(--zdp-color-accent-primary-soft);
56
+ color: inherit;
57
+ }
58
+
59
+ .zdp-term-trigger:focus-visible {
60
+ background: var(--zdp-color-focus-surface);
61
+ color: var(--zdp-color-focus-text);
62
+ outline: var(--zdp-control-focus-outline-width) solid var(--zdp-color-focus-surface);
63
+ outline-offset: var(--zdp-control-focus-outline-offset);
64
+ }
65
+
66
+ .zdp-term-trigger:disabled {
67
+ cursor: not-allowed;
68
+ opacity: 0.58;
69
+ }
70
+ </style>
@@ -0,0 +1,219 @@
1
+ <script lang="ts" context="module">
2
+ let nextTextScaleControlInstanceId = 0;
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ import {
7
+ zdpTextScaleControlOptions,
8
+ type ZdpTextScale,
9
+ type ZdpTextScaleControlOption,
10
+ type ZdpTextScaleControlSize
11
+ } from '../preferences';
12
+
13
+ export let value: ZdpTextScale = 'base';
14
+ export let options: readonly ZdpTextScaleControlOption[] = zdpTextScaleControlOptions;
15
+ export let ariaLabel = 'Text size';
16
+ export let idPrefix: string | null = null;
17
+ export let size: ZdpTextScaleControlSize = 'md';
18
+ export let disabled = false;
19
+ export let onChange:
20
+ | ((event: MouseEvent | KeyboardEvent, option: ZdpTextScaleControlOption) => void)
21
+ | null = null;
22
+
23
+ const fallbackIdPrefix = `zdp-text-scale-control-${++nextTextScaleControlInstanceId}`;
24
+
25
+ $: enabledOptions = options.filter((option) => !option.disabled);
26
+ $: activeOption =
27
+ enabledOptions.find((option) => option.value === value) ?? enabledOptions[0] ?? options[0] ?? null;
28
+ $: activeValue = activeOption?.value ?? value;
29
+ $: resolvedIdPrefix = toDomId(idPrefix ?? fallbackIdPrefix);
30
+
31
+ function selectOption(event: MouseEvent | KeyboardEvent, option: ZdpTextScaleControlOption): void {
32
+ if (disabled || option.disabled) {
33
+ return;
34
+ }
35
+
36
+ const previousValue = value;
37
+ value = option.value;
38
+
39
+ if (previousValue !== option.value) {
40
+ onChange?.(event, option);
41
+ }
42
+ }
43
+
44
+ function handleKeydown(event: KeyboardEvent): void {
45
+ if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) {
46
+ return;
47
+ }
48
+
49
+ const target = event.currentTarget as HTMLElement;
50
+ const controls = Array.from(target.querySelectorAll<HTMLButtonElement>('[role="radio"]:not(:disabled)'));
51
+
52
+ if (controls.length === 0) {
53
+ return;
54
+ }
55
+
56
+ event.preventDefault();
57
+
58
+ const currentIndex = Math.max(
59
+ 0,
60
+ controls.findIndex((control) => control.getAttribute('aria-checked') === 'true')
61
+ );
62
+ const nextIndex = getNextIndex(event.key, currentIndex, controls.length);
63
+ const nextControl = controls[nextIndex];
64
+
65
+ nextControl.focus();
66
+ nextControl.click();
67
+ }
68
+
69
+ function getNextIndex(key: string, currentIndex: number, length: number): number {
70
+ if (key === 'Home') {
71
+ return 0;
72
+ }
73
+
74
+ if (key === 'End') {
75
+ return length - 1;
76
+ }
77
+
78
+ if (key === 'ArrowLeft') {
79
+ return (currentIndex - 1 + length) % length;
80
+ }
81
+
82
+ return (currentIndex + 1) % length;
83
+ }
84
+
85
+ function optionId(option: ZdpTextScaleControlOption): string {
86
+ return `${resolvedIdPrefix}-option-${toDomId(option.value)}`;
87
+ }
88
+
89
+ function toDomId(value: string): string {
90
+ return value.trim().replace(/[^a-zA-Z0-9_-]+/g, '-') || 'option';
91
+ }
92
+ </script>
93
+
94
+ <div
95
+ class={`zdp-text-scale-control zdp-text-scale-control--${size}`}
96
+ role="radiogroup"
97
+ aria-label={ariaLabel}
98
+ aria-disabled={disabled ? 'true' : undefined}
99
+ tabindex="-1"
100
+ data-zdp-text-scale-control
101
+ data-zdp-text-scale-value={activeValue}
102
+ onkeydown={handleKeydown}
103
+ >
104
+ {#each options as option (option.value)}
105
+ <button
106
+ class={`zdp-text-scale-control__item ${
107
+ option.value === activeValue ? 'zdp-text-scale-control__item--selected' : ''
108
+ }`}
109
+ id={optionId(option)}
110
+ type="button"
111
+ role="radio"
112
+ aria-label={option.ariaLabel ?? undefined}
113
+ aria-checked={option.value === activeValue}
114
+ tabindex={option.value === activeValue ? 0 : -1}
115
+ disabled={disabled || option.disabled}
116
+ data-zdp-text-scale-option
117
+ data-zdp-text-scale-option-value={option.value}
118
+ onclick={(event) => selectOption(event, option)}
119
+ >
120
+ <span class="zdp-text-scale-control__sample" aria-hidden={option.ariaLabel ? 'true' : undefined}>
121
+ {option.label}
122
+ </span>
123
+ </button>
124
+ {/each}
125
+ </div>
126
+
127
+ <style>
128
+ .zdp-text-scale-control {
129
+ align-items: center;
130
+ background: var(--zdp-color-surface-panel);
131
+ border: var(--zdp-control-border-width) solid var(--zdp-color-line-subtle);
132
+ border-radius: var(--zdp-control-radius);
133
+ box-sizing: border-box;
134
+ color: var(--zdp-color-ink-normal);
135
+ display: inline-flex;
136
+ flex: 0 0 auto;
137
+ font-family: var(--zdp-font-family-sans);
138
+ gap: var(--zdp-space-1);
139
+ max-width: 100%;
140
+ min-width: 0;
141
+ padding: var(--zdp-space-1);
142
+ }
143
+
144
+ .zdp-text-scale-control__item {
145
+ align-items: center;
146
+ background: transparent;
147
+ border: var(--zdp-control-border-width) solid transparent;
148
+ border-radius: var(--zdp-control-radius);
149
+ box-sizing: border-box;
150
+ color: var(--zdp-color-ink-muted);
151
+ cursor: pointer;
152
+ display: inline-grid;
153
+ font-family: var(--zdp-font-family-sans);
154
+ font-weight: var(--zdp-font-weight-medium);
155
+ justify-content: center;
156
+ line-height: 1;
157
+ min-width: 0;
158
+ padding: 0;
159
+ place-items: center;
160
+ text-align: center;
161
+ transition:
162
+ background-color var(--zdp-motion-fast) ease,
163
+ border-color var(--zdp-motion-fast) ease,
164
+ color var(--zdp-motion-fast) ease;
165
+ -webkit-user-select: none;
166
+ user-select: none;
167
+ }
168
+
169
+ .zdp-text-scale-control--sm .zdp-text-scale-control__item {
170
+ height: var(--zdp-control-icon-sm);
171
+ width: var(--zdp-control-icon-sm);
172
+ }
173
+
174
+ .zdp-text-scale-control--md .zdp-text-scale-control__item {
175
+ height: var(--zdp-control-icon-md);
176
+ width: var(--zdp-control-icon-md);
177
+ }
178
+
179
+ .zdp-text-scale-control__item:hover:not(:disabled):not([aria-checked='true']) {
180
+ background: var(--zdp-color-surface-raised);
181
+ border-color: var(--zdp-color-line-strong);
182
+ color: var(--zdp-color-ink-strong);
183
+ }
184
+
185
+ .zdp-text-scale-control__item:focus-visible {
186
+ border-color: var(--zdp-color-focus-line);
187
+ outline: var(--zdp-control-focus-outline-width) solid var(--zdp-color-focus-surface);
188
+ outline-offset: var(--zdp-control-focus-outline-offset);
189
+ }
190
+
191
+ .zdp-text-scale-control__item--selected,
192
+ .zdp-text-scale-control__item[aria-checked='true'] {
193
+ background: var(--zdp-color-accent-primary-soft);
194
+ border-color: var(--zdp-color-accent-primary-strong);
195
+ color: var(--zdp-color-ink-strong);
196
+ }
197
+
198
+ .zdp-text-scale-control__item:disabled {
199
+ cursor: not-allowed;
200
+ opacity: 0.56;
201
+ }
202
+
203
+ .zdp-text-scale-control__sample {
204
+ display: inline-block;
205
+ line-height: 1;
206
+ }
207
+
208
+ .zdp-text-scale-control__item[data-zdp-text-scale-option-value='base'] .zdp-text-scale-control__sample {
209
+ font-size: var(--zdp-font-size-sm);
210
+ }
211
+
212
+ .zdp-text-scale-control__item[data-zdp-text-scale-option-value='large'] .zdp-text-scale-control__sample {
213
+ font-size: var(--zdp-font-size-md);
214
+ }
215
+
216
+ .zdp-text-scale-control__item[data-zdp-text-scale-option-value='larger'] .zdp-text-scale-control__sample {
217
+ font-size: var(--zdp-font-size-lg);
218
+ }
219
+ </style>