@happyvertical/smrt-ui 0.51.38 → 0.52.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 +14 -2
- package/README.md +257 -5
- package/dist/actions/__tests__/swipe-dismiss.test.js +69 -0
- package/dist/actions/swipe-dismiss.d.ts +42 -0
- package/dist/actions/swipe-dismiss.d.ts.map +1 -0
- package/dist/actions/swipe-dismiss.js +69 -0
- package/dist/components/calendar/Calendar.svelte +5 -0
- package/dist/components/calendar/Calendar.svelte.d.ts.map +1 -1
- package/dist/components/calendar/CalendarView.svelte +1250 -0
- package/dist/components/calendar/CalendarView.svelte.d.ts +66 -0
- package/dist/components/calendar/CalendarView.svelte.d.ts.map +1 -0
- package/dist/components/calendar/DayView.svelte +5 -0
- package/dist/components/calendar/DayView.svelte.d.ts.map +1 -1
- package/dist/components/calendar/__tests__/CalendarView.test.js +226 -0
- package/dist/components/calendar/__tests__/calendar-model.test.js +138 -0
- package/dist/components/calendar/calendar-model.d.ts +178 -0
- package/dist/components/calendar/calendar-model.d.ts.map +1 -0
- package/dist/components/calendar/calendar-model.js +362 -0
- package/dist/components/calendar/index.d.ts +12 -1
- package/dist/components/calendar/index.d.ts.map +1 -1
- package/dist/components/calendar/index.js +11 -1
- package/dist/components/data/CollectionList.svelte +23 -5
- package/dist/components/data/CollectionList.svelte.d.ts +8 -2
- package/dist/components/data/CollectionList.svelte.d.ts.map +1 -1
- package/dist/components/data/DataTable.svelte +118 -40
- package/dist/components/data/DataTable.svelte.d.ts.map +1 -1
- package/dist/components/data/DataTableController.d.ts +4 -0
- package/dist/components/data/DataTableController.d.ts.map +1 -1
- package/dist/components/data/DataTableController.js +11 -4
- package/dist/components/data/ListSortSelect.svelte +121 -0
- package/dist/components/data/ListSortSelect.svelte.d.ts +22 -0
- package/dist/components/data/ListSortSelect.svelte.d.ts.map +1 -0
- package/dist/components/data/SortableHeader.svelte +169 -0
- package/dist/components/data/SortableHeader.svelte.d.ts +30 -0
- package/dist/components/data/SortableHeader.svelte.d.ts.map +1 -0
- package/dist/components/data/__tests__/CollectionList.test.js +37 -0
- package/dist/components/data/__tests__/DataTable.test.js +76 -0
- package/dist/components/data/__tests__/DataTableController.test.js +35 -0
- package/dist/components/data/__tests__/SortableHeader.test.js +101 -0
- package/dist/components/data/__tests__/link-step-surface.test.js +172 -0
- package/dist/components/data/__tests__/list-sort-state.svelte.test.js +71 -0
- package/dist/components/data/__tests__/list-sort.test.js +91 -0
- package/dist/components/data/__tests__/surface-navigation.test.js +121 -0
- package/dist/components/data/data-surface.d.ts +1 -0
- package/dist/components/data/data-surface.d.ts.map +1 -1
- package/dist/components/data/data-surface.js +1 -0
- package/dist/components/data/index.d.ts +6 -0
- package/dist/components/data/index.d.ts.map +1 -1
- package/dist/components/data/index.js +6 -0
- package/dist/components/data/link-surface.d.ts +79 -0
- package/dist/components/data/link-surface.d.ts.map +1 -0
- package/dist/components/data/link-surface.js +155 -0
- package/dist/components/data/list-sort-state.svelte.d.ts +39 -0
- package/dist/components/data/list-sort-state.svelte.d.ts.map +1 -0
- package/dist/components/data/list-sort-state.svelte.js +47 -0
- package/dist/components/data/list-sort.d.ts +94 -0
- package/dist/components/data/list-sort.d.ts.map +1 -0
- package/dist/components/data/list-sort.js +182 -0
- package/dist/components/data/step-surface.d.ts +73 -0
- package/dist/components/data/step-surface.d.ts.map +1 -0
- package/dist/components/data/step-surface.js +177 -0
- package/dist/components/data/surface-navigation.d.ts +49 -0
- package/dist/components/data/surface-navigation.d.ts.map +1 -0
- package/dist/components/data/surface-navigation.js +100 -0
- package/dist/components/data/types.d.ts +19 -0
- package/dist/components/data/types.d.ts.map +1 -1
- package/dist/components/feedback/PhoneSheet.svelte +224 -0
- package/dist/components/feedback/PhoneSheet.svelte.d.ts +35 -0
- package/dist/components/feedback/PhoneSheet.svelte.d.ts.map +1 -0
- package/dist/components/feedback/WorkingStrip.svelte +426 -0
- package/dist/components/feedback/WorkingStrip.svelte.d.ts +37 -0
- package/dist/components/feedback/WorkingStrip.svelte.d.ts.map +1 -0
- package/dist/components/feedback/__tests__/PhoneSheet.test.js +101 -0
- package/dist/components/feedback/__tests__/WorkingStrip.test.js +172 -0
- package/dist/components/feedback/index.d.ts +4 -0
- package/dist/components/feedback/index.d.ts.map +1 -1
- package/dist/components/feedback/index.js +5 -0
- package/dist/components/feedback/working-status.d.ts +34 -0
- package/dist/components/feedback/working-status.d.ts.map +1 -0
- package/dist/components/feedback/working-status.js +5 -0
- package/dist/components/forms/Checkbox.svelte +26 -2
- package/dist/components/forms/Combobox.svelte +68 -12
- package/dist/components/forms/Combobox.svelte.d.ts +6 -0
- package/dist/components/forms/Combobox.svelte.d.ts.map +1 -1
- package/dist/components/forms/DictationButton.svelte +128 -0
- package/dist/components/forms/DictationButton.svelte.d.ts +19 -0
- package/dist/components/forms/DictationButton.svelte.d.ts.map +1 -0
- package/dist/components/forms/DictationStatus.svelte +164 -0
- package/dist/components/forms/DictationStatus.svelte.d.ts +11 -0
- package/dist/components/forms/DictationStatus.svelte.d.ts.map +1 -0
- package/dist/components/forms/Fieldset.svelte +5 -1
- package/dist/components/forms/Fieldset.svelte.d.ts +2 -0
- package/dist/components/forms/Fieldset.svelte.d.ts.map +1 -1
- package/dist/components/forms/FormActionBar.svelte +84 -0
- package/dist/components/forms/FormActionBar.svelte.d.ts +23 -0
- package/dist/components/forms/FormActionBar.svelte.d.ts.map +1 -0
- package/dist/components/forms/Input.svelte +9 -0
- package/dist/components/forms/Radio.svelte +15 -4
- package/dist/components/forms/SearchInput.svelte +207 -0
- package/dist/components/forms/SearchInput.svelte.d.ts +22 -0
- package/dist/components/forms/SearchInput.svelte.d.ts.map +1 -0
- package/dist/components/forms/Select.svelte +9 -0
- package/dist/components/forms/StagedControlReview.svelte +21 -12
- package/dist/components/forms/StagedControlReview.svelte.d.ts.map +1 -1
- package/dist/components/forms/Switch.svelte +11 -5
- package/dist/components/forms/TagsInput.svelte +8 -1
- package/dist/components/forms/Textarea.svelte +8 -0
- package/dist/components/forms/__tests__/Combobox.test.js +87 -0
- package/dist/components/forms/__tests__/ControlHitArea.test.js +73 -0
- package/dist/components/forms/__tests__/DictationButton.test.js +176 -0
- package/dist/components/forms/__tests__/Fieldset.test.js +37 -0
- package/dist/components/forms/__tests__/FormActionBar.test.js +22 -0
- package/dist/components/forms/__tests__/SearchInput.test.js +92 -0
- package/dist/components/forms/__tests__/StagedControlReview.test.js +37 -3
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte +19 -0
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts +11 -0
- package/dist/components/forms/__tests__/combobox-labels.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte +29 -0
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/composite-registration.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/control-interaction.test.js +19 -1
- package/dist/components/forms/__tests__/control-proposals.test.js +63 -0
- package/dist/components/forms/__tests__/dictation-recording.test.js +582 -0
- package/dist/components/forms/__tests__/dictation.test.js +486 -0
- package/dist/components/forms/__tests__/long-press.test.js +175 -0
- package/dist/components/forms/__tests__/place-picker.fixture.svelte +33 -0
- package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts +12 -0
- package/dist/components/forms/__tests__/place-picker.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/audio-capture.d.ts +69 -0
- package/dist/components/forms/audio-capture.d.ts.map +1 -0
- package/dist/components/forms/audio-capture.js +193 -0
- package/dist/components/forms/control-interaction.d.ts +6 -0
- package/dist/components/forms/control-interaction.d.ts.map +1 -1
- package/dist/components/forms/control-interaction.js +12 -1
- package/dist/components/forms/control-proposals.d.ts +55 -0
- package/dist/components/forms/control-proposals.d.ts.map +1 -0
- package/dist/components/forms/control-proposals.js +188 -0
- package/dist/components/forms/dictation-transcribe.d.ts +36 -0
- package/dist/components/forms/dictation-transcribe.d.ts.map +1 -0
- package/dist/components/forms/dictation-transcribe.js +65 -0
- package/dist/components/forms/dictation.svelte.d.ts +200 -0
- package/dist/components/forms/dictation.svelte.d.ts.map +1 -0
- package/dist/components/forms/dictation.svelte.js +686 -0
- package/dist/components/forms/index.d.ts +14 -1
- package/dist/components/forms/index.d.ts.map +1 -1
- package/dist/components/forms/index.js +14 -1
- package/dist/components/forms/insert-text.d.ts +12 -0
- package/dist/components/forms/insert-text.d.ts.map +1 -0
- package/dist/components/forms/insert-text.js +38 -0
- package/dist/components/forms/long-press.d.ts +88 -0
- package/dist/components/forms/long-press.d.ts.map +1 -0
- package/dist/components/forms/long-press.js +213 -0
- package/dist/components/forms/ready-beep.d.ts +24 -0
- package/dist/components/forms/ready-beep.d.ts.map +1 -0
- package/dist/components/forms/ready-beep.js +82 -0
- package/dist/components/layout/PageHeader.svelte +151 -17
- package/dist/components/layout/PageHeader.svelte.d.ts +47 -7
- package/dist/components/layout/PageHeader.svelte.d.ts.map +1 -1
- package/dist/components/layout/__tests__/PageHeader.test.js +87 -1
- package/dist/components/layout/index.d.ts +1 -0
- package/dist/components/layout/index.d.ts.map +1 -1
- package/dist/components/layout/index.js +1 -0
- package/dist/components/layout/page-header-context.d.ts +25 -0
- package/dist/components/layout/page-header-context.d.ts.map +1 -0
- package/dist/components/layout/page-header-context.js +16 -0
- package/dist/components/nav/Tabs.svelte +241 -0
- package/dist/components/nav/Tabs.svelte.d.ts +15 -0
- package/dist/components/nav/Tabs.svelte.d.ts.map +1 -1
- package/dist/components/nav/__tests__/LinkTabs.test.js +117 -0
- package/dist/components/nav/index.d.ts +1 -0
- package/dist/components/nav/index.d.ts.map +1 -1
- package/dist/components/nav/index.js +1 -0
- package/dist/components/nav/tabs-overflow.d.ts +12 -0
- package/dist/components/nav/tabs-overflow.d.ts.map +1 -0
- package/dist/components/nav/tabs-overflow.js +17 -0
- package/dist/components/nav/types.d.ts +8 -0
- package/dist/components/nav/types.d.ts.map +1 -1
- package/dist/components/ui/Button.svelte +7 -0
- package/dist/components/ui/Card.svelte +14 -5
- package/dist/components/ui/Dropdown.svelte +55 -1
- package/dist/components/ui/Dropdown.svelte.d.ts +8 -0
- package/dist/components/ui/Dropdown.svelte.d.ts.map +1 -1
- package/dist/components/ui/IconToggle.svelte +139 -0
- package/dist/components/ui/IconToggle.svelte.d.ts +32 -0
- package/dist/components/ui/IconToggle.svelte.d.ts.map +1 -0
- package/dist/components/ui/IconToggleGroup.svelte +56 -0
- package/dist/components/ui/IconToggleGroup.svelte.d.ts +21 -0
- package/dist/components/ui/IconToggleGroup.svelte.d.ts.map +1 -0
- package/dist/components/ui/Pagination.svelte +1 -1
- package/dist/components/ui/Popover.svelte +17 -1
- package/dist/components/ui/Popover.svelte.d.ts +8 -0
- package/dist/components/ui/Popover.svelte.d.ts.map +1 -1
- package/dist/components/ui/__tests__/IconToggle.test.js +59 -0
- package/dist/components/ui/__tests__/IconToggleGroup.test.js +30 -0
- package/dist/components/ui/__tests__/Popover.test.js +44 -0
- package/dist/components/ui/__tests__/dropdown-popover-icon.test.js +125 -0
- package/dist/components/ui/__tests__/phone-touch-targets.test.js +39 -0
- package/dist/components/ui/index.d.ts +2 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +2 -0
- package/dist/i18n/strings.ui.d.ts +41 -0
- package/dist/i18n/strings.ui.d.ts.map +1 -1
- package/dist/i18n/strings.ui.js +46 -0
- package/dist/svelte/__tests__/playground.test.js +1 -0
- package/dist/svelte/playground/CalendarPreview.svelte +140 -0
- package/dist/svelte/playground/CalendarPreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/CalendarPreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground.d.ts.map +1 -1
- package/dist/svelte/playground.js +10 -0
- package/dist/types-generic.d.ts +2 -0
- package/dist/types-generic.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -10,9 +10,19 @@
|
|
|
10
10
|
* - Proper ARIA roles: tablist, tab, tabpanel
|
|
11
11
|
* - aria-selected indicates active tab
|
|
12
12
|
* - aria-controls links tab to panel
|
|
13
|
+
*
|
|
14
|
+
* Link tabs: give every tab an `href` and the row renders as navigation — a
|
|
15
|
+
* `<nav>` of links with `aria-current="page"` on the active one, each tab its
|
|
16
|
+
* own URL (a content item's Article / Images / Review pages). With
|
|
17
|
+
* `maxVisible`, extra link tabs move to a "More" menu while the active tab
|
|
18
|
+
* always stays in the row; `badge` marks a tab needing attention (and puts a
|
|
19
|
+
* dot on "More" when a hidden tab has one). The row scrolls sideways on
|
|
20
|
+
* phones and keeps the active tab scrolled into view. It carries
|
|
21
|
+
* `data-shell-tabs`, so AdminShell keeps it sticky under its phone top bar.
|
|
13
22
|
*/
|
|
14
23
|
import type { Snippet } from 'svelte';
|
|
15
24
|
import { ripple } from '../../actions/ripple.js';
|
|
25
|
+
import { splitTabs } from './tabs-overflow.js';
|
|
16
26
|
import type { Tab } from './types.js';
|
|
17
27
|
|
|
18
28
|
export interface Props {
|
|
@@ -30,6 +40,12 @@ export interface Props {
|
|
|
30
40
|
children?: Snippet;
|
|
31
41
|
/** Accessible label for the tablist */
|
|
32
42
|
'aria-label'?: string;
|
|
43
|
+
/** Link tabs only: most tabs shown in the row; the rest go under "More". */
|
|
44
|
+
maxVisible?: number;
|
|
45
|
+
/** Link tabs only: label of the overflow menu (default "More"). */
|
|
46
|
+
moreLabel?: string;
|
|
47
|
+
/** Link tabs only: screen-reader text for the "More" attention dot. */
|
|
48
|
+
moreBadgeLabel?: string;
|
|
33
49
|
}
|
|
34
50
|
|
|
35
51
|
const {
|
|
@@ -40,8 +56,39 @@ const {
|
|
|
40
56
|
variant = 'primary',
|
|
41
57
|
children,
|
|
42
58
|
'aria-label': ariaLabel,
|
|
59
|
+
maxVisible,
|
|
60
|
+
moreLabel = 'More',
|
|
61
|
+
moreBadgeLabel = 'has updates',
|
|
43
62
|
}: Props = $props();
|
|
44
63
|
|
|
64
|
+
/** Link tabs: every tab has an href, so the row is navigation, not a tablist. */
|
|
65
|
+
const linkMode = $derived(tabs.length > 0 && tabs.every((tab) => !!tab.href));
|
|
66
|
+
const split = $derived(
|
|
67
|
+
linkMode
|
|
68
|
+
? splitTabs(tabs, active, maxVisible)
|
|
69
|
+
: { visible: tabs, overflow: [] as Tab[] },
|
|
70
|
+
);
|
|
71
|
+
// Same rule as the tab's own badge, so an empty badge dots neither.
|
|
72
|
+
const overflowHasBadge = $derived(split.overflow.some((tab) => hasBadge(tab)));
|
|
73
|
+
let moreMenu = $state<HTMLDetailsElement | null>(null);
|
|
74
|
+
let linkRow = $state<HTMLElement | null>(null);
|
|
75
|
+
|
|
76
|
+
// Keep the active link tab scrolled into view in a sideways-scrolling row,
|
|
77
|
+
// and close "More" once the active tab changes (a link was followed).
|
|
78
|
+
$effect(() => {
|
|
79
|
+
const current = active;
|
|
80
|
+
if (!linkMode || !linkRow) return;
|
|
81
|
+
if (moreMenu) moreMenu.open = false;
|
|
82
|
+
const activeLink = linkRow.querySelector<HTMLElement>(
|
|
83
|
+
`[data-tab-id="${CSS.escape(current)}"]`,
|
|
84
|
+
);
|
|
85
|
+
activeLink?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
function hasBadge(tab: Tab): boolean {
|
|
89
|
+
return tab.badge !== undefined && tab.badge !== null && tab.badge !== '';
|
|
90
|
+
}
|
|
91
|
+
|
|
45
92
|
/** Get enabled tabs only */
|
|
46
93
|
const enabledTabs = $derived(tabs.filter((t) => !t.disabled));
|
|
47
94
|
|
|
@@ -51,6 +98,9 @@ let tablistEl: HTMLElement | null = $state(null);
|
|
|
51
98
|
const instanceId = $props.id();
|
|
52
99
|
|
|
53
100
|
function handleClick(id: string) {
|
|
101
|
+
// A disabled tab (a link one has no href but is still clickable) never
|
|
102
|
+
// changes the active tab.
|
|
103
|
+
if (tabs.find((tab) => tab.id === id)?.disabled) return;
|
|
54
104
|
if (id !== active) {
|
|
55
105
|
onchange?.(id);
|
|
56
106
|
}
|
|
@@ -101,6 +151,70 @@ function handleKeydown(event: KeyboardEvent, tabId: string) {
|
|
|
101
151
|
}
|
|
102
152
|
</script>
|
|
103
153
|
|
|
154
|
+
{#snippet linkTab(tab: Tab)}
|
|
155
|
+
<a
|
|
156
|
+
class="tab-link"
|
|
157
|
+
class:active={tab.id === active}
|
|
158
|
+
href={tab.disabled ? undefined : tab.href}
|
|
159
|
+
aria-current={tab.id === active ? 'page' : undefined}
|
|
160
|
+
aria-disabled={tab.disabled ? 'true' : undefined}
|
|
161
|
+
data-tab-id={tab.id}
|
|
162
|
+
onclick={(event) => {
|
|
163
|
+
if (tab.disabled) {
|
|
164
|
+
event.preventDefault();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
handleClick(tab.id);
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
<span class="tab-label">{tab.label}</span>
|
|
171
|
+
{#if tab.count !== undefined}
|
|
172
|
+
<span class="tab-count">{tab.count}</span>
|
|
173
|
+
{/if}
|
|
174
|
+
{#if hasBadge(tab)}
|
|
175
|
+
<span class="tab-badge">{tab.badge}</span>
|
|
176
|
+
{/if}
|
|
177
|
+
</a>
|
|
178
|
+
{/snippet}
|
|
179
|
+
|
|
180
|
+
{#if linkMode}
|
|
181
|
+
<div class="tabs-container">
|
|
182
|
+
<nav
|
|
183
|
+
class="tabs-links"
|
|
184
|
+
class:sm={size === 'sm'}
|
|
185
|
+
class:lg={size === 'lg'}
|
|
186
|
+
aria-label={ariaLabel}
|
|
187
|
+
data-shell-tabs
|
|
188
|
+
>
|
|
189
|
+
<div class="tabs-links-row" bind:this={linkRow}>
|
|
190
|
+
{#each split.visible as tab (tab.id)}
|
|
191
|
+
{@render linkTab(tab)}
|
|
192
|
+
{/each}
|
|
193
|
+
</div>
|
|
194
|
+
{#if split.overflow.length > 0}
|
|
195
|
+
<details class="tabs-more" bind:this={moreMenu}>
|
|
196
|
+
<summary class="tab-link">
|
|
197
|
+
<span class="tab-label">{moreLabel}</span>
|
|
198
|
+
{#if overflowHasBadge}
|
|
199
|
+
<span class="tab-dot" aria-hidden="true"></span>
|
|
200
|
+
<span class="visually-hidden">({moreBadgeLabel})</span>
|
|
201
|
+
{/if}
|
|
202
|
+
</summary>
|
|
203
|
+
<div class="tabs-more-menu">
|
|
204
|
+
{#each split.overflow as tab (tab.id)}
|
|
205
|
+
{@render linkTab(tab)}
|
|
206
|
+
{/each}
|
|
207
|
+
</div>
|
|
208
|
+
</details>
|
|
209
|
+
{/if}
|
|
210
|
+
</nav>
|
|
211
|
+
{#if children}
|
|
212
|
+
<div class="tab-panel">
|
|
213
|
+
{@render children()}
|
|
214
|
+
</div>
|
|
215
|
+
{/if}
|
|
216
|
+
</div>
|
|
217
|
+
{:else}
|
|
104
218
|
<div class="tabs-container">
|
|
105
219
|
<div
|
|
106
220
|
class="tabs-nav"
|
|
@@ -132,6 +246,9 @@ function handleKeydown(event: KeyboardEvent, tabId: string) {
|
|
|
132
246
|
{#if tab.count !== undefined}
|
|
133
247
|
<span class="tab-count">{tab.count}</span>
|
|
134
248
|
{/if}
|
|
249
|
+
{#if hasBadge(tab)}
|
|
250
|
+
<span class="tab-badge">{tab.badge}</span>
|
|
251
|
+
{/if}
|
|
135
252
|
</span>
|
|
136
253
|
<div class="active-indicator"></div>
|
|
137
254
|
</button>
|
|
@@ -149,6 +266,7 @@ function handleKeydown(event: KeyboardEvent, tabId: string) {
|
|
|
149
266
|
</div>
|
|
150
267
|
{/if}
|
|
151
268
|
</div>
|
|
269
|
+
{/if}
|
|
152
270
|
|
|
153
271
|
<style>
|
|
154
272
|
.tabs-container {
|
|
@@ -249,4 +367,127 @@ function handleKeydown(event: KeyboardEvent, tabId: string) {
|
|
|
249
367
|
.tab-panel {
|
|
250
368
|
padding-top: 1.5rem;
|
|
251
369
|
}
|
|
370
|
+
|
|
371
|
+
.tab-badge {
|
|
372
|
+
min-width: 1.25rem;
|
|
373
|
+
padding: 0.05rem 0.4rem;
|
|
374
|
+
border-radius: 999px;
|
|
375
|
+
background: var(--smrt-color-surface-container-high);
|
|
376
|
+
color: var(--smrt-color-on-surface);
|
|
377
|
+
font: var(--smrt-typography-label-small-font);
|
|
378
|
+
font-weight: var(--smrt-typography-weight-bold, 700);
|
|
379
|
+
text-align: center;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/* Link tabs: a navigation row, one URL per tab. */
|
|
383
|
+
.tabs-links {
|
|
384
|
+
display: flex;
|
|
385
|
+
align-items: stretch;
|
|
386
|
+
gap: var(--smrt-spacing-1, 4px);
|
|
387
|
+
width: 100%;
|
|
388
|
+
border-bottom: 1px solid var(--smrt-color-outline-variant);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.tabs-links-row {
|
|
392
|
+
display: flex;
|
|
393
|
+
flex: 1 1 auto;
|
|
394
|
+
min-width: 0;
|
|
395
|
+
gap: var(--smrt-spacing-1, 4px);
|
|
396
|
+
overflow-x: auto;
|
|
397
|
+
scrollbar-width: thin;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.tab-link {
|
|
401
|
+
display: inline-flex;
|
|
402
|
+
flex: 0 0 auto;
|
|
403
|
+
align-items: center;
|
|
404
|
+
gap: var(--smrt-spacing-2, 8px);
|
|
405
|
+
min-height: 44px;
|
|
406
|
+
padding: 0 var(--smrt-spacing-4, 16px);
|
|
407
|
+
margin-bottom: -1px;
|
|
408
|
+
border-bottom: 2px solid transparent;
|
|
409
|
+
color: var(--smrt-color-on-surface-variant);
|
|
410
|
+
font: var(--smrt-typography-title-small-font);
|
|
411
|
+
font-weight: var(--smrt-typography-weight-semibold, 600);
|
|
412
|
+
text-decoration: none;
|
|
413
|
+
white-space: nowrap;
|
|
414
|
+
cursor: pointer;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.tabs-links.sm .tab-link {
|
|
418
|
+
min-height: 40px;
|
|
419
|
+
font: var(--smrt-typography-label-large-font);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.tabs-links.lg .tab-link {
|
|
423
|
+
min-height: 56px;
|
|
424
|
+
font: var(--smrt-typography-title-medium-font);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.tab-link:hover {
|
|
428
|
+
color: var(--smrt-color-on-surface);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.tab-link.active {
|
|
432
|
+
border-bottom-color: var(--smrt-color-primary);
|
|
433
|
+
color: var(--smrt-color-primary);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.tab-link[aria-disabled='true'] {
|
|
437
|
+
opacity: 0.38;
|
|
438
|
+
cursor: not-allowed;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.tab-link:focus-visible {
|
|
442
|
+
outline: 2px solid var(--smrt-color-primary);
|
|
443
|
+
outline-offset: -2px;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.tab-dot {
|
|
447
|
+
width: 0.5rem;
|
|
448
|
+
height: 0.5rem;
|
|
449
|
+
border-radius: 999px;
|
|
450
|
+
background: var(--smrt-color-primary);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.tabs-more {
|
|
454
|
+
position: relative;
|
|
455
|
+
flex: 0 0 auto;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.tabs-more summary {
|
|
459
|
+
list-style: none;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.tabs-more summary::-webkit-details-marker {
|
|
463
|
+
display: none;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.tabs-more-menu {
|
|
467
|
+
position: absolute;
|
|
468
|
+
inset-inline-end: 0;
|
|
469
|
+
z-index: var(--smrt-z-index-dropdown, 20);
|
|
470
|
+
display: grid;
|
|
471
|
+
min-width: 12rem;
|
|
472
|
+
padding: var(--smrt-spacing-1, 4px);
|
|
473
|
+
border: 1px solid var(--smrt-color-outline-variant);
|
|
474
|
+
border-radius: var(--smrt-radius-small, 0.5rem);
|
|
475
|
+
background: var(--smrt-color-surface);
|
|
476
|
+
box-shadow: var(--smrt-elevation-2);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.tabs-more-menu .tab-link {
|
|
480
|
+
margin: 0;
|
|
481
|
+
border-bottom: 0;
|
|
482
|
+
border-radius: var(--smrt-radius-extra-small, 0.35rem);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.visually-hidden {
|
|
486
|
+
position: absolute;
|
|
487
|
+
width: 1px;
|
|
488
|
+
height: 1px;
|
|
489
|
+
overflow: hidden;
|
|
490
|
+
clip: rect(0 0 0 0);
|
|
491
|
+
white-space: nowrap;
|
|
492
|
+
}
|
|
252
493
|
</style>
|
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
* - Proper ARIA roles: tablist, tab, tabpanel
|
|
10
10
|
* - aria-selected indicates active tab
|
|
11
11
|
* - aria-controls links tab to panel
|
|
12
|
+
*
|
|
13
|
+
* Link tabs: give every tab an `href` and the row renders as navigation — a
|
|
14
|
+
* `<nav>` of links with `aria-current="page"` on the active one, each tab its
|
|
15
|
+
* own URL (a content item's Article / Images / Review pages). With
|
|
16
|
+
* `maxVisible`, extra link tabs move to a "More" menu while the active tab
|
|
17
|
+
* always stays in the row; `badge` marks a tab needing attention (and puts a
|
|
18
|
+
* dot on "More" when a hidden tab has one). The row scrolls sideways on
|
|
19
|
+
* phones and keeps the active tab scrolled into view. It carries
|
|
20
|
+
* `data-shell-tabs`, so AdminShell keeps it sticky under its phone top bar.
|
|
12
21
|
*/
|
|
13
22
|
import type { Snippet } from 'svelte';
|
|
14
23
|
import type { Tab } from './types.js';
|
|
@@ -27,6 +36,12 @@ export interface Props {
|
|
|
27
36
|
children?: Snippet;
|
|
28
37
|
/** Accessible label for the tablist */
|
|
29
38
|
'aria-label'?: string;
|
|
39
|
+
/** Link tabs only: most tabs shown in the row; the rest go under "More". */
|
|
40
|
+
maxVisible?: number;
|
|
41
|
+
/** Link tabs only: label of the overflow menu (default "More"). */
|
|
42
|
+
moreLabel?: string;
|
|
43
|
+
/** Link tabs only: screen-reader text for the "More" attention dot. */
|
|
44
|
+
moreBadgeLabel?: string;
|
|
30
45
|
}
|
|
31
46
|
declare const Tabs: import("svelte").Component<Props, {}, "">;
|
|
32
47
|
type Tabs = ReturnType<typeof Tabs>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Tabs.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/nav/Tabs.svelte.ts"],"names":[],"mappings":"AAGA
|
|
1
|
+
{"version":3,"file":"Tabs.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/nav/Tabs.svelte.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAGtC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAGtC,MAAM,WAAW,KAAK;IACpB,qBAAqB;IACrB,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,8BAA8B;IAC9B,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,mBAAmB;IACnB,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,qBAAqB;IACrB,OAAO,CAAC,EAAE,SAAS,GAAG,WAAW,CAAC;IAClC,uBAAuB;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uCAAuC;IACvC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4EAA4E;IAC5E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mEAAmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AA+LD,QAAA,MAAM,IAAI,2CAAwC,CAAC;AACnD,KAAK,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;AACpC,eAAe,IAAI,CAAC"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/** Link tabs: a navigation row of URLs with overflow, badges, active-in-view. */
|
|
2
|
+
import { render, screen, within } from '@testing-library/svelte';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { expectNoA11yViolations } from '../../../test-support/a11y';
|
|
6
|
+
import Tabs from '../Tabs.svelte';
|
|
7
|
+
import { splitTabs } from '../tabs-overflow.js';
|
|
8
|
+
const TABS = [
|
|
9
|
+
{ id: 'article', label: 'Article', href: '/c/1' },
|
|
10
|
+
{ id: 'images', label: 'Images', href: '/c/1/images', badge: 3 },
|
|
11
|
+
{ id: 'videos', label: 'Videos', href: '/c/1/videos' },
|
|
12
|
+
{ id: 'social', label: 'Social', href: '/c/1/social' },
|
|
13
|
+
{ id: 'review', label: 'Review', href: '/c/1/review', badge: '!' },
|
|
14
|
+
{ id: 'jobs', label: 'Background work', href: '/c/1/jobs' },
|
|
15
|
+
];
|
|
16
|
+
describe('splitTabs', () => {
|
|
17
|
+
it('keeps everything in the row under the limit', () => {
|
|
18
|
+
expect(splitTabs(TABS, 'article', 10).overflow).toEqual([]);
|
|
19
|
+
expect(splitTabs(TABS, 'article', undefined).overflow).toEqual([]);
|
|
20
|
+
});
|
|
21
|
+
it('promotes the active tab into the row', () => {
|
|
22
|
+
const { visible, overflow } = splitTabs(TABS, 'jobs', 4);
|
|
23
|
+
expect(visible.map((tab) => tab.id)).toEqual([
|
|
24
|
+
'article',
|
|
25
|
+
'images',
|
|
26
|
+
'videos',
|
|
27
|
+
'jobs',
|
|
28
|
+
]);
|
|
29
|
+
expect(overflow.map((tab) => tab.id)).toEqual(['social', 'review']);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe('Tabs with hrefs', () => {
|
|
33
|
+
it('renders navigation links, not a tablist, with aria-current on the active tab', () => {
|
|
34
|
+
render(Tabs, {
|
|
35
|
+
props: { tabs: TABS, active: 'images', 'aria-label': 'Content sections' },
|
|
36
|
+
});
|
|
37
|
+
const nav = screen.getByRole('navigation', { name: 'Content sections' });
|
|
38
|
+
expect(nav).toHaveAttribute('data-shell-tabs');
|
|
39
|
+
expect(screen.queryByRole('tablist')).not.toBeInTheDocument();
|
|
40
|
+
const active = within(nav).getByRole('link', { name: /Images/ });
|
|
41
|
+
expect(active).toHaveAttribute('href', '/c/1/images');
|
|
42
|
+
expect(active).toHaveAttribute('aria-current', 'page');
|
|
43
|
+
expect(active).toHaveTextContent('3');
|
|
44
|
+
expect(within(nav).getByRole('link', { name: 'Article' })).not.toHaveAttribute('aria-current');
|
|
45
|
+
});
|
|
46
|
+
it('moves extra tabs under "More", keeps the active one visible, and dots the menu for a hidden badge', async () => {
|
|
47
|
+
const { container } = render(Tabs, {
|
|
48
|
+
props: {
|
|
49
|
+
tabs: TABS,
|
|
50
|
+
active: 'jobs',
|
|
51
|
+
maxVisible: 4,
|
|
52
|
+
'aria-label': 'Content sections',
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
const row = container.querySelector('.tabs-links-row');
|
|
56
|
+
expect(within(row)
|
|
57
|
+
.getAllByRole('link')
|
|
58
|
+
.map((link) => link.dataset.tabId)).toEqual(['article', 'images', 'videos', 'jobs']);
|
|
59
|
+
const more = container.querySelector('details.tabs-more');
|
|
60
|
+
expect(within(more).getByText('More')).toBeInTheDocument();
|
|
61
|
+
expect(within(more).getByText('(has updates)')).toBeInTheDocument();
|
|
62
|
+
await userEvent.click(within(more).getByText('More'));
|
|
63
|
+
expect(more.open).toBe(true);
|
|
64
|
+
expect(within(more).getByRole('link', { name: /Review/ })).toHaveAttribute('href', '/c/1/review');
|
|
65
|
+
});
|
|
66
|
+
it('a disabled link tab never fires onchange', async () => {
|
|
67
|
+
const onchange = vi.fn();
|
|
68
|
+
render(Tabs, {
|
|
69
|
+
props: {
|
|
70
|
+
tabs: [
|
|
71
|
+
{ id: 'article', label: 'Article', href: '/c/1' },
|
|
72
|
+
{
|
|
73
|
+
id: 'locked',
|
|
74
|
+
label: 'Locked',
|
|
75
|
+
href: '/c/1/locked',
|
|
76
|
+
disabled: true,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
active: 'article',
|
|
80
|
+
onchange,
|
|
81
|
+
'aria-label': 'Content sections',
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
const locked = screen.getByText('Locked').closest('a');
|
|
85
|
+
await userEvent.click(locked);
|
|
86
|
+
expect(onchange).not.toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
it('does not dot "More" for an empty-string badge, matching the tab itself', () => {
|
|
89
|
+
const { container } = render(Tabs, {
|
|
90
|
+
props: {
|
|
91
|
+
tabs: [
|
|
92
|
+
{ id: 'a', label: 'A', href: '/a' },
|
|
93
|
+
{ id: 'b', label: 'B', href: '/b' },
|
|
94
|
+
{ id: 'c', label: 'C', href: '/c', badge: '' },
|
|
95
|
+
],
|
|
96
|
+
active: 'a',
|
|
97
|
+
maxVisible: 2,
|
|
98
|
+
'aria-label': 'Sections',
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
const more = container.querySelector('details.tabs-more');
|
|
102
|
+
expect(more).not.toBeNull();
|
|
103
|
+
expect(more.querySelector('.tab-dot')).toBeNull();
|
|
104
|
+
expect(more.querySelector('.tab-badge')).toBeNull();
|
|
105
|
+
});
|
|
106
|
+
it('is axe-clean', async () => {
|
|
107
|
+
const { container } = render(Tabs, {
|
|
108
|
+
props: {
|
|
109
|
+
tabs: TABS,
|
|
110
|
+
active: 'article',
|
|
111
|
+
maxVisible: 4,
|
|
112
|
+
'aria-label': 'Content sections',
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
await expectNoA11yViolations(container);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/nav/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/nav/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE/C,YAAY,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split tabs into the row and a "More" menu, keeping the active tab in the
|
|
3
|
+
* row: with more tabs than `maxVisible`, the first `maxVisible - 1` show, then
|
|
4
|
+
* the active tab (or the next one), and the rest go to the menu.
|
|
5
|
+
*/
|
|
6
|
+
export declare function splitTabs<T extends {
|
|
7
|
+
id: string;
|
|
8
|
+
}>(tabs: readonly T[], activeId: string | null | undefined, maxVisible: number | undefined): {
|
|
9
|
+
visible: T[];
|
|
10
|
+
overflow: T[];
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=tabs-overflow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tabs-overflow.d.ts","sourceRoot":"","sources":["../../../src/components/nav/tabs-overflow.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAChD,IAAI,EAAE,SAAS,CAAC,EAAE,EAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACnC,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B;IAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IAAC,QAAQ,EAAE,CAAC,EAAE,CAAA;CAAE,CAWjC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split tabs into the row and a "More" menu, keeping the active tab in the
|
|
3
|
+
* row: with more tabs than `maxVisible`, the first `maxVisible - 1` show, then
|
|
4
|
+
* the active tab (or the next one), and the rest go to the menu.
|
|
5
|
+
*/
|
|
6
|
+
export function splitTabs(tabs, activeId, maxVisible) {
|
|
7
|
+
if (!maxVisible || maxVisible < 1 || tabs.length <= maxVisible) {
|
|
8
|
+
return { visible: [...tabs], overflow: [] };
|
|
9
|
+
}
|
|
10
|
+
const visible = tabs.slice(0, maxVisible - 1);
|
|
11
|
+
const rest = tabs.slice(maxVisible - 1);
|
|
12
|
+
const promoted = rest.find((tab) => tab.id === activeId) ?? rest[0];
|
|
13
|
+
return {
|
|
14
|
+
visible: promoted ? [...visible, promoted] : visible,
|
|
15
|
+
overflow: rest.filter((tab) => tab !== promoted),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -20,5 +20,13 @@ export interface Tab {
|
|
|
20
20
|
count?: number;
|
|
21
21
|
/** Disabled state */
|
|
22
22
|
disabled?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Makes this a link tab: the tab row renders as navigation (`<nav>` of
|
|
25
|
+
* links, `aria-current="page"` on the active one) instead of a tablist.
|
|
26
|
+
* Give every tab an `href` or none.
|
|
27
|
+
*/
|
|
28
|
+
href?: string;
|
|
29
|
+
/** Short attention badge (e.g. `3` pending, `!`); `null`/absent shows none. */
|
|
30
|
+
badge?: string | number | null;
|
|
23
31
|
}
|
|
24
32
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/nav/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,YAAY;IAC3B,mBAAmB;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,oBAAoB;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,GAAG;IAClB,qBAAqB;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,oBAAoB;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/nav/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,YAAY;IAC3B,mBAAmB;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,oBAAoB;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,GAAG;IAClB,qBAAqB;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,oBAAoB;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CAChC"}
|
|
@@ -295,6 +295,13 @@ function handleClick(event: MouseEvent) {
|
|
|
295
295
|
outline-offset: 2px;
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
/* Phones: every size is at least a 44px touch target. */
|
|
299
|
+
@media (max-width: 48rem) {
|
|
300
|
+
.button {
|
|
301
|
+
min-block-size: 2.75rem;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
298
305
|
/* Reduced motion */
|
|
299
306
|
@media (prefers-reduced-motion: reduce) {
|
|
300
307
|
.spinner {
|
|
@@ -52,15 +52,24 @@ const {
|
|
|
52
52
|
</div>
|
|
53
53
|
|
|
54
54
|
<style>
|
|
55
|
+
/*
|
|
56
|
+
* Themeable look (app-level custom properties; defaults keep the stock look):
|
|
57
|
+
* --smrt-card-border edge of default and elevated cards
|
|
58
|
+
* --smrt-card-background card fill
|
|
59
|
+
* --smrt-card-shadow shadow of default cards (elevated keeps its elevation)
|
|
60
|
+
* --smrt-card-divider rule between the header/footer and the content
|
|
61
|
+
* The `outlined` variant always draws its outline.
|
|
62
|
+
*/
|
|
55
63
|
.card {
|
|
56
|
-
background: var(--smrt-color-surface);
|
|
64
|
+
background: var(--smrt-card-background, var(--smrt-color-surface));
|
|
57
65
|
border-radius: var(--smrt-radius-medium);
|
|
58
66
|
transition: all var(--smrt-duration-short2) var(--smrt-easing-standard);
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
/* Variants */
|
|
62
70
|
.default {
|
|
63
|
-
border: 1px solid var(--smrt-color-outline-variant);
|
|
71
|
+
border: var(--smrt-card-border, 1px solid var(--smrt-color-outline-variant));
|
|
72
|
+
box-shadow: var(--smrt-card-shadow, none);
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
.outlined {
|
|
@@ -68,7 +77,7 @@ const {
|
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
.elevated {
|
|
71
|
-
border: 1px solid var(--smrt-color-outline-variant);
|
|
80
|
+
border: var(--smrt-card-border, 1px solid var(--smrt-color-outline-variant));
|
|
72
81
|
box-shadow: var(--smrt-elevation-2);
|
|
73
82
|
}
|
|
74
83
|
|
|
@@ -98,7 +107,7 @@ const {
|
|
|
98
107
|
/* Header */
|
|
99
108
|
.card-header {
|
|
100
109
|
padding: var(--smrt-spacing-6);
|
|
101
|
-
border-bottom: 1px solid var(--smrt-color-outline-variant);
|
|
110
|
+
border-bottom: var(--smrt-card-divider, 1px solid var(--smrt-color-outline-variant));
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
.padding-sm .card-header {
|
|
@@ -112,7 +121,7 @@ const {
|
|
|
112
121
|
/* Footer */
|
|
113
122
|
.card-footer {
|
|
114
123
|
padding: var(--smrt-spacing-6);
|
|
115
|
-
border-top: 1px solid var(--smrt-color-outline-variant);
|
|
124
|
+
border-top: var(--smrt-card-divider, 1px solid var(--smrt-color-outline-variant));
|
|
116
125
|
background: var(--smrt-color-surface-container-low);
|
|
117
126
|
border-bottom-left-radius: var(--smrt-radius-medium);
|
|
118
127
|
border-bottom-right-radius: var(--smrt-radius-medium);
|