zabi-components 8.0.0 → 8.1.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/CHANGELOG.md +1924 -0
- package/README.md +243 -10
- package/THEME.md +149 -332
- package/THEMING.md +1192 -266
- package/dist/atoms/ActionPanel.svelte +15 -5
- package/dist/atoms/Avatar.svelte +104 -0
- package/dist/atoms/Avatar.svelte.d.ts +34 -0
- package/dist/atoms/Badge.svelte +36 -7
- package/dist/atoms/Badge.svelte.d.ts +2 -2
- package/dist/atoms/Button.svelte +177 -27
- package/dist/atoms/Button.svelte.d.ts +30 -2
- package/dist/atoms/Card.svelte +4 -4
- package/dist/atoms/CardHeader.svelte +16 -4
- package/dist/atoms/Checkbox.svelte +48 -21
- package/dist/atoms/Checkbox.svelte.d.ts +4 -2
- package/dist/atoms/CodeBlock.svelte +20 -2
- package/dist/atoms/CodeBlock.svelte.d.ts +4 -0
- package/dist/atoms/ColorPicker.svelte +259 -49
- package/dist/atoms/ColorPicker.svelte.d.ts +3 -0
- package/dist/atoms/Container.svelte +16 -8
- package/dist/atoms/DateField.svelte +107 -0
- package/dist/atoms/DateField.svelte.d.ts +53 -0
- package/dist/atoms/FieldMessages.svelte +69 -0
- package/dist/atoms/FieldMessages.svelte.d.ts +23 -0
- package/dist/atoms/FloatingActionButton.svelte +174 -0
- package/dist/atoms/FloatingActionButton.svelte.d.ts +51 -0
- package/dist/atoms/Heading.svelte +37 -5
- package/dist/atoms/Heading.svelte.d.ts +7 -0
- package/dist/atoms/IconButton.svelte +220 -24
- package/dist/atoms/IconButton.svelte.d.ts +49 -4
- package/dist/atoms/Input.svelte +253 -52
- package/dist/atoms/Input.svelte.d.ts +32 -2
- package/dist/atoms/List.svelte +4 -1
- package/dist/atoms/ListItem.svelte +44 -11
- package/dist/atoms/ListItem.svelte.d.ts +5 -0
- package/dist/atoms/Progress.svelte +27 -8
- package/dist/atoms/Progress.svelte.d.ts +4 -0
- package/dist/atoms/Radio.svelte +19 -3
- package/dist/atoms/Radio.svelte.d.ts +4 -2
- package/dist/atoms/Rating.svelte +494 -0
- package/dist/atoms/Rating.svelte.d.ts +56 -0
- package/dist/atoms/Select.svelte +433 -91
- package/dist/atoms/Select.svelte.d.ts +49 -3
- package/dist/atoms/SelectionControl.svelte +94 -17
- package/dist/atoms/SelectionControl.svelte.d.ts +4 -3
- package/dist/atoms/Skeleton.svelte +1 -1
- package/dist/atoms/Slider.svelte +309 -0
- package/dist/atoms/Slider.svelte.d.ts +40 -0
- package/dist/atoms/Spinner.svelte +57 -0
- package/dist/atoms/Spinner.svelte.d.ts +19 -0
- package/dist/atoms/Table.svelte +151 -6
- package/dist/atoms/Table.svelte.d.ts +15 -3
- package/dist/atoms/TemporalField.svelte +140 -0
- package/dist/atoms/TemporalField.svelte.d.ts +32 -0
- package/dist/atoms/Text.svelte +23 -11
- package/dist/atoms/Text.svelte.d.ts +6 -3
- package/dist/atoms/Textarea.svelte +55 -44
- package/dist/atoms/Textarea.svelte.d.ts +14 -2
- package/dist/atoms/ThemeToggle.svelte +223 -44
- package/dist/atoms/ThemeToggle.svelte.d.ts +26 -3
- package/dist/atoms/TimeField.svelte +108 -0
- package/dist/atoms/TimeField.svelte.d.ts +54 -0
- package/dist/atoms/Toast.svelte +17 -4
- package/dist/atoms/Toast.svelte.d.ts +2 -0
- package/dist/atoms/Toggle.svelte +41 -8
- package/dist/atoms/Toggle.svelte.d.ts +11 -2
- package/dist/atoms/Tooltip.svelte +508 -23
- package/dist/atoms/Tooltip.svelte.d.ts +11 -0
- package/dist/atoms/ZabiStringsProvider.svelte +45 -0
- package/dist/atoms/ZabiStringsProvider.svelte.d.ts +27 -0
- package/dist/atoms/index.d.ts +17 -0
- package/dist/atoms/index.js +12 -0
- package/dist/atoms/selection-control.styles.d.ts +36 -5
- package/dist/atoms/selection-control.styles.js +40 -9
- package/dist/components/atoms/index.d.ts +17 -0
- package/dist/components/atoms/index.d.ts.map +1 -1
- package/dist/components/atoms/selection-control.styles.d.ts +36 -5
- package/dist/components/atoms/selection-control.styles.d.ts.map +1 -1
- package/dist/components/index.d.ts +80 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/molecules/index.d.ts +46 -0
- package/dist/components/molecules/index.d.ts.map +1 -1
- package/dist/components/molecules/toast-store.d.ts +53 -7
- package/dist/components/molecules/toast-store.d.ts.map +1 -1
- package/dist/components/organisms/index.d.ts +5 -0
- package/dist/components/organisms/index.d.ts.map +1 -1
- package/dist/components/types/variants.d.ts +15 -1
- package/dist/components/types/variants.d.ts.map +1 -1
- package/dist/components/util/app-shell.d.ts +28 -0
- package/dist/components/util/app-shell.d.ts.map +1 -0
- package/dist/components/util/avatar.d.ts +36 -0
- package/dist/components/util/avatar.d.ts.map +1 -0
- package/dist/components/util/bottom-sheet.d.ts +30 -0
- package/dist/components/util/bottom-sheet.d.ts.map +1 -0
- package/dist/components/util/bottom-tab-bar.d.ts +35 -0
- package/dist/components/util/bottom-tab-bar.d.ts.map +1 -0
- package/dist/components/util/calendar.d.ts +56 -0
- package/dist/components/util/calendar.d.ts.map +1 -0
- package/dist/components/util/collapsible.d.ts +58 -0
- package/dist/components/util/collapsible.d.ts.map +1 -0
- package/dist/components/util/confirm-dialog.d.ts +12 -0
- package/dist/components/util/confirm-dialog.d.ts.map +1 -0
- package/dist/components/util/date.d.ts +96 -0
- package/dist/components/util/date.d.ts.map +1 -0
- package/dist/components/util/drawer.d.ts +13 -0
- package/dist/components/util/drawer.d.ts.map +1 -0
- package/dist/components/util/dropdown.d.ts +47 -0
- package/dist/components/util/dropdown.d.ts.map +1 -0
- package/dist/components/util/field.d.ts +39 -0
- package/dist/components/util/field.d.ts.map +1 -0
- package/dist/components/util/fit-in-viewport.d.ts +201 -0
- package/dist/components/util/fit-in-viewport.d.ts.map +1 -0
- package/dist/components/util/focus-utils.d.ts +75 -2
- package/dist/components/util/focus-utils.d.ts.map +1 -1
- package/dist/components/util/hydration.d.ts +45 -0
- package/dist/components/util/hydration.d.ts.map +1 -0
- package/dist/components/util/keyboard-inset.d.ts +18 -0
- package/dist/components/util/keyboard-inset.d.ts.map +1 -0
- package/dist/components/util/media-grid.d.ts +51 -0
- package/dist/components/util/media-grid.d.ts.map +1 -0
- package/dist/components/util/media.d.ts +6 -0
- package/dist/components/util/media.d.ts.map +1 -0
- package/dist/components/util/overlay-chrome.d.ts +53 -0
- package/dist/components/util/overlay-chrome.d.ts.map +1 -0
- package/dist/components/util/overlay.d.ts +72 -0
- package/dist/components/util/overlay.d.ts.map +1 -0
- package/dist/components/util/photo.d.ts +215 -0
- package/dist/components/util/photo.d.ts.map +1 -0
- package/dist/components/util/portal.d.ts +23 -0
- package/dist/components/util/portal.d.ts.map +1 -0
- package/dist/components/util/pull-to-refresh.d.ts +27 -0
- package/dist/components/util/pull-to-refresh.d.ts.map +1 -0
- package/dist/components/util/radio-keys.d.ts +25 -0
- package/dist/components/util/radio-keys.d.ts.map +1 -0
- package/dist/components/util/rating.d.ts +23 -0
- package/dist/components/util/rating.d.ts.map +1 -0
- package/dist/components/util/ready-made-strings.d.ts +77 -0
- package/dist/components/util/ready-made-strings.d.ts.map +1 -0
- package/dist/components/util/scroll-padding.d.ts +20 -0
- package/dist/components/util/scroll-padding.d.ts.map +1 -0
- package/dist/components/util/segmented-control.d.ts +16 -0
- package/dist/components/util/segmented-control.d.ts.map +1 -0
- package/dist/components/util/select.d.ts +41 -0
- package/dist/components/util/select.d.ts.map +1 -0
- package/dist/components/util/sheet-drag.d.ts +60 -0
- package/dist/components/util/sheet-drag.d.ts.map +1 -0
- package/dist/components/util/sidebar.d.ts +63 -0
- package/dist/components/util/sidebar.d.ts.map +1 -0
- package/dist/components/util/sortable-list.d.ts +65 -0
- package/dist/components/util/sortable-list.d.ts.map +1 -0
- package/dist/components/util/stepper.d.ts +40 -0
- package/dist/components/util/stepper.d.ts.map +1 -0
- package/dist/components/util/swipeable-list-item.d.ts +36 -0
- package/dist/components/util/swipeable-list-item.d.ts.map +1 -0
- package/dist/components/util/theme-mode.d.ts +80 -0
- package/dist/components/util/theme-mode.d.ts.map +1 -0
- package/dist/components/util/toaster.d.ts +93 -0
- package/dist/components/util/toaster.d.ts.map +1 -0
- package/dist/components/util/tooltip.d.ts +10 -0
- package/dist/components/util/tooltip.d.ts.map +1 -0
- package/dist/components/util/top-navbar.d.ts +11 -0
- package/dist/components/util/top-navbar.d.ts.map +1 -0
- package/dist/components/util/touch-target.d.ts +31 -0
- package/dist/components/util/touch-target.d.ts.map +1 -0
- package/dist/components/util/zabi-strings.d.ts +133 -0
- package/dist/components/util/zabi-strings.d.ts.map +1 -0
- package/dist/create-theme/cli.js +169 -0
- package/dist/create-theme/index.d.ts +176 -0
- package/dist/create-theme/index.js +817 -0
- package/dist/create-theme/lib/ramp-math.js +128 -0
- package/dist/create-theme/lib/resolve.js +141 -0
- package/dist/create-theme/theme-data.js +1116 -0
- package/dist/index.d.ts +80 -2
- package/dist/index.js +57 -2
- package/dist/molecules/Alert.svelte +33 -5
- package/dist/molecules/Alert.svelte.d.ts +2 -0
- package/dist/molecules/AppBar.svelte +415 -0
- package/dist/molecules/AppBar.svelte.d.ts +69 -0
- package/dist/molecules/AvatarGroup.svelte +116 -0
- package/dist/molecules/AvatarGroup.svelte.d.ts +40 -0
- package/dist/molecules/BottomSheet.svelte +540 -0
- package/dist/molecules/BottomSheet.svelte.d.ts +93 -0
- package/dist/molecules/BottomTabBar.svelte +297 -0
- package/dist/molecules/BottomTabBar.svelte.d.ts +64 -0
- package/dist/molecules/Calendar.svelte +597 -0
- package/dist/molecules/Calendar.svelte.d.ts +72 -0
- package/dist/molecules/Collapsible.svelte +243 -0
- package/dist/molecules/Collapsible.svelte.d.ts +44 -0
- package/dist/molecules/CollapsibleGroup.svelte +126 -0
- package/dist/molecules/CollapsibleGroup.svelte.d.ts +14 -0
- package/dist/molecules/ComponentDemo.svelte +16 -4
- package/dist/molecules/ComponentDemo.svelte.d.ts +3 -0
- package/dist/molecules/ConfirmDialog.svelte +253 -0
- package/dist/molecules/ConfirmDialog.svelte.d.ts +55 -0
- package/dist/molecules/ContactForm.svelte +34 -17
- package/dist/molecules/ContactForm.svelte.d.ts +8 -0
- package/dist/molecules/Drawer.svelte +394 -0
- package/dist/molecules/Drawer.svelte.d.ts +55 -0
- package/dist/molecules/Dropdown.svelte +509 -48
- package/dist/molecules/Dropdown.svelte.d.ts +45 -6
- package/dist/molecules/DropdownItem.svelte +122 -0
- package/dist/molecules/DropdownItem.svelte.d.ts +28 -0
- package/dist/molecules/EmptyState.svelte +55 -10
- package/dist/molecules/EmptyState.svelte.d.ts +8 -0
- package/dist/molecules/FormField.svelte +9 -1
- package/dist/molecules/FormField.svelte.d.ts +2 -0
- package/dist/molecules/Header.svelte +1 -1
- package/dist/molecules/ImageUpload.svelte +383 -70
- package/dist/molecules/ImageUpload.svelte.d.ts +47 -2
- package/dist/molecules/MediaGrid.svelte +527 -0
- package/dist/molecules/MediaGrid.svelte.d.ts +77 -0
- package/dist/molecules/Modal.svelte +288 -72
- package/dist/molecules/Modal.svelte.d.ts +51 -2
- package/dist/molecules/NavigationMenuContent.svelte +58 -1
- package/dist/molecules/NavigationMenuItem.svelte +0 -1
- package/dist/molecules/NavigationMenuLink.svelte +1 -1
- package/dist/molecules/NavigationMenuList.svelte +10 -3
- package/dist/molecules/NavigationMenuTrigger.svelte +2 -2
- package/dist/molecules/Page.svelte +32 -2
- package/dist/molecules/Page.svelte.d.ts +11 -0
- package/dist/molecules/PhotoGrid.svelte +433 -0
- package/dist/molecules/PhotoGrid.svelte.d.ts +62 -0
- package/dist/molecules/PhotoViewer.svelte +1116 -0
- package/dist/molecules/PhotoViewer.svelte.d.ts +56 -0
- package/dist/molecules/PropsTable.svelte +22 -10
- package/dist/molecules/PropsTable.svelte.d.ts +3 -0
- package/dist/molecules/PullToRefresh.svelte +263 -0
- package/dist/molecules/PullToRefresh.svelte.d.ts +52 -0
- package/dist/molecules/RadioGroup.svelte +52 -14
- package/dist/molecules/RadioGroup.svelte.d.ts +14 -3
- package/dist/molecules/Section.svelte +1 -1
- package/dist/molecules/SegmentedControl.svelte +323 -0
- package/dist/molecules/SegmentedControl.svelte.d.ts +34 -0
- package/dist/molecules/SidebarBrandHeader.svelte +11 -1
- package/dist/molecules/SidebarBrandHeader.svelte.d.ts +3 -0
- package/dist/molecules/SidebarFooter.svelte +29 -6
- package/dist/molecules/SidebarFooter.svelte.d.ts +3 -0
- package/dist/molecules/SlideUp.svelte +220 -61
- package/dist/molecules/SlideUp.svelte.d.ts +26 -0
- package/dist/molecules/SortableList.svelte +632 -0
- package/dist/molecules/SortableList.svelte.d.ts +69 -0
- package/dist/molecules/Stepper.svelte +548 -0
- package/dist/molecules/Stepper.svelte.d.ts +39 -0
- package/dist/molecules/StickyActionBar.svelte +191 -0
- package/dist/molecules/StickyActionBar.svelte.d.ts +65 -0
- package/dist/molecules/SwipeableListItem.svelte +334 -0
- package/dist/molecules/SwipeableListItem.svelte.d.ts +54 -0
- package/dist/molecules/Tabs.svelte +233 -9
- package/dist/molecules/Tabs.svelte.d.ts +7 -0
- package/dist/molecules/Toaster.svelte +350 -5
- package/dist/molecules/Toaster.svelte.d.ts +95 -2
- package/dist/molecules/ToasterToast.svelte +253 -67
- package/dist/molecules/ToasterToast.svelte.d.ts +9 -0
- package/dist/molecules/UnsavedChangesBar.svelte +212 -0
- package/dist/molecules/UnsavedChangesBar.svelte.d.ts +43 -0
- package/dist/molecules/index.d.ts +46 -0
- package/dist/molecules/index.js +27 -0
- package/dist/molecules/toast-store.d.ts +53 -7
- package/dist/molecules/toast-store.js +21 -0
- package/dist/organisms/AppShell.svelte +196 -0
- package/dist/organisms/AppShell.svelte.d.ts +61 -0
- package/dist/organisms/SidebarAccountPanel.svelte +107 -12
- package/dist/organisms/SidebarAccountPanel.svelte.d.ts +19 -0
- package/dist/organisms/SidebarNavigation.svelte +96 -15
- package/dist/organisms/SidebarNavigation.svelte.d.ts +29 -1
- package/dist/organisms/SidebarPanel.svelte +31 -10
- package/dist/organisms/SidebarShell.svelte +162 -11
- package/dist/organisms/SidebarShell.svelte.d.ts +56 -2
- package/dist/organisms/TopNavbar.svelte +256 -31
- package/dist/organisms/TopNavbar.svelte.d.ts +26 -0
- package/dist/organisms/index.d.ts +5 -0
- package/dist/organisms/index.js +3 -0
- package/dist/types/components.d.ts +4 -0
- package/dist/types/components.d.ts.map +1 -1
- package/dist/types/index.d.ts +605 -25
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/variants.ts +17 -1
- package/dist/util/app-shell.d.ts +27 -0
- package/dist/util/app-shell.js +72 -0
- package/dist/util/avatar.d.ts +35 -0
- package/dist/util/avatar.js +52 -0
- package/dist/util/bottom-sheet.d.ts +29 -0
- package/dist/util/bottom-sheet.js +43 -0
- package/dist/util/bottom-tab-bar.d.ts +34 -0
- package/dist/util/bottom-tab-bar.js +54 -0
- package/dist/util/calendar.d.ts +55 -0
- package/dist/util/calendar.js +74 -0
- package/dist/util/collapsible.d.ts +57 -0
- package/dist/util/collapsible.js +23 -0
- package/dist/util/confirm-dialog.d.ts +11 -0
- package/dist/util/confirm-dialog.js +2 -0
- package/dist/util/date.d.ts +95 -0
- package/dist/util/date.js +237 -0
- package/dist/util/drawer.d.ts +12 -0
- package/dist/util/drawer.js +7 -0
- package/dist/util/dropdown.d.ts +46 -0
- package/dist/util/dropdown.js +21 -0
- package/dist/util/field.d.ts +38 -0
- package/dist/util/field.js +38 -0
- package/dist/util/fit-in-viewport.d.ts +200 -0
- package/dist/util/fit-in-viewport.js +399 -0
- package/dist/util/focus-utils.d.ts +75 -2
- package/dist/util/focus-utils.js +182 -9
- package/dist/util/hydration.d.ts +44 -0
- package/dist/util/hydration.js +58 -0
- package/dist/util/keyboard-inset.d.ts +17 -0
- package/dist/util/keyboard-inset.js +53 -0
- package/dist/util/media-grid.d.ts +50 -0
- package/dist/util/media-grid.js +81 -0
- package/dist/util/media.d.ts +5 -0
- package/dist/util/media.js +10 -0
- package/dist/util/overlay-chrome.d.ts +52 -0
- package/dist/util/overlay-chrome.js +56 -0
- package/dist/util/overlay.d.ts +71 -0
- package/dist/util/overlay.js +240 -0
- package/dist/util/photo.d.ts +214 -0
- package/dist/util/photo.js +206 -0
- package/dist/util/portal.d.ts +22 -0
- package/dist/util/portal.js +50 -0
- package/dist/util/pull-to-refresh.d.ts +26 -0
- package/dist/util/pull-to-refresh.js +30 -0
- package/dist/util/radio-keys.d.ts +24 -0
- package/dist/util/radio-keys.js +49 -0
- package/dist/util/rating.d.ts +22 -0
- package/dist/util/rating.js +26 -0
- package/dist/util/ready-made-strings.d.ts +76 -0
- package/dist/util/ready-made-strings.js +67 -0
- package/dist/util/scroll-padding.d.ts +19 -0
- package/dist/util/scroll-padding.js +44 -0
- package/dist/util/segmented-control.d.ts +15 -0
- package/dist/util/segmented-control.js +1 -0
- package/dist/util/select.d.ts +40 -0
- package/dist/util/select.js +63 -0
- package/dist/util/sheet-drag.d.ts +59 -0
- package/dist/util/sheet-drag.js +241 -0
- package/dist/util/sidebar.d.ts +62 -0
- package/dist/util/sidebar.js +38 -0
- package/dist/util/sortable-list.d.ts +64 -0
- package/dist/util/sortable-list.js +53 -0
- package/dist/util/stepper.d.ts +39 -0
- package/dist/util/stepper.js +38 -0
- package/dist/util/swipeable-list-item.d.ts +35 -0
- package/dist/util/swipeable-list-item.js +32 -0
- package/dist/util/theme-mode.d.ts +79 -0
- package/dist/util/theme-mode.js +121 -0
- package/dist/util/toaster.d.ts +92 -0
- package/dist/util/toaster.js +75 -0
- package/dist/util/tooltip.d.ts +9 -0
- package/dist/util/tooltip.js +18 -0
- package/dist/util/top-navbar.d.ts +10 -0
- package/dist/util/top-navbar.js +5 -0
- package/dist/util/touch-target.d.ts +30 -0
- package/dist/util/touch-target.js +30 -0
- package/dist/util/zabi-strings.d.ts +132 -0
- package/dist/util/zabi-strings.js +105 -0
- package/dist/zabi-components-colors.css +5 -2
- package/dist/zabi-components-theme-dark-only.css +543 -241
- package/dist/zabi-components-theme-dark.css +543 -241
- package/dist/zabi-components-theme-only.css +1584 -367
- package/dist/zabi-components-theme.css +1584 -367
- package/dist/zabi-components.css +3885 -518
- package/docs/lucide-icons.md +40 -17
- package/docs/theme-imports.md +81 -11
- package/package.json +35 -8
|
@@ -49,15 +49,25 @@
|
|
|
49
49
|
);
|
|
50
50
|
|
|
51
51
|
const baseClasses =
|
|
52
|
-
"group focus-ring focus-ring--muted block rounded-container border border-border bg-card p-6 text-left transition-colors duration-150
|
|
52
|
+
"group focus-ring focus-ring--muted block rounded-container border border-border bg-card p-6 text-left transition-colors duration-150";
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
/**
|
|
55
|
+
* The hover and pressed tints are translucent, so they are drawn as a
|
|
56
|
+
* background image over the card fill rather than in place of it: swapping
|
|
57
|
+
* `bg-card` for a tint would let the page show through the panel. (A
|
|
58
|
+
* `hover:bg-*` beside `bg-card` also never applies; see THEMING.md, state
|
|
59
|
+
* variants.)
|
|
60
|
+
*/
|
|
61
|
+
const interactiveClasses =
|
|
62
|
+
"bg-linear-to-b from-transparent to-transparent hover:border-border-medium hover:from-surface-hover hover:to-surface-hover active:scale-[0.99] active:from-surface-active active:to-surface-active";
|
|
63
|
+
|
|
64
|
+
const disabledClasses = "opacity-50 cursor-not-allowed pointer-events-none";
|
|
56
65
|
|
|
57
66
|
const panelClasses = $derived(
|
|
58
|
-
cn(baseClasses, isDisabled ? disabledClasses :
|
|
67
|
+
cn(baseClasses, isDisabled ? disabledClasses : interactiveClasses, className),
|
|
59
68
|
);
|
|
60
69
|
|
|
70
|
+
|
|
61
71
|
function handleKeydown(event: KeyboardEvent) {
|
|
62
72
|
if (isDisabled) return;
|
|
63
73
|
if (!href && onclick && (event.key === "Enter" || event.key === " ")) {
|
|
@@ -83,7 +93,7 @@
|
|
|
83
93
|
{/if}
|
|
84
94
|
{#if loading}
|
|
85
95
|
<span
|
|
86
|
-
class="inline-block size-5 shrink-0 animate-spin rounded-full border-2 border-description border-t-transparent opacity-70"
|
|
96
|
+
class="inline-block size-5 shrink-0 animate-spin rounded-full border-2 border-description border-t-transparent opacity-70 motion-reduce:animate-pulse"
|
|
87
97
|
aria-hidden="true"
|
|
88
98
|
></span>
|
|
89
99
|
{/if}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import User from "@lucide/svelte/icons/user";
|
|
3
|
+
import { untrack } from "svelte";
|
|
4
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
5
|
+
import { initialsOf, type AvatarSize } from "../util/avatar.js";
|
|
6
|
+
import { cn } from "../util/cn.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A round picture of a person, which falls back to their initials.
|
|
10
|
+
*
|
|
11
|
+
* It is one image with one name, the person's: the picture, the initials
|
|
12
|
+
* and the icon inside are presentational. Not interactive: put it in a
|
|
13
|
+
* link or a button of your own.
|
|
14
|
+
*
|
|
15
|
+
* Other attributes (`id`, `data-*`, ...) land on the root element.
|
|
16
|
+
*/
|
|
17
|
+
type Props = Omit<HTMLAttributes<HTMLSpanElement>, "class" | "role" | "aria-label"> & {
|
|
18
|
+
/** The person's name: the accessible name, and where the initials come from. */
|
|
19
|
+
name: string;
|
|
20
|
+
/** Address of the picture. Without it, or when it fails to load, the initials show. */
|
|
21
|
+
src?: string;
|
|
22
|
+
/** Diameter: 24, 32 and 48px. It does not grow with the text size. */
|
|
23
|
+
size?: "sm" | "md" | "lg";
|
|
24
|
+
/**
|
|
25
|
+
* Accessible name, when it should differ from `name`. `alt=""` makes
|
|
26
|
+
* the avatar decorative: for a name that is printed beside it.
|
|
27
|
+
*/
|
|
28
|
+
alt?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Language the initials are upper-cased in ("tr" turns "i" into "İ").
|
|
31
|
+
* The page's own (`<html lang>`) by default, which the server cannot
|
|
32
|
+
* read: pass it where that matters.
|
|
33
|
+
*/
|
|
34
|
+
locale?: string;
|
|
35
|
+
/** Extra classes for the root element. */
|
|
36
|
+
class?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
let {
|
|
40
|
+
name,
|
|
41
|
+
src,
|
|
42
|
+
size = "md",
|
|
43
|
+
alt,
|
|
44
|
+
locale,
|
|
45
|
+
class: className = "",
|
|
46
|
+
...restProps
|
|
47
|
+
}: Props = $props();
|
|
48
|
+
|
|
49
|
+
const pageLocale = typeof document !== "undefined" ? document.documentElement.lang : "";
|
|
50
|
+
const initials = $derived(initialsOf(name, locale || pageLocale));
|
|
51
|
+
const label = $derived(alt ?? name);
|
|
52
|
+
|
|
53
|
+
/** The address that did not load. Another address gets its own try. */
|
|
54
|
+
let failedSrc = $state<string>();
|
|
55
|
+
const failed = $derived(!!src && failedSrc === src);
|
|
56
|
+
|
|
57
|
+
/** A picture that had already failed before the page hydrated fired its `error` to nobody. */
|
|
58
|
+
function settled(image: HTMLImageElement) {
|
|
59
|
+
if (image.complete && image.naturalWidth === 0) untrack(() => (failedSrc = src));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The circle and the type in it are in px: a face in a row of people is
|
|
63
|
+
// the same size whatever the text size is, and the initials stay inside.
|
|
64
|
+
const SIZES: Record<AvatarSize, string> = {
|
|
65
|
+
sm: "size-[24px] text-[10px]",
|
|
66
|
+
md: "size-[32px] text-[12px]",
|
|
67
|
+
lg: "size-[48px] text-[16px]",
|
|
68
|
+
};
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<!-- The subtle primary pair, as a selected pill tab has: held to 4.5:1 in both
|
|
72
|
+
themes by scripts/contrast-pairs.js. The border is for forced colours, where
|
|
73
|
+
the fill is gone and a transparent border is drawn. -->
|
|
74
|
+
<span
|
|
75
|
+
class={cn(
|
|
76
|
+
"relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-pill border border-transparent bg-action-primary-subtle align-middle leading-none font-semibold text-link select-none",
|
|
77
|
+
SIZES[size] ?? SIZES.md,
|
|
78
|
+
className,
|
|
79
|
+
)}
|
|
80
|
+
data-avatar
|
|
81
|
+
data-size={size}
|
|
82
|
+
role={label ? "img" : undefined}
|
|
83
|
+
aria-label={label || undefined}
|
|
84
|
+
aria-hidden={label ? undefined : "true"}
|
|
85
|
+
{...restProps}
|
|
86
|
+
>
|
|
87
|
+
<!-- Under the picture from the start, in the server's markup too: nothing
|
|
88
|
+
moves when the picture arrives, or when it does not. -->
|
|
89
|
+
{#if initials}
|
|
90
|
+
<span aria-hidden="true" data-avatar-initials>{initials}</span>
|
|
91
|
+
{:else}
|
|
92
|
+
<User class="size-[60%]" aria-hidden="true" />
|
|
93
|
+
{/if}
|
|
94
|
+
{#if src && !failed}
|
|
95
|
+
<img
|
|
96
|
+
{src}
|
|
97
|
+
alt=""
|
|
98
|
+
class="absolute inset-0 size-full object-cover"
|
|
99
|
+
draggable="false"
|
|
100
|
+
onerror={() => (failedSrc = src)}
|
|
101
|
+
{@attach settled}
|
|
102
|
+
/>
|
|
103
|
+
{/if}
|
|
104
|
+
</span>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
2
|
+
/**
|
|
3
|
+
* A round picture of a person, which falls back to their initials.
|
|
4
|
+
*
|
|
5
|
+
* It is one image with one name, the person's: the picture, the initials
|
|
6
|
+
* and the icon inside are presentational. Not interactive: put it in a
|
|
7
|
+
* link or a button of your own.
|
|
8
|
+
*
|
|
9
|
+
* Other attributes (`id`, `data-*`, ...) land on the root element.
|
|
10
|
+
*/
|
|
11
|
+
type Props = Omit<HTMLAttributes<HTMLSpanElement>, "class" | "role" | "aria-label"> & {
|
|
12
|
+
/** The person's name: the accessible name, and where the initials come from. */
|
|
13
|
+
name: string;
|
|
14
|
+
/** Address of the picture. Without it, or when it fails to load, the initials show. */
|
|
15
|
+
src?: string;
|
|
16
|
+
/** Diameter: 24, 32 and 48px. It does not grow with the text size. */
|
|
17
|
+
size?: "sm" | "md" | "lg";
|
|
18
|
+
/**
|
|
19
|
+
* Accessible name, when it should differ from `name`. `alt=""` makes
|
|
20
|
+
* the avatar decorative: for a name that is printed beside it.
|
|
21
|
+
*/
|
|
22
|
+
alt?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Language the initials are upper-cased in ("tr" turns "i" into "İ").
|
|
25
|
+
* The page's own (`<html lang>`) by default, which the server cannot
|
|
26
|
+
* read: pass it where that matters.
|
|
27
|
+
*/
|
|
28
|
+
locale?: string;
|
|
29
|
+
/** Extra classes for the root element. */
|
|
30
|
+
class?: string;
|
|
31
|
+
};
|
|
32
|
+
declare const Avatar: import("svelte").Component<Props, {}, "">;
|
|
33
|
+
type Avatar = ReturnType<typeof Avatar>;
|
|
34
|
+
export default Avatar;
|
package/dist/atoms/Badge.svelte
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
3
|
import type { HTMLAttributes } from "svelte/elements";
|
|
4
|
-
import
|
|
4
|
+
import Check from "@lucide/svelte/icons/check";
|
|
5
|
+
import TriangleAlert from "@lucide/svelte/icons/triangle-alert";
|
|
6
|
+
import X from "@lucide/svelte/icons/x";
|
|
7
|
+
import Info from "@lucide/svelte/icons/info";
|
|
8
|
+
import Zap from "@lucide/svelte/icons/zap";
|
|
5
9
|
import { cn } from "../util/cn.js";
|
|
6
10
|
import type {
|
|
7
|
-
|
|
11
|
+
BadgeVariant,
|
|
8
12
|
SizeVariant,
|
|
9
13
|
} from "../types/variants.js";
|
|
10
14
|
|
|
@@ -16,7 +20,7 @@
|
|
|
16
20
|
type BadgeEmphasis = "subtle" | "solid";
|
|
17
21
|
|
|
18
22
|
type Props = Omit<HTMLAttributes<HTMLSpanElement>, "class"> & {
|
|
19
|
-
variant?:
|
|
23
|
+
variant?: BadgeVariant;
|
|
20
24
|
size?: SizeVariant;
|
|
21
25
|
emphasis?: BadgeEmphasis;
|
|
22
26
|
/** Label text. Ignored when `children` is provided. */
|
|
@@ -37,10 +41,25 @@
|
|
|
37
41
|
...restProps
|
|
38
42
|
}: Props = $props();
|
|
39
43
|
|
|
44
|
+
/**
|
|
45
|
+
* 20, 24 and 28px tall for a label on one line, as a minimum: a label that
|
|
46
|
+
* does not fit wraps and the badge grows. It used to be a fixed height
|
|
47
|
+
* with `whitespace-nowrap`, and a long label beside a flexible name made
|
|
48
|
+
* the page wider than a phone at 200% text.
|
|
49
|
+
*
|
|
50
|
+
* The vertical padding is what is left of the height after one line of
|
|
51
|
+
* text and the 1px border on each side (16 + 2 + 2, 16 + 6 + 2 and
|
|
52
|
+
* 20 + 6 + 2), so a one-line badge is exactly as tall as it was.
|
|
53
|
+
*
|
|
54
|
+
* The corner is half of that one-line height, not the pill radius: for
|
|
55
|
+
* one line that is the same full round end, and a badge of two lines
|
|
56
|
+
* keeps the same corner and becomes a rounded rectangle, where a pill
|
|
57
|
+
* radius would have turned it into a lozenge with its text in the curve.
|
|
58
|
+
*/
|
|
40
59
|
const sizeClass = $derived.by(() => {
|
|
41
|
-
if (size === "sm") return { box: "h-5 px-2 gap-1 text-xs", icon: 12 };
|
|
42
|
-
if (size === "lg") return { box: "h-7 px-3 gap-2 text-sm", icon: 16 };
|
|
43
|
-
return { box: "h-6 px-2 gap-1 text-xs", icon: 14 };
|
|
60
|
+
if (size === "sm") return { box: "min-h-5 py-px px-2 gap-1 text-xs rounded-[0.625rem]", icon: 12 };
|
|
61
|
+
if (size === "lg") return { box: "min-h-7 py-[3px] px-3 gap-2 text-sm rounded-[0.875rem]", icon: 16 };
|
|
62
|
+
return { box: "min-h-6 py-[3px] px-2 gap-1 text-xs rounded-[0.75rem]", icon: 14 };
|
|
44
63
|
});
|
|
45
64
|
|
|
46
65
|
/**
|
|
@@ -72,6 +91,13 @@
|
|
|
72
91
|
return solid
|
|
73
92
|
? "bg-energetic text-card border-transparent"
|
|
74
93
|
: "bg-energetic-subtle text-energetic-text border-energetic-border";
|
|
94
|
+
// The app's second brand colour. Its solid label is the "on accent"
|
|
95
|
+
// role, not the card surface: an app with a light accent (a
|
|
96
|
+
// yellow) sets a dark label there, and white would be unreadable.
|
|
97
|
+
case "accent":
|
|
98
|
+
return solid
|
|
99
|
+
? "bg-accent text-on-accent border-transparent"
|
|
100
|
+
: "bg-accent-subtle text-accent-text border-accent-border";
|
|
75
101
|
case "neutral":
|
|
76
102
|
case "default":
|
|
77
103
|
default:
|
|
@@ -82,7 +108,10 @@
|
|
|
82
108
|
});
|
|
83
109
|
|
|
84
110
|
const badgeClasses = $derived(
|
|
85
|
-
|
|
111
|
+
// Never wider than what it is in, and its words break onto the next
|
|
112
|
+
// line before they push anything out; a word longer than the line
|
|
113
|
+
// breaks inside. The icon stays centred beside the lines.
|
|
114
|
+
cn(`inline-flex [max-inline-size:100%] min-w-0 items-center justify-center border font-medium [overflow-wrap:anywhere] ${sizeClass.box} ${variantClass} ${className}`),
|
|
86
115
|
);
|
|
87
116
|
|
|
88
117
|
/** One outline icon family, so the variants don't look like a rummage. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Snippet } from "svelte";
|
|
2
2
|
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
-
import type {
|
|
3
|
+
import type { BadgeVariant, SizeVariant } from "../types/variants.js";
|
|
4
4
|
/**
|
|
5
5
|
* `subtle` is the default because a page full of saturated fills is noise —
|
|
6
6
|
* a badge is a label, not a button. `solid` is there for the one badge on
|
|
@@ -8,7 +8,7 @@ import type { ExtendedSemanticVariant, SizeVariant } from "../types/variants.js"
|
|
|
8
8
|
*/
|
|
9
9
|
type BadgeEmphasis = "subtle" | "solid";
|
|
10
10
|
type Props = Omit<HTMLAttributes<HTMLSpanElement>, "class"> & {
|
|
11
|
-
variant?:
|
|
11
|
+
variant?: BadgeVariant;
|
|
12
12
|
size?: SizeVariant;
|
|
13
13
|
emphasis?: BadgeEmphasis;
|
|
14
14
|
/** Label text. Ignored when `children` is provided. */
|
package/dist/atoms/Button.svelte
CHANGED
|
@@ -1,12 +1,40 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type
|
|
3
|
-
import type { HTMLButtonAttributes } from "svelte/elements";
|
|
2
|
+
import { tick, untrack, type Snippet } from "svelte";
|
|
3
|
+
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
|
4
4
|
import type { ButtonVariant, SizeVariant } from "../types/variants.js";
|
|
5
5
|
import { cn } from "../util/cn.js";
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* One type for the button and the link, not a union of the two elements'
|
|
9
|
+
* attributes: that union is too large for TypeScript to carry through a
|
|
10
|
+
* wrapper such as a Storybook `Meta`. Event handlers are typed for the
|
|
11
|
+
* button; on a link they are called with the same event.
|
|
12
|
+
*/
|
|
7
13
|
type Props = Omit<HTMLButtonAttributes, "class"> & {
|
|
14
|
+
/**
|
|
15
|
+
* Where the link goes. Makes this an `<a>` that looks the same;
|
|
16
|
+
* without it, it is a `<button>`. A link cannot be disabled: while
|
|
17
|
+
* `disabled` or `loading` it has no `href`, is `aria-disabled` and
|
|
18
|
+
* does nothing when pressed. While `loading` it stays a Tab stop.
|
|
19
|
+
*/
|
|
20
|
+
href?: string;
|
|
21
|
+
/** With `href`: where the link opens. */
|
|
22
|
+
target?: HTMLAnchorAttributes["target"];
|
|
23
|
+
/** With `href`: the link's relationship, such as `noopener`. */
|
|
24
|
+
rel?: HTMLAnchorAttributes["rel"];
|
|
25
|
+
/** With `href`: download the address instead of opening it. */
|
|
26
|
+
download?: HTMLAnchorAttributes["download"];
|
|
27
|
+
hreflang?: HTMLAnchorAttributes["hreflang"];
|
|
28
|
+
referrerpolicy?: HTMLAnchorAttributes["referrerpolicy"];
|
|
29
|
+
ping?: HTMLAnchorAttributes["ping"];
|
|
8
30
|
variant?: ButtonVariant;
|
|
9
31
|
size?: SizeVariant;
|
|
32
|
+
/**
|
|
33
|
+
* Shows a spinner and makes the button unavailable without taking
|
|
34
|
+
* focus from it: `aria-disabled` and `aria-busy`, still a Tab stop,
|
|
35
|
+
* and a press does nothing. It is not `disabled`: match it in CSS
|
|
36
|
+
* with `[aria-busy="true"]`.
|
|
37
|
+
*/
|
|
10
38
|
loading?: boolean;
|
|
11
39
|
text?: string;
|
|
12
40
|
/** Stretch to the width of the container. */
|
|
@@ -22,7 +50,8 @@
|
|
|
22
50
|
size = "md",
|
|
23
51
|
disabled = false,
|
|
24
52
|
loading = false,
|
|
25
|
-
type
|
|
53
|
+
type,
|
|
54
|
+
href,
|
|
26
55
|
text = "",
|
|
27
56
|
fullWidth = false,
|
|
28
57
|
isFullWidth = false,
|
|
@@ -32,25 +61,85 @@
|
|
|
32
61
|
...restProps
|
|
33
62
|
}: Props = $props();
|
|
34
63
|
|
|
35
|
-
const isDisabled = $derived(disabled || loading);
|
|
64
|
+
const isDisabled = $derived(!!disabled || loading);
|
|
65
|
+
/**
|
|
66
|
+
* Loading without `disabled`: unavailable, and still where focus is.
|
|
67
|
+
*
|
|
68
|
+
* `disabled` on a button, or no `href` on a link, makes the element
|
|
69
|
+
* unfocusable, and the browser then drops focus on `<body>`: whoever
|
|
70
|
+
* pressed Save with the keyboard was at the top of the page when the
|
|
71
|
+
* save came back. So a loading control says it the ARIA way
|
|
72
|
+
* (`aria-disabled`, `aria-busy`), stays a Tab stop, and swallows the
|
|
73
|
+
* press. `disabled` is the platform's: out of the Tab order.
|
|
74
|
+
*/
|
|
75
|
+
const isBusy = $derived(loading && !disabled);
|
|
76
|
+
const isLink = $derived(href !== undefined && href !== null);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A loading link has no `href`, so it is a Tab stop only by `tabindex`.
|
|
80
|
+
* The browser takes focus from an element the moment it stops being
|
|
81
|
+
* focusable, so the two attributes must never both be missing: the
|
|
82
|
+
* `tabindex` goes on before the `href` comes off (their order in the
|
|
83
|
+
* markup), and when loading ends it stays until the `href` is back, one
|
|
84
|
+
* update longer.
|
|
85
|
+
*/
|
|
86
|
+
let keepsTabStop = $state(false);
|
|
87
|
+
$effect.pre(() => {
|
|
88
|
+
if (isBusy) {
|
|
89
|
+
keepsTabStop = true;
|
|
90
|
+
} else if (untrack(() => keepsTabStop)) {
|
|
91
|
+
void tick().then(() => (keepsTabStop = false));
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/** A text link in a sentence: `variant="link"` on a real link. */
|
|
96
|
+
const isInlineLink = $derived(isLink && variant === "link");
|
|
97
|
+
|
|
98
|
+
/** A disabled link has no `href`, but a pointer can still press it. */
|
|
99
|
+
function handleLinkClick(event: MouseEvent) {
|
|
100
|
+
if (isDisabled) {
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
event.stopPropagation();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
(onclick as ((event: MouseEvent) => void) | null | undefined)?.(event);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** A loading button is not `disabled`, so the press reaches it: it ends here, and submits nothing. */
|
|
109
|
+
function handleButtonClick(event: MouseEvent & { currentTarget: EventTarget & HTMLButtonElement }) {
|
|
110
|
+
if (isBusy) {
|
|
111
|
+
event.preventDefault();
|
|
112
|
+
event.stopPropagation();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
onclick?.(event);
|
|
116
|
+
}
|
|
36
117
|
|
|
37
118
|
/**
|
|
38
119
|
* One height scale, shared with Input, Select and IconButton, so controls
|
|
39
|
-
* of the same size line up in a row
|
|
40
|
-
*
|
|
41
|
-
* and
|
|
120
|
+
* of the same size line up in a row: 32 / 40 / 48px. It is a minimum
|
|
121
|
+
* height, not a fixed one, so a label that does not fit on one line wraps
|
|
122
|
+
* and the button grows; it used to paint outside the button (a long label
|
|
123
|
+
* at 200% text on a phone). One line of text and the vertical padding are
|
|
124
|
+
* never taller than the minimum, the outline variant's border included,
|
|
125
|
+
* so a label that fits is exactly on the scale:
|
|
126
|
+
* scripts/check-control-geometry.js holds the padding to that.
|
|
42
127
|
*
|
|
43
128
|
* Weight and tracking do NOT change with size: a large button used to drop
|
|
44
129
|
* to font-normal, so the biggest button had the lightest label.
|
|
45
130
|
*/
|
|
46
131
|
const sizeClass = $derived.by(() => {
|
|
132
|
+
// sm and md are 44px tall on a coarse pointer, like Input, Select and
|
|
133
|
+
// IconButton, so a row still lines up and the box is the touch target.
|
|
134
|
+
// No minimum width: text and padding make a button wide enough, and one
|
|
135
|
+
// would stop the actions of an ImageUpload shrinking in a narrow space.
|
|
47
136
|
if (size === "sm") {
|
|
48
|
-
return { box: "h-8 px-3", text: "text-sm", gap: "gap-2", spinner: "size-3.5" };
|
|
137
|
+
return { box: "min-h-8 px-3 py-1 pointer-coarse:min-h-11", text: "text-sm", gap: "gap-2", spinner: "size-3.5" };
|
|
49
138
|
}
|
|
50
139
|
if (size === "lg") {
|
|
51
|
-
return { box: "h-12 px-5", text: "text-base", gap: "gap-2", spinner: "size-4" };
|
|
140
|
+
return { box: "min-h-12 px-5 py-2", text: "text-base", gap: "gap-2", spinner: "size-4" };
|
|
52
141
|
}
|
|
53
|
-
return { box: "h-10 px-4", text: "text-sm", gap: "gap-2", spinner: "size-4" };
|
|
142
|
+
return { box: "min-h-10 px-4 py-2 pointer-coarse:min-h-11", text: "text-sm", gap: "gap-2", spinner: "size-4" };
|
|
54
143
|
});
|
|
55
144
|
|
|
56
145
|
/**
|
|
@@ -58,48 +147,77 @@
|
|
|
58
147
|
* made a disabled primary lighter and more colourful than an enabled
|
|
59
148
|
* secondary. Every variant falls back to the same neutral disabled pair.
|
|
60
149
|
*/
|
|
150
|
+
const DISABLED_LINK =
|
|
151
|
+
"bg-(--color-action-disabled) text-action-disabled-text border border-transparent cursor-not-allowed";
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* A loading button wears the same pair. It is not `:disabled`, so the
|
|
155
|
+
* variant's classes are left out instead of overridden: none of its hover
|
|
156
|
+
* or pressed fills can show. What the variant gives the box is kept (the
|
|
157
|
+
* outline's border, the link's missing side padding), so the button does
|
|
158
|
+
* not change size when it starts loading. It keeps the focus ring: it
|
|
159
|
+
* can be focused.
|
|
160
|
+
*/
|
|
161
|
+
const BUSY_BUTTON = "bg-(--color-action-disabled) text-action-disabled-text cursor-not-allowed";
|
|
162
|
+
|
|
61
163
|
const disabledClass =
|
|
62
164
|
"disabled:bg-action-disabled disabled:text-action-disabled-text disabled:border-transparent disabled:no-underline disabled:shadow-none disabled:cursor-not-allowed disabled:active:scale-100";
|
|
63
165
|
|
|
64
166
|
const variantClass = $derived.by(() => {
|
|
65
167
|
switch (variant) {
|
|
66
168
|
case "secondary":
|
|
67
|
-
return "bg-action-secondary text-headline hover:bg-action-secondary-hover active:bg-action-secondary-active active:scale-[0.98]";
|
|
169
|
+
return "bg-action-secondary text-headline hover:bg-action-secondary-hover active:bg-action-secondary-active active:scale-[0.98] motion-reduce:active:scale-100";
|
|
68
170
|
case "danger":
|
|
69
|
-
return "bg-action-danger text-action-danger-text hover:bg-action-danger-hover active:bg-action-danger-active active:scale-[0.98] focus-ring--danger";
|
|
171
|
+
return "bg-action-danger text-action-danger-text hover:bg-action-danger-hover active:bg-action-danger-active active:scale-[0.98] motion-reduce:active:scale-100 focus-ring--danger";
|
|
70
172
|
case "ghost":
|
|
71
|
-
return "bg-transparent text-headline hover:bg-surface-hover active:bg-surface-active active:scale-[0.98] focus-ring--muted";
|
|
173
|
+
return "bg-transparent text-headline hover:bg-surface-hover active:bg-surface-active active:scale-[0.98] motion-reduce:active:scale-100 focus-ring--muted";
|
|
72
174
|
case "outline":
|
|
73
|
-
return "bg-transparent border border-border text-headline hover:bg-surface-hover hover:border-border-medium active:bg-surface-active active:scale-[0.98]";
|
|
175
|
+
return "bg-transparent border border-border text-headline hover:bg-surface-hover hover:border-border-medium active:bg-surface-active active:scale-[0.98] motion-reduce:active:scale-100";
|
|
74
176
|
case "link":
|
|
75
177
|
// A link variant has to look like a link at rest, not only on
|
|
76
178
|
// hover — otherwise it is indistinguishable from `ghost`.
|
|
77
179
|
return "bg-transparent text-link hover:text-link-hover underline underline-offset-4 decoration-1 hover:decoration-2 px-0 focus-ring--muted";
|
|
180
|
+
case "accent":
|
|
181
|
+
return "bg-accent text-on-accent hover:bg-accent-hover active:bg-accent-active active:scale-[0.98] motion-reduce:active:scale-100";
|
|
78
182
|
case "primary":
|
|
79
183
|
default:
|
|
80
|
-
return "bg-action-primary text-action-primary hover:bg-action-primary-hover active:bg-action-primary-active active:scale-[0.98]";
|
|
184
|
+
return "bg-action-primary text-action-primary hover:bg-action-primary-hover active:bg-action-primary-active active:scale-[0.98] motion-reduce:active:scale-100";
|
|
81
185
|
}
|
|
82
186
|
});
|
|
83
187
|
|
|
84
188
|
const buttonClasses = $derived.by(() => {
|
|
85
189
|
const s = sizeClass;
|
|
86
190
|
const layout = fullWidth || isFullWidth ? "flex w-full" : "inline-flex";
|
|
87
|
-
|
|
88
|
-
|
|
191
|
+
if (isInlineLink) {
|
|
192
|
+
// Flows with the sentence it is in: no box, no minimum height, and
|
|
193
|
+
// it breaks across lines as a link does.
|
|
194
|
+
const state = isDisabled
|
|
195
|
+
? "text-action-disabled-text no-underline cursor-not-allowed"
|
|
196
|
+
: "text-link hover:text-link-hover underline underline-offset-4 decoration-1 hover:decoration-2 cursor-pointer";
|
|
197
|
+
return cn(
|
|
198
|
+
`focus-ring focus-ring--muted inline rounded-control font-medium transition-colors duration-150 ${s.text} ${state} ${fullWidth || isFullWidth ? "block w-full" : ""} ${className}`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
// The label wraps when it has to, balanced over its lines; the icon
|
|
202
|
+
// and the spinner stay centred beside it.
|
|
203
|
+
const base = `${layout} focus-ring items-center justify-center rounded-control text-center font-medium text-balance transition-colors duration-150 cursor-pointer select-none`;
|
|
204
|
+
// `disabled:` does not match an `<a>`: a disabled link wears the
|
|
205
|
+
// disabled pair itself, in place of its variant.
|
|
206
|
+
const busyBox = variant === "outline" ? "border border-transparent" : variant === "link" ? "px-0" : "";
|
|
207
|
+
const state =
|
|
208
|
+
isLink && isDisabled
|
|
209
|
+
? DISABLED_LINK
|
|
210
|
+
: isBusy
|
|
211
|
+
? `${BUSY_BUTTON} ${busyBox}`
|
|
212
|
+
: `${variantClass} ${disabledClass}`;
|
|
213
|
+
return cn(`${base} ${s.box} ${s.text} ${s.gap} ${state} ${className}`);
|
|
89
214
|
});
|
|
90
215
|
</script>
|
|
91
216
|
|
|
92
|
-
|
|
93
|
-
{type}
|
|
94
|
-
class={buttonClasses}
|
|
95
|
-
disabled={isDisabled}
|
|
96
|
-
aria-busy={loading ? "true" : undefined}
|
|
97
|
-
{onclick}
|
|
98
|
-
{...restProps}
|
|
99
|
-
>
|
|
217
|
+
{#snippet content()}
|
|
100
218
|
{#if loading}
|
|
101
219
|
<span
|
|
102
|
-
class="inline-block {sizeClass.spinner} shrink-0 animate-spin rounded-full border-2 border-current border-t-transparent opacity-80"
|
|
220
|
+
class="inline-block {sizeClass.spinner} shrink-0 animate-spin rounded-full border-2 border-current border-t-transparent opacity-80 motion-reduce:animate-pulse"
|
|
103
221
|
aria-hidden="true"
|
|
104
222
|
></span>
|
|
105
223
|
{/if}
|
|
@@ -108,4 +226,36 @@
|
|
|
108
226
|
{:else if children}
|
|
109
227
|
{@render children()}
|
|
110
228
|
{/if}
|
|
111
|
-
|
|
229
|
+
{/snippet}
|
|
230
|
+
|
|
231
|
+
{#if isLink}
|
|
232
|
+
<!-- A link that cannot be followed keeps its role and says it is
|
|
233
|
+
unavailable; without an `href` it is not a Tab stop, as a disabled button
|
|
234
|
+
is not. A loading one is given the Tab stop back, so focus stays on it:
|
|
235
|
+
`tabindex` is written before `href` is taken away, in that order (see
|
|
236
|
+
`keepsTabStop`). -->
|
|
237
|
+
<a
|
|
238
|
+
tabindex={isBusy || keepsTabStop ? 0 : undefined}
|
|
239
|
+
href={isDisabled ? undefined : href}
|
|
240
|
+
class={buttonClasses}
|
|
241
|
+
role={isDisabled ? "link" : undefined}
|
|
242
|
+
aria-disabled={isDisabled ? "true" : undefined}
|
|
243
|
+
aria-busy={loading ? "true" : undefined}
|
|
244
|
+
{...(restProps as unknown as HTMLAnchorAttributes)}
|
|
245
|
+
onclick={handleLinkClick}
|
|
246
|
+
>
|
|
247
|
+
{@render content()}
|
|
248
|
+
</a>
|
|
249
|
+
{:else}
|
|
250
|
+
<button
|
|
251
|
+
type={type ?? "button"}
|
|
252
|
+
class={buttonClasses}
|
|
253
|
+
disabled={!!disabled}
|
|
254
|
+
aria-disabled={isBusy ? "true" : undefined}
|
|
255
|
+
aria-busy={loading ? "true" : undefined}
|
|
256
|
+
{...restProps}
|
|
257
|
+
onclick={handleButtonClick}
|
|
258
|
+
>
|
|
259
|
+
{@render content()}
|
|
260
|
+
</button>
|
|
261
|
+
{/if}
|
|
@@ -1,9 +1,37 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type { HTMLButtonAttributes } from "svelte/elements";
|
|
1
|
+
import { type Snippet } from "svelte";
|
|
2
|
+
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
|
3
3
|
import type { ButtonVariant, SizeVariant } from "../types/variants.js";
|
|
4
|
+
/**
|
|
5
|
+
* One type for the button and the link, not a union of the two elements'
|
|
6
|
+
* attributes: that union is too large for TypeScript to carry through a
|
|
7
|
+
* wrapper such as a Storybook `Meta`. Event handlers are typed for the
|
|
8
|
+
* button; on a link they are called with the same event.
|
|
9
|
+
*/
|
|
4
10
|
type Props = Omit<HTMLButtonAttributes, "class"> & {
|
|
11
|
+
/**
|
|
12
|
+
* Where the link goes. Makes this an `<a>` that looks the same;
|
|
13
|
+
* without it, it is a `<button>`. A link cannot be disabled: while
|
|
14
|
+
* `disabled` or `loading` it has no `href`, is `aria-disabled` and
|
|
15
|
+
* does nothing when pressed. While `loading` it stays a Tab stop.
|
|
16
|
+
*/
|
|
17
|
+
href?: string;
|
|
18
|
+
/** With `href`: where the link opens. */
|
|
19
|
+
target?: HTMLAnchorAttributes["target"];
|
|
20
|
+
/** With `href`: the link's relationship, such as `noopener`. */
|
|
21
|
+
rel?: HTMLAnchorAttributes["rel"];
|
|
22
|
+
/** With `href`: download the address instead of opening it. */
|
|
23
|
+
download?: HTMLAnchorAttributes["download"];
|
|
24
|
+
hreflang?: HTMLAnchorAttributes["hreflang"];
|
|
25
|
+
referrerpolicy?: HTMLAnchorAttributes["referrerpolicy"];
|
|
26
|
+
ping?: HTMLAnchorAttributes["ping"];
|
|
5
27
|
variant?: ButtonVariant;
|
|
6
28
|
size?: SizeVariant;
|
|
29
|
+
/**
|
|
30
|
+
* Shows a spinner and makes the button unavailable without taking
|
|
31
|
+
* focus from it: `aria-disabled` and `aria-busy`, still a Tab stop,
|
|
32
|
+
* and a press does nothing. It is not `disabled`: match it in CSS
|
|
33
|
+
* with `[aria-busy="true"]`.
|
|
34
|
+
*/
|
|
7
35
|
loading?: boolean;
|
|
8
36
|
text?: string;
|
|
9
37
|
/** Stretch to the width of the container. */
|
package/dist/atoms/Card.svelte
CHANGED
|
@@ -50,13 +50,13 @@
|
|
|
50
50
|
const shared = "cursor-pointer focus-ring";
|
|
51
51
|
switch (variant) {
|
|
52
52
|
case "elevated":
|
|
53
|
-
return `${shared} hover:bg-card-hover`;
|
|
53
|
+
return `${shared} hover:bg-card-hover active:bg-card-active`;
|
|
54
54
|
case "outlined":
|
|
55
|
-
return `${shared} hover:border-border-medium hover:bg-card-hover`;
|
|
55
|
+
return `${shared} hover:border-border-medium hover:bg-card-hover active:bg-card-active`;
|
|
56
56
|
case "flat":
|
|
57
|
-
return `${shared} hover:bg-card-hover`;
|
|
57
|
+
return `${shared} hover:bg-card-hover active:bg-card-active`;
|
|
58
58
|
default:
|
|
59
|
-
return `${shared} hover:shadow-lg hover:bg-card-hover`;
|
|
59
|
+
return `${shared} hover:shadow-lg hover:bg-card-hover active:bg-card-active`;
|
|
60
60
|
}
|
|
61
61
|
});
|
|
62
62
|
|
|
@@ -34,7 +34,6 @@
|
|
|
34
34
|
* Both are merged here so existing call sites keep working. */
|
|
35
35
|
const className = $derived(cn(`${classAttr} ${legacyClass}`));
|
|
36
36
|
|
|
37
|
-
const headingTag = $derived(`h${level}`);
|
|
38
37
|
const headingClasses = $derived(() => {
|
|
39
38
|
return "text-2xl font-semibold leading-none tracking-tight text-headline";
|
|
40
39
|
});
|
|
@@ -42,9 +41,22 @@
|
|
|
42
41
|
|
|
43
42
|
<header class={cn(`flex flex-col space-y-2 pb-4 ${className}`)} {...restProps}>
|
|
44
43
|
{#if title}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
<!-- One branch per level, not a dynamic element: hydration takes a dynamic
|
|
45
|
+
element out and puts it back, which blurs a control inside it that the user
|
|
46
|
+
had already tabbed to. -->
|
|
47
|
+
{#if level === 1}
|
|
48
|
+
<h1 class={headingClasses()}>{title}</h1>
|
|
49
|
+
{:else if level === 2}
|
|
50
|
+
<h2 class={headingClasses()}>{title}</h2>
|
|
51
|
+
{:else if level === 3}
|
|
52
|
+
<h3 class={headingClasses()}>{title}</h3>
|
|
53
|
+
{:else if level === 4}
|
|
54
|
+
<h4 class={headingClasses()}>{title}</h4>
|
|
55
|
+
{:else if level === 5}
|
|
56
|
+
<h5 class={headingClasses()}>{title}</h5>
|
|
57
|
+
{:else}
|
|
58
|
+
<h6 class={headingClasses()}>{title}</h6>
|
|
59
|
+
{/if}
|
|
48
60
|
{/if}
|
|
49
61
|
{#if children}
|
|
50
62
|
{@render children()}
|