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,152 @@
1
+ <script lang="ts">
2
+ type DescribedBy = string | readonly string[] | null;
3
+
4
+ export let id: string | null = null;
5
+ export let name: string | null = null;
6
+ export let checked = false;
7
+ export let describedBy: DescribedBy = null;
8
+ export let errorMessageId: string | null = null;
9
+ export let invalid = false;
10
+ export let disabled = false;
11
+ export let required = false;
12
+
13
+ $: ariaDescribedBy = normalizeIdRefs(describedBy);
14
+ $: resolvedErrorMessageId = invalid && errorMessageId ? errorMessageId : null;
15
+
16
+ function handleChange(event: Event): void {
17
+ checked = (event.currentTarget as HTMLInputElement).checked;
18
+ }
19
+
20
+ function normalizeIdRefs(value: DescribedBy): string | null {
21
+ if (value === null) {
22
+ return null;
23
+ }
24
+
25
+ if (typeof value === 'string') {
26
+ const normalized = value.trim();
27
+ return normalized ? normalized : null;
28
+ }
29
+
30
+ const normalized = value.map((entry) => entry.trim()).filter(Boolean);
31
+ return normalized.length > 0 ? normalized.join(' ') : null;
32
+ }
33
+ </script>
34
+
35
+ <label class="zdp-switch" data-invalid={invalid ? 'true' : undefined}>
36
+ <input
37
+ class="zdp-switch__input"
38
+ id={id ?? undefined}
39
+ name={name ?? undefined}
40
+ type="checkbox"
41
+ role="switch"
42
+ {checked}
43
+ aria-describedby={ariaDescribedBy ?? undefined}
44
+ aria-errormessage={resolvedErrorMessageId ?? undefined}
45
+ aria-invalid={invalid ? 'true' : undefined}
46
+ {disabled}
47
+ {required}
48
+ onchange={handleChange}
49
+ />
50
+ <span class="zdp-switch__track" aria-hidden="true"></span>
51
+ <span class="zdp-switch__body">
52
+ <span class="zdp-switch__label"><slot /></span>
53
+ <slot name="help" />
54
+ </span>
55
+ </label>
56
+
57
+ <style>
58
+ .zdp-switch {
59
+ align-items: start;
60
+ color: var(--zdp-color-ink-normal);
61
+ cursor: pointer;
62
+ display: grid;
63
+ font-family: var(--zdp-font-family-sans);
64
+ gap: var(--zdp-space-3);
65
+ grid-template-columns: var(--zdp-control-switch-width) minmax(0, 1fr);
66
+ line-height: var(--zdp-type-body-small-line-height);
67
+ min-width: 0;
68
+ -webkit-user-select: none;
69
+ user-select: none;
70
+ }
71
+
72
+ .zdp-switch__input {
73
+ height: 1px;
74
+ margin: 0;
75
+ opacity: 0;
76
+ position: absolute;
77
+ width: 1px;
78
+ }
79
+
80
+ .zdp-switch__track {
81
+ background: var(--zdp-color-surface-panel);
82
+ border: var(--zdp-control-border-width) solid var(--zdp-color-line-strong);
83
+ border-radius: var(--zdp-control-radius);
84
+ box-sizing: border-box;
85
+ display: inline-flex;
86
+ height: var(--zdp-control-switch-height);
87
+ margin-top: var(--zdp-control-switch-thumb-offset);
88
+ position: relative;
89
+ -webkit-user-select: none;
90
+ user-select: none;
91
+ transition:
92
+ background-color var(--zdp-motion-fast) ease,
93
+ border-color var(--zdp-motion-fast) ease;
94
+ width: var(--zdp-control-switch-width);
95
+ }
96
+
97
+ .zdp-switch__track::after {
98
+ background: var(--zdp-color-ink-muted);
99
+ border-radius: var(--zdp-radius-sm);
100
+ content: "";
101
+ height: var(--zdp-control-switch-thumb-size);
102
+ left: var(--zdp-control-switch-thumb-offset);
103
+ position: absolute;
104
+ top: var(--zdp-control-switch-thumb-offset);
105
+ transition:
106
+ background-color var(--zdp-motion-fast) ease,
107
+ left var(--zdp-motion-fast) ease;
108
+ width: var(--zdp-control-switch-thumb-size);
109
+ }
110
+
111
+ .zdp-switch:hover .zdp-switch__input:not(:checked):not(:disabled) + .zdp-switch__track {
112
+ background: var(--zdp-color-surface-raised);
113
+ border-color: var(--zdp-color-line-strong);
114
+ }
115
+
116
+ .zdp-switch__input:checked + .zdp-switch__track {
117
+ background: var(--zdp-color-accent-primary);
118
+ border-color: var(--zdp-color-accent-primary-strong);
119
+ }
120
+
121
+ .zdp-switch__input:checked + .zdp-switch__track::after {
122
+ background: var(--zdp-color-ink-inverse);
123
+ left: var(--zdp-control-switch-thumb-checked-offset);
124
+ }
125
+
126
+ .zdp-switch__input:focus-visible + .zdp-switch__track {
127
+ border-color: var(--zdp-color-focus-line);
128
+ outline: var(--zdp-control-focus-outline-width) solid var(--zdp-color-focus-surface);
129
+ outline-offset: var(--zdp-control-focus-outline-offset);
130
+ }
131
+
132
+ .zdp-switch__input[aria-invalid="true"] + .zdp-switch__track {
133
+ border-color: var(--zdp-color-accent-danger);
134
+ }
135
+
136
+ .zdp-switch:has(.zdp-switch__input:disabled) {
137
+ cursor: not-allowed;
138
+ opacity: 0.56;
139
+ }
140
+
141
+ .zdp-switch__body {
142
+ display: grid;
143
+ gap: var(--zdp-space-1);
144
+ min-width: 0;
145
+ }
146
+
147
+ .zdp-switch__label {
148
+ color: var(--zdp-color-ink-strong);
149
+ font-size: var(--zdp-type-body-small-size);
150
+ line-height: var(--zdp-type-body-small-line-height);
151
+ }
152
+ </style>
@@ -0,0 +1,94 @@
1
+ <script lang="ts">
2
+ export let caption: string | null = null;
3
+ export let captionMode: 'visible' | 'hidden' = 'visible';
4
+ export let density: 'default' | 'compact' = 'default';
5
+ export let labelledBy: string | null = null;
6
+ </script>
7
+
8
+ <div class="zdp-table-wrap">
9
+ <table
10
+ class={`zdp-table zdp-table--density-${density}`}
11
+ aria-labelledby={labelledBy ?? undefined}
12
+ >
13
+ {#if caption}
14
+ <caption class={`zdp-table__caption zdp-table__caption--${captionMode}`}>
15
+ {caption}
16
+ </caption>
17
+ {/if}
18
+ <slot />
19
+ </table>
20
+ </div>
21
+
22
+ <style>
23
+ .zdp-table-wrap {
24
+ background: var(--zdp-color-surface-panel);
25
+ border: 1px solid var(--zdp-color-line-subtle);
26
+ border-radius: var(--zdp-control-radius);
27
+ box-sizing: border-box;
28
+ max-width: 100%;
29
+ min-width: 0;
30
+ overscroll-behavior-inline: contain;
31
+ overflow-x: auto;
32
+ -webkit-overflow-scrolling: touch;
33
+ touch-action: pan-x pan-y;
34
+ }
35
+
36
+ .zdp-table {
37
+ background: var(--zdp-color-surface-panel);
38
+ border-collapse: collapse;
39
+ color: var(--zdp-color-ink-normal);
40
+ font-family: var(--zdp-font-family-sans);
41
+ font-size: var(--zdp-type-body-small-size);
42
+ inline-size: 100%;
43
+ line-height: var(--zdp-type-body-small-line-height);
44
+ min-inline-size: 36rem;
45
+ }
46
+
47
+ .zdp-table__caption {
48
+ color: var(--zdp-color-ink-strong);
49
+ font-size: var(--zdp-type-label-size);
50
+ font-weight: var(--zdp-font-weight-medium);
51
+ line-height: var(--zdp-type-label-line-height);
52
+ padding: var(--zdp-space-3) var(--zdp-space-4);
53
+ text-align: start;
54
+ }
55
+
56
+ .zdp-table__caption--hidden {
57
+ border: 0;
58
+ clip: rect(0 0 0 0);
59
+ clip-path: inset(50%);
60
+ height: 1px;
61
+ margin: -1px;
62
+ overflow: hidden;
63
+ padding: 0;
64
+ position: absolute;
65
+ white-space: nowrap;
66
+ width: 1px;
67
+ }
68
+
69
+ .zdp-table :global(th),
70
+ .zdp-table :global(td) {
71
+ background: var(--zdp-color-surface-panel);
72
+ border-block-start: 1px solid var(--zdp-color-line-subtle);
73
+ overflow-wrap: normal;
74
+ padding: var(--zdp-space-3) var(--zdp-space-4);
75
+ text-align: start;
76
+ vertical-align: top;
77
+ white-space: nowrap;
78
+ word-break: normal;
79
+ }
80
+
81
+ .zdp-table :global(th) {
82
+ color: var(--zdp-color-ink-strong);
83
+ font-weight: var(--zdp-font-weight-medium);
84
+ }
85
+
86
+ .zdp-table :global(thead th) {
87
+ background: var(--zdp-color-surface-raised);
88
+ }
89
+
90
+ .zdp-table--density-compact :global(th),
91
+ .zdp-table--density-compact :global(td) {
92
+ padding: var(--zdp-space-2) var(--zdp-space-3);
93
+ }
94
+ </style>
@@ -0,0 +1,195 @@
1
+ <script lang="ts">
2
+ import SegmentedControl from './SegmentedControl.svelte';
3
+ import type { ZdpSegmentedControlItem } from '../segmented';
4
+ import type { ZdpTableDensity, ZdpTableToolbarDensityItem } from '../table-tools';
5
+
6
+ const defaultDensityItems: readonly ZdpTableToolbarDensityItem[] = [
7
+ { id: 'default', label: 'Default' },
8
+ { id: 'compact', label: 'Compact' }
9
+ ];
10
+
11
+ export let title: string | null = null;
12
+ export let summary: string | null = null;
13
+ export let selectedCount: number | null = null;
14
+ export let selectedLabel: string | null = null;
15
+ export let density: ZdpTableDensity = 'default';
16
+ export let densityLabel = 'Table density';
17
+ export let densityItems: readonly ZdpTableToolbarDensityItem[] = defaultDensityItems;
18
+ export let labelledBy: string | null = null;
19
+ export let ariaLabel = 'Table tools';
20
+ export let onDensityChange:
21
+ | ((event: MouseEvent | KeyboardEvent, density: ZdpTableDensity, item: ZdpTableToolbarDensityItem) => void)
22
+ | null = null;
23
+
24
+ $: normalizedSelectedCount = normalizeCount(selectedCount);
25
+ $: resolvedSelectedLabel =
26
+ selectedLabel ?? (normalizedSelectedCount > 0 ? `${normalizedSelectedCount} selected` : null);
27
+ $: segmentedDensityItems = densityItems.map((item) => ({
28
+ id: item.id,
29
+ label: item.label,
30
+ ariaLabel: item.ariaLabel,
31
+ disabled: item.disabled
32
+ }));
33
+ $: activeDensity = normalizeDensity(density, densityItems);
34
+
35
+ function handleDensityChange(
36
+ event: MouseEvent | KeyboardEvent,
37
+ item: ZdpSegmentedControlItem
38
+ ): void {
39
+ const densityItem = densityItems.find((entry) => entry.id === item.id);
40
+
41
+ if (densityItem) {
42
+ onDensityChange?.(event, densityItem.id, densityItem);
43
+ }
44
+ }
45
+
46
+ function normalizeCount(value: number | null): number {
47
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
48
+ return 0;
49
+ }
50
+
51
+ return Math.floor(value);
52
+ }
53
+
54
+ function normalizeDensity(
55
+ value: ZdpTableDensity,
56
+ items: readonly ZdpTableToolbarDensityItem[]
57
+ ): ZdpTableDensity {
58
+ if (items.some((item) => item.id === value && !item.disabled)) {
59
+ return value;
60
+ }
61
+
62
+ return items.find((item) => !item.disabled)?.id ?? value;
63
+ }
64
+ </script>
65
+
66
+ <div
67
+ class="zdp-table-toolbar"
68
+ role="group"
69
+ aria-label={labelledBy ? undefined : ariaLabel}
70
+ aria-labelledby={labelledBy ?? undefined}
71
+ >
72
+ <div class="zdp-table-toolbar__body">
73
+ {#if title}
74
+ <strong class="zdp-table-toolbar__title">{title}</strong>
75
+ {/if}
76
+ {#if summary}
77
+ <p class="zdp-table-toolbar__summary">{summary}</p>
78
+ {/if}
79
+ {#if resolvedSelectedLabel}
80
+ <p class="zdp-table-toolbar__selection">{resolvedSelectedLabel}</p>
81
+ {/if}
82
+ <slot />
83
+ </div>
84
+
85
+ <div class="zdp-table-toolbar__controls">
86
+ <slot name="controls" />
87
+ {#if densityItems.length > 0}
88
+ <div class="zdp-table-toolbar__density">
89
+ <span class="zdp-table-toolbar__density-label">{densityLabel}</span>
90
+ <SegmentedControl
91
+ ariaLabel={densityLabel}
92
+ items={segmentedDensityItems}
93
+ selectedId={activeDensity}
94
+ size="sm"
95
+ onChange={handleDensityChange}
96
+ />
97
+ </div>
98
+ {/if}
99
+ </div>
100
+
101
+ <div class="zdp-table-toolbar__actions">
102
+ {#if normalizedSelectedCount > 0}
103
+ <slot name="selection-actions" />
104
+ {/if}
105
+ <slot name="actions" />
106
+ </div>
107
+ </div>
108
+
109
+ <style>
110
+ .zdp-table-toolbar {
111
+ align-items: center;
112
+ background: var(--zdp-color-surface-panel);
113
+ border: var(--zdp-control-border-width) solid var(--zdp-color-line-subtle);
114
+ border-radius: var(--zdp-control-radius);
115
+ box-sizing: border-box;
116
+ color: var(--zdp-color-ink-normal);
117
+ display: flex;
118
+ flex-wrap: wrap;
119
+ font-family: var(--zdp-font-family-sans);
120
+ gap: var(--zdp-space-3);
121
+ justify-content: space-between;
122
+ min-width: 0;
123
+ padding: var(--zdp-space-3);
124
+ }
125
+
126
+ .zdp-table-toolbar__body {
127
+ display: grid;
128
+ flex: 1 1 16rem;
129
+ gap: var(--zdp-space-1);
130
+ min-inline-size: min(100%, 16rem);
131
+ }
132
+
133
+ .zdp-table-toolbar__title {
134
+ color: var(--zdp-color-ink-strong);
135
+ font-size: var(--zdp-type-body-small-size);
136
+ font-weight: var(--zdp-font-weight-medium);
137
+ line-height: var(--zdp-type-body-small-line-height);
138
+ min-width: 0;
139
+ overflow-wrap: var(--zdp-i18n-overflow-wrap);
140
+ }
141
+
142
+ .zdp-table-toolbar__summary,
143
+ .zdp-table-toolbar__selection {
144
+ font-size: var(--zdp-type-caption-size);
145
+ line-height: var(--zdp-type-caption-line-height);
146
+ margin: 0;
147
+ min-width: 0;
148
+ overflow-wrap: var(--zdp-i18n-overflow-wrap);
149
+ }
150
+
151
+ .zdp-table-toolbar__summary {
152
+ color: var(--zdp-color-ink-muted);
153
+ }
154
+
155
+ .zdp-table-toolbar__selection {
156
+ color: var(--zdp-color-ink-strong);
157
+ font-weight: var(--zdp-font-weight-medium);
158
+ }
159
+
160
+ .zdp-table-toolbar__controls,
161
+ .zdp-table-toolbar__actions,
162
+ .zdp-table-toolbar__density {
163
+ align-items: center;
164
+ display: flex;
165
+ flex: 0 1 auto;
166
+ flex-wrap: wrap;
167
+ gap: var(--zdp-space-2);
168
+ min-width: 0;
169
+ }
170
+
171
+ .zdp-table-toolbar__controls:empty,
172
+ .zdp-table-toolbar__actions:empty {
173
+ display: none;
174
+ }
175
+
176
+ .zdp-table-toolbar__actions {
177
+ justify-content: flex-end;
178
+ margin-inline-start: auto;
179
+ }
180
+
181
+ .zdp-table-toolbar__density-label {
182
+ color: var(--zdp-color-ink-muted);
183
+ font-size: var(--zdp-type-caption-size);
184
+ font-weight: var(--zdp-font-weight-medium);
185
+ line-height: var(--zdp-type-caption-line-height);
186
+ }
187
+
188
+ @media (max-width: 48rem) {
189
+ .zdp-table-toolbar__actions {
190
+ inline-size: 100%;
191
+ justify-content: flex-start;
192
+ margin-inline-start: 0;
193
+ }
194
+ }
195
+ </style>
@@ -0,0 +1,205 @@
1
+ <script lang="ts" context="module">
2
+ let nextTabsInstanceId = 0;
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ interface TabItem {
7
+ readonly id: string;
8
+ readonly label: string;
9
+ readonly disabled?: boolean;
10
+ }
11
+
12
+ export let items: readonly TabItem[] = [];
13
+ export let selectedId: string | null = null;
14
+ export let ariaLabel = 'Tabs';
15
+ export let idPrefix: string | null = null;
16
+
17
+ const fallbackIdPrefix = `zdp-tabs-${++nextTabsInstanceId}`;
18
+
19
+ $: selectedItem =
20
+ items.find((item) => item.id === selectedId && !item.disabled) ??
21
+ items.find((item) => !item.disabled) ??
22
+ items[0] ??
23
+ null;
24
+ $: activeId = selectedItem?.id ?? '';
25
+
26
+ function selectTab(item: TabItem): void {
27
+ if (item.disabled) {
28
+ return;
29
+ }
30
+
31
+ selectedId = item.id;
32
+ }
33
+
34
+ function handleKeydown(event: KeyboardEvent): void {
35
+ if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) {
36
+ return;
37
+ }
38
+
39
+ const target = event.currentTarget as HTMLElement;
40
+ const tabs = Array.from(target.querySelectorAll<HTMLButtonElement>('[role="tab"]:not(:disabled)'));
41
+
42
+ if (tabs.length === 0) {
43
+ return;
44
+ }
45
+
46
+ event.preventDefault();
47
+
48
+ const currentIndex = Math.max(
49
+ 0,
50
+ tabs.findIndex((tab) => tab.getAttribute('aria-selected') === 'true')
51
+ );
52
+ const nextIndex = getNextIndex(event.key, currentIndex, tabs.length);
53
+ const nextTab = tabs[nextIndex];
54
+
55
+ nextTab.focus();
56
+ nextTab.click();
57
+ }
58
+
59
+ function getNextIndex(key: string, currentIndex: number, length: number): number {
60
+ if (key === 'Home') {
61
+ return 0;
62
+ }
63
+
64
+ if (key === 'End') {
65
+ return length - 1;
66
+ }
67
+
68
+ if (key === 'ArrowLeft') {
69
+ return (currentIndex - 1 + length) % length;
70
+ }
71
+
72
+ return (currentIndex + 1) % length;
73
+ }
74
+
75
+ $: resolvedIdPrefix = toDomId(idPrefix ?? fallbackIdPrefix);
76
+
77
+ function tabId(id: string): string {
78
+ return `${resolvedIdPrefix}-tab-${toDomId(id)}`;
79
+ }
80
+
81
+ function panelId(id: string): string {
82
+ return `${resolvedIdPrefix}-panel-${toDomId(id)}`;
83
+ }
84
+
85
+ function toDomId(id: string): string {
86
+ return id.trim().replace(/[^a-zA-Z0-9_-]+/g, '-') || 'item';
87
+ }
88
+ </script>
89
+
90
+ <div class="zdp-tabs">
91
+ <div
92
+ class="zdp-tabs__list"
93
+ role="tablist"
94
+ aria-label={ariaLabel}
95
+ tabindex="-1"
96
+ onkeydown={handleKeydown}
97
+ >
98
+ {#each items as item}
99
+ <button
100
+ class={`zdp-tabs__tab ${item.id === activeId ? 'zdp-tabs__tab--active' : ''}`}
101
+ id={tabId(item.id)}
102
+ type="button"
103
+ role="tab"
104
+ aria-selected={item.id === activeId}
105
+ aria-controls={panelId(item.id)}
106
+ tabindex={item.id === activeId ? 0 : -1}
107
+ disabled={item.disabled}
108
+ onclick={() => selectTab(item)}
109
+ >
110
+ {item.label}
111
+ </button>
112
+ {/each}
113
+ </div>
114
+
115
+ {#if selectedItem}
116
+ <div
117
+ class="zdp-tabs__panel"
118
+ id={panelId(selectedItem.id)}
119
+ role="tabpanel"
120
+ aria-labelledby={tabId(selectedItem.id)}
121
+ tabindex="0"
122
+ >
123
+ <slot selectedId={selectedItem.id} selectedItem={selectedItem} />
124
+ </div>
125
+ {/if}
126
+ </div>
127
+
128
+ <style>
129
+ .zdp-tabs {
130
+ display: grid;
131
+ gap: var(--zdp-space-3);
132
+ min-width: 0;
133
+ }
134
+
135
+ .zdp-tabs__list {
136
+ align-items: center;
137
+ border-bottom: 1px solid var(--zdp-color-line-subtle);
138
+ display: flex;
139
+ flex-wrap: wrap;
140
+ gap: var(--zdp-space-1);
141
+ min-width: 0;
142
+ }
143
+
144
+ .zdp-tabs__tab {
145
+ align-items: center;
146
+ background: transparent;
147
+ border: 0;
148
+ border-bottom: var(--zdp-control-border-width) solid transparent;
149
+ border-radius: var(--zdp-control-radius) var(--zdp-control-radius) 0 0;
150
+ color: var(--zdp-color-ink-muted);
151
+ cursor: pointer;
152
+ display: inline-flex;
153
+ font-family: var(--zdp-font-family-sans);
154
+ font-size: var(--zdp-type-control-size);
155
+ font-weight: var(--zdp-font-weight-medium);
156
+ justify-content: center;
157
+ line-height: var(--zdp-type-control-line-height);
158
+ min-height: var(--zdp-control-height-md);
159
+ padding: 0 var(--zdp-space-3);
160
+ transition:
161
+ background-color var(--zdp-motion-fast) ease,
162
+ border-color var(--zdp-motion-fast) ease,
163
+ color var(--zdp-motion-fast) ease;
164
+ -webkit-user-select: none;
165
+ user-select: none;
166
+ }
167
+
168
+ .zdp-tabs__tab:hover:not(:disabled) {
169
+ background: var(--zdp-color-surface-raised);
170
+ color: var(--zdp-color-ink-strong);
171
+ }
172
+
173
+ .zdp-tabs__tab--active {
174
+ border-bottom-color: var(--zdp-color-accent-primary-strong);
175
+ color: var(--zdp-color-ink-strong);
176
+ }
177
+
178
+ .zdp-tabs__tab:focus-visible {
179
+ border-color: var(--zdp-color-focus-line);
180
+ outline: var(--zdp-control-focus-outline-width) solid var(--zdp-color-focus-surface);
181
+ outline-offset: var(--zdp-control-focus-outline-offset);
182
+ }
183
+
184
+ .zdp-tabs__tab:disabled {
185
+ cursor: not-allowed;
186
+ opacity: 0.56;
187
+ }
188
+
189
+ .zdp-tabs__panel {
190
+ background: var(--zdp-color-surface-panel);
191
+ border: 1px solid var(--zdp-color-line-subtle);
192
+ border-radius: var(--zdp-control-radius);
193
+ color: var(--zdp-color-ink-normal);
194
+ display: grid;
195
+ gap: var(--zdp-space-2);
196
+ min-width: 0;
197
+ padding: var(--zdp-space-4);
198
+ }
199
+
200
+ .zdp-tabs__panel:focus-visible {
201
+ border-color: var(--zdp-color-focus-line);
202
+ outline: var(--zdp-control-focus-outline-width) solid var(--zdp-color-focus-surface);
203
+ outline-offset: var(--zdp-control-focus-outline-offset);
204
+ }
205
+ </style>