zabi-components 7.0.2 → 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 +287 -11
- package/THEME.md +149 -332
- package/THEMING.md +1329 -163
- package/dist/atoms/ActionPanel.svelte +24 -9
- package/dist/atoms/ActionPanel.svelte.d.ts +2 -0
- package/dist/atoms/Avatar.svelte +104 -0
- package/dist/atoms/Avatar.svelte.d.ts +34 -0
- package/dist/atoms/Badge.svelte +124 -58
- package/dist/atoms/Badge.svelte.d.ts +17 -5
- package/dist/atoms/Button.svelte +212 -62
- package/dist/atoms/Button.svelte.d.ts +33 -2
- package/dist/atoms/Card.svelte +31 -28
- package/dist/atoms/Card.svelte.d.ts +5 -2
- package/dist/atoms/CardContent.svelte +10 -2
- package/dist/atoms/CardContent.svelte.d.ts +2 -0
- package/dist/atoms/CardFooter.svelte +10 -2
- package/dist/atoms/CardFooter.svelte.d.ts +2 -0
- package/dist/atoms/CardHeader.svelte +26 -6
- package/dist/atoms/CardHeader.svelte.d.ts +2 -0
- package/dist/atoms/Checkbox.svelte +52 -21
- package/dist/atoms/Checkbox.svelte.d.ts +6 -2
- package/dist/atoms/CodeBlock.svelte +35 -9
- package/dist/atoms/CodeBlock.svelte.d.ts +13 -3
- package/dist/atoms/ColorPicker.svelte +272 -52
- package/dist/atoms/ColorPicker.svelte.d.ts +6 -1
- package/dist/atoms/Container.svelte +17 -10
- package/dist/atoms/DateField.svelte +107 -0
- package/dist/atoms/DateField.svelte.d.ts +53 -0
- package/dist/atoms/Divider.svelte +4 -4
- package/dist/atoms/FeatureCard.svelte +4 -3
- 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 +93 -32
- package/dist/atoms/Heading.svelte.d.ts +24 -4
- package/dist/atoms/IconButton.svelte +242 -53
- package/dist/atoms/IconButton.svelte.d.ts +49 -4
- package/dist/atoms/Input.svelte +267 -74
- package/dist/atoms/Input.svelte.d.ts +32 -2
- package/dist/atoms/List.svelte +13 -2
- package/dist/atoms/List.svelte.d.ts +2 -0
- package/dist/atoms/ListItem.svelte +44 -11
- package/dist/atoms/ListItem.svelte.d.ts +5 -0
- package/dist/atoms/ListItemLeading.svelte +4 -2
- package/dist/atoms/ListItemLeading.svelte.d.ts +1 -1
- package/dist/atoms/OptimizedImage.svelte +11 -2
- package/dist/atoms/OptimizedImage.svelte.d.ts +2 -0
- package/dist/atoms/Progress.svelte +31 -9
- package/dist/atoms/Progress.svelte.d.ts +6 -0
- package/dist/atoms/Radio.svelte +23 -3
- package/dist/atoms/Radio.svelte.d.ts +6 -2
- package/dist/atoms/Rating.svelte +494 -0
- package/dist/atoms/Rating.svelte.d.ts +56 -0
- package/dist/atoms/Select.svelte +475 -135
- package/dist/atoms/Select.svelte.d.ts +52 -2
- package/dist/atoms/SelectionControl.svelte +99 -18
- package/dist/atoms/SelectionControl.svelte.d.ts +6 -3
- package/dist/atoms/Skeleton.svelte +5 -3
- 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 +152 -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 +56 -15
- package/dist/atoms/Text.svelte.d.ts +13 -4
- package/dist/atoms/Textarea.svelte +57 -45
- package/dist/atoms/Textarea.svelte.d.ts +14 -2
- package/dist/atoms/ThemeToggle.svelte +238 -56
- package/dist/atoms/ThemeToggle.svelte.d.ts +28 -3
- package/dist/atoms/TimeField.svelte +108 -0
- package/dist/atoms/TimeField.svelte.d.ts +54 -0
- package/dist/atoms/Toast.svelte +25 -10
- package/dist/atoms/Toast.svelte.d.ts +2 -0
- package/dist/atoms/Toggle.svelte +60 -19
- package/dist/atoms/Toggle.svelte.d.ts +18 -2
- package/dist/atoms/Tooltip.svelte +583 -40
- package/dist/atoms/Tooltip.svelte.d.ts +26 -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 +83 -3
- 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 +6 -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/cn.d.ts +8 -0
- package/dist/components/util/cn.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/ssr-safe.d.ts +4 -4
- package/dist/components/util/ssr-safe.d.ts.map +1 -1
- 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 +83 -3
- package/dist/index.js +60 -8
- package/dist/molecules/Alert.svelte +73 -66
- package/dist/molecules/Alert.svelte.d.ts +9 -1
- 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 +25 -5
- package/dist/molecules/ComponentDemo.svelte.d.ts +5 -0
- package/dist/molecules/ConfirmDialog.svelte +253 -0
- package/dist/molecules/ConfirmDialog.svelte.d.ts +55 -0
- package/dist/molecules/ContactForm.svelte +46 -19
- package/dist/molecules/ContactForm.svelte.d.ts +10 -0
- package/dist/molecules/Drawer.svelte +394 -0
- package/dist/molecules/Drawer.svelte.d.ts +55 -0
- package/dist/molecules/Dropdown.svelte +540 -50
- package/dist/molecules/Dropdown.svelte.d.ts +49 -6
- package/dist/molecules/DropdownItem.svelte +122 -0
- package/dist/molecules/DropdownItem.svelte.d.ts +28 -0
- package/dist/molecules/EmptyState.svelte +56 -10
- package/dist/molecules/EmptyState.svelte.d.ts +8 -0
- package/dist/molecules/Form.svelte +25 -14
- package/dist/molecules/Form.svelte.d.ts +13 -5
- package/dist/molecules/FormField.svelte +11 -2
- package/dist/molecules/FormField.svelte.d.ts +2 -0
- package/dist/molecules/Header.svelte +13 -3
- package/dist/molecules/Header.svelte.d.ts +2 -0
- package/dist/molecules/ImageUpload.svelte +422 -70
- package/dist/molecules/ImageUpload.svelte.d.ts +61 -5
- package/dist/molecules/MediaGrid.svelte +527 -0
- package/dist/molecules/MediaGrid.svelte.d.ts +77 -0
- package/dist/molecules/Modal.svelte +340 -45
- package/dist/molecules/Modal.svelte.d.ts +59 -7
- package/dist/molecules/NavigationMenu.svelte +15 -3
- package/dist/molecules/NavigationMenu.svelte.d.ts +4 -0
- package/dist/molecules/NavigationMenuContent.svelte +91 -36
- package/dist/molecules/NavigationMenuContent.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuItem.svelte +10 -3
- package/dist/molecules/NavigationMenuItem.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuLink.svelte +10 -2
- package/dist/molecules/NavigationMenuLink.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuList.svelte +19 -4
- package/dist/molecules/NavigationMenuList.svelte.d.ts +2 -0
- package/dist/molecules/NavigationMenuTrigger.svelte +16 -3
- package/dist/molecules/NavigationMenuTrigger.svelte.d.ts +2 -0
- package/dist/molecules/Page.svelte +38 -2
- package/dist/molecules/Page.svelte.d.ts +13 -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 +26 -11
- package/dist/molecules/PropsTable.svelte.d.ts +5 -0
- package/dist/molecules/PullToRefresh.svelte +263 -0
- package/dist/molecules/PullToRefresh.svelte.d.ts +52 -0
- package/dist/molecules/RadioGroup.svelte +57 -15
- package/dist/molecules/RadioGroup.svelte.d.ts +16 -3
- package/dist/molecules/Section.svelte +13 -5
- package/dist/molecules/Section.svelte.d.ts +3 -1
- package/dist/molecules/SegmentedControl.svelte +323 -0
- package/dist/molecules/SegmentedControl.svelte.d.ts +34 -0
- package/dist/molecules/SidebarBrandHeader.svelte +24 -5
- package/dist/molecules/SidebarBrandHeader.svelte.d.ts +5 -0
- package/dist/molecules/SidebarFooter.svelte +39 -8
- package/dist/molecules/SidebarFooter.svelte.d.ts +5 -0
- package/dist/molecules/SidebarNavSection.svelte +12 -4
- package/dist/molecules/SidebarNavSection.svelte.d.ts +2 -0
- package/dist/molecules/SlideUp.svelte +253 -30
- package/dist/molecules/SlideUp.svelte.d.ts +31 -4
- 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 +254 -24
- package/dist/molecules/Tabs.svelte.d.ts +15 -4
- package/dist/molecules/Toaster.svelte +352 -6
- package/dist/molecules/Toaster.svelte.d.ts +95 -2
- package/dist/molecules/ToasterToast.svelte +279 -62
- package/dist/molecules/ToasterToast.svelte.d.ts +11 -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 +111 -12
- package/dist/organisms/SidebarAccountPanel.svelte.d.ts +21 -0
- package/dist/organisms/SidebarNavigation.svelte +193 -69
- package/dist/organisms/SidebarNavigation.svelte.d.ts +31 -1
- package/dist/organisms/SidebarPanel.svelte +45 -16
- package/dist/organisms/SidebarPanel.svelte.d.ts +2 -0
- package/dist/organisms/SidebarShell.svelte +263 -0
- package/dist/organisms/SidebarShell.svelte.d.ts +106 -0
- package/dist/organisms/TopNavbar.svelte +329 -33
- package/dist/organisms/TopNavbar.svelte.d.ts +28 -0
- package/dist/organisms/index.d.ts +6 -0
- package/dist/organisms/index.js +4 -0
- package/dist/routes/lib/variant-utils.d.ts +26 -0
- package/dist/routes/lib/{variant-utils.ts → variant-utils.js} +15 -46
- package/dist/types/components.d.ts +4 -0
- package/dist/types/components.d.ts.map +1 -1
- package/dist/types/events.js +1 -0
- package/dist/types/index.d.ts +605 -25
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +3 -0
- 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/cn.d.ts +7 -0
- package/dist/util/cn.js +48 -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/ssr-safe.d.ts +4 -4
- 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 +574 -225
- package/dist/zabi-components-theme-dark.css +574 -225
- package/dist/zabi-components-theme-only.css +1680 -317
- package/dist/zabi-components-theme.css +1680 -317
- package/dist/zabi-components.css +4473 -1158
- package/docs/lucide-icons.md +41 -16
- package/docs/theme-imports.md +81 -11
- package/package.json +51 -29
- package/dist/lib/layout-width-tokens.d.ts +0 -17
- package/dist/lib/layout-width-tokens.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Button.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Button.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Input.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Input.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/List.d.ts +0 -3
- package/dist/lib/showcase/component-docs/List.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Modal.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Modal.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Radio.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Radio.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts +0 -3
- package/dist/lib/showcase/component-docs/SidebarNavigation.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/Skeleton.d.ts +0 -3
- package/dist/lib/showcase/component-docs/Skeleton.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/_shared.d.ts +0 -12
- package/dist/lib/showcase/component-docs/_shared.d.ts.map +0 -1
- package/dist/lib/showcase/component-docs/index.d.ts +0 -4
- package/dist/lib/showcase/component-docs/index.d.ts.map +0 -1
- package/dist/lib/showcase/components-catalog.d.ts +0 -3
- package/dist/lib/showcase/components-catalog.d.ts.map +0 -1
- package/dist/lib/showcase/components-showcase-constants.d.ts +0 -89
- package/dist/lib/showcase/components-showcase-constants.d.ts.map +0 -1
- package/dist/lib/showcase/docs-sidebar-helpers.d.ts +0 -15
- package/dist/lib/showcase/docs-sidebar-helpers.d.ts.map +0 -1
- package/dist/routes/lib/focus-utils.d.ts +0 -12
- package/dist/routes/lib/focus-utils.ts +0 -110
- package/dist/routes/lib/ssr-safe.ts +0 -58
- package/scripts/fix-lucide-svelte-icon-dts.js +0 -35
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
<script lang="ts" generics="T">
|
|
2
|
+
import { mergeStrings } from "../util/ready-made-strings.js";
|
|
3
|
+
import { zabiStringsFor } from "../util/zabi-strings.js";
|
|
4
|
+
import { tick, untrack, type Snippet } from "svelte";
|
|
5
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
6
|
+
import Check from "@lucide/svelte/icons/check";
|
|
7
|
+
import ImageOff from "@lucide/svelte/icons/image-off";
|
|
8
|
+
import Play from "@lucide/svelte/icons/play";
|
|
9
|
+
import Trash2 from "@lucide/svelte/icons/trash-2";
|
|
10
|
+
import Skeleton from "../atoms/Skeleton.svelte";
|
|
11
|
+
import EmptyState from "./EmptyState.svelte";
|
|
12
|
+
import { cn } from "../util/cn.js";
|
|
13
|
+
import type { CollapsibleHeadingLevel } from "../util/collapsible.js";
|
|
14
|
+
import { generateId } from "../util/ssr-safe.js";
|
|
15
|
+
import {
|
|
16
|
+
MEDIA_GRID_STRINGS,
|
|
17
|
+
columnCount,
|
|
18
|
+
isVideoUrl,
|
|
19
|
+
moveForKey,
|
|
20
|
+
moveIndex,
|
|
21
|
+
type MediaGridItemType,
|
|
22
|
+
type MediaGridKey,
|
|
23
|
+
type MediaGridSelectDetail,
|
|
24
|
+
type MediaGridStrings,
|
|
25
|
+
} from "../util/media-grid.js";
|
|
26
|
+
|
|
27
|
+
/** Other attributes (`id`, `data-*`, ...) land on the host `<div>`; the two ARIA names go to the `<ul>`. */
|
|
28
|
+
type Props = Omit<
|
|
29
|
+
HTMLAttributes<HTMLDivElement>,
|
|
30
|
+
// `onselect` is also a native event (text selection); ours replaces it.
|
|
31
|
+
"class" | "aria-label" | "aria-labelledby" | "onselect"
|
|
32
|
+
> & {
|
|
33
|
+
/** Extra classes for the host element. */
|
|
34
|
+
class?: string;
|
|
35
|
+
items: T[];
|
|
36
|
+
/** Stable identity of an item. Tiles are keyed by it, and selection is stored as keys. */
|
|
37
|
+
getKey: (item: T) => MediaGridKey;
|
|
38
|
+
/** Name of an item: the image's `alt`, and part of the delete button's name. */
|
|
39
|
+
getLabel: (item: T) => string;
|
|
40
|
+
/** Address of the image or video. */
|
|
41
|
+
getUrl: (item: T) => string;
|
|
42
|
+
/** Whether an item is a video. Without it, or when it returns nothing, the URL's extension decides. */
|
|
43
|
+
getType?: (item: T) => MediaGridItemType | undefined;
|
|
44
|
+
/** A still image for a video, shown instead of loading the video's first frame. */
|
|
45
|
+
getPoster?: (item: T) => string | undefined;
|
|
46
|
+
/** Key of the selected item, or `null`; supports `bind:selected`. Used unless `multiple`. */
|
|
47
|
+
selected?: MediaGridKey | null;
|
|
48
|
+
/** Any number of items can be selected; the keys are in `selectedKeys`. */
|
|
49
|
+
multiple?: boolean;
|
|
50
|
+
/** Keys of the selected items; supports `bind:selectedKeys`. Used when `multiple`. */
|
|
51
|
+
selectedKeys?: MediaGridKey[];
|
|
52
|
+
/** Called when an item is selected or its selection is cleared. */
|
|
53
|
+
onselect?: (detail: MediaGridSelectDetail<T>) => void;
|
|
54
|
+
/**
|
|
55
|
+
* Shows a delete button on each item and reports which one was
|
|
56
|
+
* activated. The grid neither confirms nor removes: confirm, then
|
|
57
|
+
* take the item out of `items`.
|
|
58
|
+
*/
|
|
59
|
+
ondelete?: (item: T) => void;
|
|
60
|
+
/** Leaves the delete button off one item. */
|
|
61
|
+
isItemDeletable?: (item: T) => boolean;
|
|
62
|
+
/** Adds placeholder tiles after the items, for a first load or a next page. */
|
|
63
|
+
loading?: boolean;
|
|
64
|
+
/** How many placeholder tiles `loading` shows. */
|
|
65
|
+
loadingCount?: number;
|
|
66
|
+
/** Replaces the built-in empty state. */
|
|
67
|
+
empty?: Snippet;
|
|
68
|
+
/**
|
|
69
|
+
* Heading element of the built-in empty state's title. 3 by default:
|
|
70
|
+
* the grid usually sits under a Modal or section title.
|
|
71
|
+
*/
|
|
72
|
+
emptyHeadingLevel?: CollapsibleHeadingLevel;
|
|
73
|
+
/** Disables selecting and deleting. */
|
|
74
|
+
disabled?: boolean;
|
|
75
|
+
/** Smallest tile edge, as px or any CSS length. The grid fits as many columns as that allows. */
|
|
76
|
+
minTileSize?: number | string;
|
|
77
|
+
/** Overrides for the built-in strings. */
|
|
78
|
+
strings?: Partial<MediaGridStrings>;
|
|
79
|
+
"aria-label"?: string;
|
|
80
|
+
"aria-labelledby"?: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
let {
|
|
84
|
+
class: className = "",
|
|
85
|
+
items,
|
|
86
|
+
getKey,
|
|
87
|
+
getLabel,
|
|
88
|
+
getUrl,
|
|
89
|
+
getType,
|
|
90
|
+
getPoster,
|
|
91
|
+
selected = $bindable<Exclude<Props["selected"], undefined>>(),
|
|
92
|
+
multiple = false,
|
|
93
|
+
selectedKeys = $bindable<Exclude<Props["selectedKeys"], undefined>>(),
|
|
94
|
+
onselect,
|
|
95
|
+
ondelete,
|
|
96
|
+
isItemDeletable,
|
|
97
|
+
loading = false,
|
|
98
|
+
loadingCount = 8,
|
|
99
|
+
empty,
|
|
100
|
+
emptyHeadingLevel = 3,
|
|
101
|
+
disabled = false,
|
|
102
|
+
minTileSize = 96,
|
|
103
|
+
strings,
|
|
104
|
+
"aria-label": ariaLabel,
|
|
105
|
+
"aria-labelledby": ariaLabelledby,
|
|
106
|
+
...restProps
|
|
107
|
+
}: Props = $props();
|
|
108
|
+
|
|
109
|
+
// No fallback on a bindable prop: Svelte refuses `bind:…={undefined}` on
|
|
110
|
+
// one that has a fallback (`props_invalid_value`), and a page that throws
|
|
111
|
+
// while it hydrates never becomes interactive. The default is applied
|
|
112
|
+
// here instead: at once, for the server and the first render, and again
|
|
113
|
+
// whenever a parent hands back `undefined`.
|
|
114
|
+
const applyDefaults = () => {
|
|
115
|
+
if (selected === undefined) selected = null;
|
|
116
|
+
if (selectedKeys === undefined) selectedKeys = [];
|
|
117
|
+
};
|
|
118
|
+
applyDefaults();
|
|
119
|
+
$effect.pre(applyDefaults);
|
|
120
|
+
|
|
121
|
+
/** How long to wait for a confirmation dialog to give focus up after a delete. */
|
|
122
|
+
const FOCUS_WAIT_MS = 2000;
|
|
123
|
+
|
|
124
|
+
/** The app-wide words for this component, from a `ZabiStringsProvider` above it, if there is one. */
|
|
125
|
+
const provided = zabiStringsFor("mediaGrid");
|
|
126
|
+
const text = $derived(mergeStrings(MEDIA_GRID_STRINGS, provided(), strings));
|
|
127
|
+
const keys = $derived(items.map(getKey));
|
|
128
|
+
const tileSize = $derived(
|
|
129
|
+
typeof minTileSize === "number" ? `${minTileSize}px` : minTileSize,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const hintId = generateId("media-grid-hint");
|
|
133
|
+
|
|
134
|
+
let host: HTMLDivElement | undefined = $state();
|
|
135
|
+
/** URLs that failed to load; a changed URL is tried again. */
|
|
136
|
+
let failed = $state<string[]>([]);
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The tile the arrow keys last reached. Only its buttons are in the Tab
|
|
140
|
+
* order (a roving tabindex), so the grid is one stop however many items
|
|
141
|
+
* it holds.
|
|
142
|
+
*/
|
|
143
|
+
let activeKey = $state<MediaGridKey | null>(null);
|
|
144
|
+
const tabbableKey = $derived.by(() => {
|
|
145
|
+
if (activeKey !== null && keys.includes(activeKey)) return activeKey;
|
|
146
|
+
if (!multiple && selected !== null && keys.includes(selected)) return selected;
|
|
147
|
+
return keys[0] ?? null;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Nothing about a list of buttons says that the arrow keys work, so the
|
|
152
|
+
* item focus arrives on is described by the hint. Only that one: a
|
|
153
|
+
* description on every item would be read again at each arrow press.
|
|
154
|
+
* While focus is outside, that item is the Tab stop; once inside, it
|
|
155
|
+
* stays the item focus came in on until focus leaves.
|
|
156
|
+
*/
|
|
157
|
+
let focusInside = $state(false);
|
|
158
|
+
let entryKey = $state<MediaGridKey | null>(null);
|
|
159
|
+
const describedKey = $derived(focusInside ? entryKey : tabbableKey);
|
|
160
|
+
|
|
161
|
+
function handleTileFocus(key: MediaGridKey) {
|
|
162
|
+
// Runs before the host's own focusin, which sets `focusInside`.
|
|
163
|
+
if (!focusInside) entryKey = key;
|
|
164
|
+
activeKey = key;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function handleHostFocusOut(event: FocusEvent) {
|
|
168
|
+
const next = event.relatedTarget;
|
|
169
|
+
if (!(next instanceof Node) || !host?.contains(next)) focusInside = false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isSelected(key: MediaGridKey): boolean {
|
|
173
|
+
return multiple ? selectedKeys.includes(key) : selected === key;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isVideo(entry: T): boolean {
|
|
177
|
+
const type = getType?.(entry);
|
|
178
|
+
return type ? type === "video" : isVideoUrl(getUrl(entry));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function isDeletable(entry: T): boolean {
|
|
182
|
+
return !!ondelete && (isItemDeletable?.(entry) ?? true);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function tileButtons(): HTMLElement[] {
|
|
186
|
+
return host
|
|
187
|
+
? Array.from(host.querySelectorAll<HTMLElement>("[data-media-grid-item]"))
|
|
188
|
+
: [];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function toggle(entry: T) {
|
|
192
|
+
if (disabled) return;
|
|
193
|
+
const key = getKey(entry);
|
|
194
|
+
const on = !isSelected(key);
|
|
195
|
+
if (multiple) {
|
|
196
|
+
selectedKeys = on
|
|
197
|
+
? [...selectedKeys, key]
|
|
198
|
+
: selectedKeys.filter((candidate) => candidate !== key);
|
|
199
|
+
onselect?.({ item: entry, selected: on, keys: selectedKeys });
|
|
200
|
+
} else {
|
|
201
|
+
// A single choice is never cleared by pressing it again; only the
|
|
202
|
+
// checkbox-like `multiple` mode toggles off. Removing the item, or
|
|
203
|
+
// the parent through the binding, still clears it.
|
|
204
|
+
if (!on) return;
|
|
205
|
+
selected = key;
|
|
206
|
+
onselect?.({ item: entry, selected: true, keys: [key] });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** The item whose delete was last requested; its removal is this grid's doing. */
|
|
211
|
+
let pendingDelete: MediaGridKey | null = null;
|
|
212
|
+
|
|
213
|
+
function requestDelete(entry: T) {
|
|
214
|
+
if (disabled || !isDeletable(entry)) return;
|
|
215
|
+
pendingDelete = getKey(entry);
|
|
216
|
+
ondelete?.(entry);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
220
|
+
const tile = (event.target as HTMLElement).closest<HTMLElement>(
|
|
221
|
+
"[data-media-grid-tile]",
|
|
222
|
+
);
|
|
223
|
+
if (!tile || event.altKey || event.metaKey) return;
|
|
224
|
+
const buttons = tileButtons();
|
|
225
|
+
const index = buttons.findIndex((button) => tile.contains(button));
|
|
226
|
+
if (index === -1) return;
|
|
227
|
+
|
|
228
|
+
if (event.key === "Delete" && !event.ctrlKey) {
|
|
229
|
+
const entry = items[index];
|
|
230
|
+
if (entry !== undefined && isDeletable(entry) && !disabled) {
|
|
231
|
+
event.preventDefault();
|
|
232
|
+
requestDelete(entry);
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// The arrows follow what is on screen, which is mirrored in RTL.
|
|
238
|
+
const move = moveForKey(event, getComputedStyle(tile).direction === "rtl");
|
|
239
|
+
if (!move) return;
|
|
240
|
+
|
|
241
|
+
// Also keeps the arrow keys from scrolling a Modal body under the grid.
|
|
242
|
+
event.preventDefault();
|
|
243
|
+
const columns = columnCount(
|
|
244
|
+
buttons.map((button) => button.getBoundingClientRect().top),
|
|
245
|
+
);
|
|
246
|
+
const target = moveIndex(index, move, columns, buttons.length);
|
|
247
|
+
if (target !== index) buttons[target]?.focus();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let focusFrame: number | undefined;
|
|
251
|
+
|
|
252
|
+
function stopWaitingForFocus() {
|
|
253
|
+
if (focusFrame !== undefined) cancelAnimationFrame(focusFrame);
|
|
254
|
+
focusFrame = undefined;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function focusLost(): boolean {
|
|
258
|
+
const active = document.activeElement;
|
|
259
|
+
return !active || active === document.body;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** With nothing left, the host takes focus so it does not land on `<body>`. */
|
|
263
|
+
function focusTile(key: MediaGridKey | null) {
|
|
264
|
+
const index = key === null ? -1 : keys.indexOf(key);
|
|
265
|
+
(tileButtons()[index] ?? host)?.focus();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Removing an item removes the node that had focus, and the browser then
|
|
270
|
+
* drops focus on `<body>`. Hand it to the item that took its place.
|
|
271
|
+
*
|
|
272
|
+
* A delete is usually confirmed in a dialog, and the dialog may still be
|
|
273
|
+
* open when the item goes: focus is then inside the dialog, and is lost
|
|
274
|
+
* only when the dialog closes and cannot return it to a delete button
|
|
275
|
+
* that no longer exists. Nothing fires at that moment, so the grid looks
|
|
276
|
+
* for it frame by frame for a short while. It never takes focus from an
|
|
277
|
+
* element that has it.
|
|
278
|
+
*/
|
|
279
|
+
let previousKeys: MediaGridKey[] = [];
|
|
280
|
+
$effect.pre(() => {
|
|
281
|
+
const current = keys;
|
|
282
|
+
untrack(() => {
|
|
283
|
+
const before = previousKeys;
|
|
284
|
+
previousKeys = current;
|
|
285
|
+
if (!host) return;
|
|
286
|
+
|
|
287
|
+
const focusInside = host.contains(document.activeElement);
|
|
288
|
+
const focusedGone =
|
|
289
|
+
focusInside && activeKey !== null && !current.includes(activeKey)
|
|
290
|
+
? activeKey
|
|
291
|
+
: null;
|
|
292
|
+
const deletedGone =
|
|
293
|
+
pendingDelete !== null && !current.includes(pendingDelete)
|
|
294
|
+
? pendingDelete
|
|
295
|
+
: null;
|
|
296
|
+
if (deletedGone !== null) pendingDelete = null;
|
|
297
|
+
const gone = focusedGone ?? deletedGone;
|
|
298
|
+
if (gone === null || !before.includes(gone)) return;
|
|
299
|
+
|
|
300
|
+
const target =
|
|
301
|
+
current[Math.min(before.indexOf(gone), current.length - 1)] ?? null;
|
|
302
|
+
activeKey = target;
|
|
303
|
+
|
|
304
|
+
void tick().then(() => {
|
|
305
|
+
if (focusLost()) {
|
|
306
|
+
focusTile(target);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (deletedGone === null || host?.contains(document.activeElement)) return;
|
|
310
|
+
|
|
311
|
+
stopWaitingForFocus();
|
|
312
|
+
const deadline = performance.now() + FOCUS_WAIT_MS;
|
|
313
|
+
const check = () => {
|
|
314
|
+
focusFrame = undefined;
|
|
315
|
+
if (focusLost()) {
|
|
316
|
+
focusTile(target);
|
|
317
|
+
} else if (
|
|
318
|
+
!host?.contains(document.activeElement) &&
|
|
319
|
+
performance.now() < deadline
|
|
320
|
+
) {
|
|
321
|
+
focusFrame = requestAnimationFrame(check);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
focusFrame = requestAnimationFrame(check);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
$effect(() => stopWaitingForFocus);
|
|
330
|
+
|
|
331
|
+
const badgeClasses =
|
|
332
|
+
"pointer-events-none absolute flex size-6 items-center justify-center";
|
|
333
|
+
// Nested corners: a mark in the corner of a tile is 0.25rem (`start-1
|
|
334
|
+
// top-1`) inside the tile's border, so its radius is the tile's less that
|
|
335
|
+
// gap. The border is 2px on a selected tile, where the check is, and 1px
|
|
336
|
+
// otherwise. A pill there would need a tile radius of 18px.
|
|
337
|
+
const CHECK_RADIUS = "rounded-[calc(var(--radius-container)-0.25rem-2px)]";
|
|
338
|
+
const VIDEO_RADIUS = "rounded-[calc(var(--radius-container)-0.25rem-1px)]";
|
|
339
|
+
</script>
|
|
340
|
+
|
|
341
|
+
<div
|
|
342
|
+
bind:this={host}
|
|
343
|
+
tabindex="-1"
|
|
344
|
+
class={cn("group/media focus-ring relative rounded-container", className)}
|
|
345
|
+
onfocusin={() => (focusInside = true)}
|
|
346
|
+
onfocusout={handleHostFocusOut}
|
|
347
|
+
data-disabled={disabled ? "" : undefined}
|
|
348
|
+
{...restProps}
|
|
349
|
+
>
|
|
350
|
+
{#if items.length === 0 && !loading}
|
|
351
|
+
{#if empty}
|
|
352
|
+
{@render empty()}
|
|
353
|
+
{:else}
|
|
354
|
+
<EmptyState
|
|
355
|
+
title={text.emptyTitle}
|
|
356
|
+
description={text.emptyDescription}
|
|
357
|
+
size="compact"
|
|
358
|
+
headingLevel={emptyHeadingLevel}
|
|
359
|
+
/>
|
|
360
|
+
{/if}
|
|
361
|
+
{:else}
|
|
362
|
+
<!-- The reset removes the markers, and Safari then drops the list role. -->
|
|
363
|
+
<!-- svelte-ignore a11y_no_redundant_roles, a11y_no_noninteractive_element_interactions -->
|
|
364
|
+
<ul
|
|
365
|
+
role="list"
|
|
366
|
+
class="m-0 grid list-none gap-2 p-0"
|
|
367
|
+
style:grid-template-columns="repeat(auto-fill, minmax(min({tileSize}, 100%), 1fr))"
|
|
368
|
+
aria-label={ariaLabel}
|
|
369
|
+
aria-labelledby={ariaLabelledby}
|
|
370
|
+
aria-busy={loading ? "true" : undefined}
|
|
371
|
+
onkeydown={handleKeydown}
|
|
372
|
+
>
|
|
373
|
+
{#each items as entry (getKey(entry))}
|
|
374
|
+
{@const key = getKey(entry)}
|
|
375
|
+
{@const label = getLabel(entry)}
|
|
376
|
+
{@const url = getUrl(entry)}
|
|
377
|
+
{@const video = isVideo(entry)}
|
|
378
|
+
{@const poster = video ? getPoster?.(entry) : undefined}
|
|
379
|
+
{@const source = poster ?? url}
|
|
380
|
+
{@const on = isSelected(key)}
|
|
381
|
+
{@const tabindex = key === tabbableKey ? 0 : -1}
|
|
382
|
+
<li
|
|
383
|
+
class="relative aspect-square"
|
|
384
|
+
data-media-grid-tile
|
|
385
|
+
data-selected={on ? "" : undefined}
|
|
386
|
+
onfocusin={() => handleTileFocus(key)}
|
|
387
|
+
>
|
|
388
|
+
<!-- Selection is a thicker border and a check mark, not
|
|
389
|
+
a colour alone. -->
|
|
390
|
+
<button
|
|
391
|
+
type="button"
|
|
392
|
+
class={cn(
|
|
393
|
+
"focus-ring relative block size-full cursor-pointer overflow-hidden rounded-container border bg-surface-2 transition-colors duration-150 motion-reduce:transition-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
394
|
+
on
|
|
395
|
+
? "border-2 border-action-primary"
|
|
396
|
+
: "border-border enabled:hover:border-border-medium enabled:active:scale-[0.98]",
|
|
397
|
+
)}
|
|
398
|
+
aria-pressed={on}
|
|
399
|
+
aria-label={video ? text.videoLabel(label) : label}
|
|
400
|
+
aria-describedby={key === describedKey ? hintId : undefined}
|
|
401
|
+
{tabindex}
|
|
402
|
+
{disabled}
|
|
403
|
+
data-media-grid-item
|
|
404
|
+
onclick={() => toggle(entry)}
|
|
405
|
+
>
|
|
406
|
+
{#if failed.includes(source)}
|
|
407
|
+
<span
|
|
408
|
+
class="flex size-full items-center justify-center text-description"
|
|
409
|
+
data-media-grid-fallback
|
|
410
|
+
>
|
|
411
|
+
<ImageOff size={24} aria-hidden="true" />
|
|
412
|
+
</span>
|
|
413
|
+
{:else if video && !poster}
|
|
414
|
+
<!-- A still first frame: never plays, so there is
|
|
415
|
+
no motion to reduce and no sound. The name is on
|
|
416
|
+
the button. -->
|
|
417
|
+
<video
|
|
418
|
+
src={url}
|
|
419
|
+
muted
|
|
420
|
+
playsinline
|
|
421
|
+
preload="metadata"
|
|
422
|
+
tabindex="-1"
|
|
423
|
+
aria-hidden="true"
|
|
424
|
+
class="pointer-events-none size-full object-cover"
|
|
425
|
+
onerror={() => (failed = [...failed, source])}
|
|
426
|
+
></video>
|
|
427
|
+
{:else}
|
|
428
|
+
<img
|
|
429
|
+
src={source}
|
|
430
|
+
alt={label}
|
|
431
|
+
loading="lazy"
|
|
432
|
+
decoding="async"
|
|
433
|
+
draggable="false"
|
|
434
|
+
class="size-full object-cover"
|
|
435
|
+
onerror={() => (failed = [...failed, source])}
|
|
436
|
+
/>
|
|
437
|
+
{/if}
|
|
438
|
+
{#if on}
|
|
439
|
+
<span
|
|
440
|
+
class={cn(
|
|
441
|
+
badgeClasses,
|
|
442
|
+
CHECK_RADIUS,
|
|
443
|
+
"start-1 top-1 bg-action-primary text-action-primary",
|
|
444
|
+
)}
|
|
445
|
+
data-media-grid-check
|
|
446
|
+
>
|
|
447
|
+
<Check size={14} aria-hidden="true" />
|
|
448
|
+
</span>
|
|
449
|
+
{/if}
|
|
450
|
+
{#if video}
|
|
451
|
+
<!-- Opaque, so it reads over any frame. Assistive
|
|
452
|
+
technology gets "video" from the button's name. -->
|
|
453
|
+
<span
|
|
454
|
+
class={cn(
|
|
455
|
+
badgeClasses,
|
|
456
|
+
VIDEO_RADIUS,
|
|
457
|
+
"bottom-1 start-1 border border-border-overlay bg-surface-overlay text-body",
|
|
458
|
+
)}
|
|
459
|
+
data-media-grid-video
|
|
460
|
+
>
|
|
461
|
+
<Play size={12} aria-hidden="true" />
|
|
462
|
+
</span>
|
|
463
|
+
{/if}
|
|
464
|
+
</button>
|
|
465
|
+
{#if isDeletable(entry)}
|
|
466
|
+
<!-- Beside the item's button, not inside it, and
|
|
467
|
+
always visible: hover cannot reveal it on a touch
|
|
468
|
+
screen. On the end corner, so it mirrors in a
|
|
469
|
+
right-to-left layout. On a coarse pointer the
|
|
470
|
+
pseudo-element widens what takes the tap to 44px
|
|
471
|
+
without changing the 28px that is drawn, so a near
|
|
472
|
+
miss no longer toggles the selection. -->
|
|
473
|
+
<button
|
|
474
|
+
type="button"
|
|
475
|
+
class="media-grid-delete focus-ring focus-ring--danger absolute end-1 top-1 flex size-7 cursor-pointer before:absolute before:hidden before:-inset-[9px] before:content-[''] pointer-coarse:before:block items-center justify-center rounded-control border border-border-overlay bg-surface-overlay text-body transition-colors duration-150 enabled:hover:text-error-text motion-reduce:transition-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
476
|
+
aria-label={text.deleteLabel(label)}
|
|
477
|
+
{tabindex}
|
|
478
|
+
{disabled}
|
|
479
|
+
data-media-grid-delete
|
|
480
|
+
onclick={() => requestDelete(entry)}
|
|
481
|
+
>
|
|
482
|
+
<Trash2 size={14} aria-hidden="true" />
|
|
483
|
+
</button>
|
|
484
|
+
{/if}
|
|
485
|
+
</li>
|
|
486
|
+
{/each}
|
|
487
|
+
{#if loading}
|
|
488
|
+
{#each { length: loadingCount } as _, index (index)}
|
|
489
|
+
<!-- One status below speaks for all of them. -->
|
|
490
|
+
<li class="aspect-square" aria-hidden="true" data-media-grid-skeleton>
|
|
491
|
+
<Skeleton variant="block" class="h-full rounded-container" />
|
|
492
|
+
</li>
|
|
493
|
+
{/each}
|
|
494
|
+
{/if}
|
|
495
|
+
</ul>
|
|
496
|
+
<!-- Hidden until the grid has keyboard focus: a sighted keyboard user
|
|
497
|
+
needs it as much as a screen reader user, and a pointer user does not.
|
|
498
|
+
It describes the item focus enters on whether it is shown or not. -->
|
|
499
|
+
<p
|
|
500
|
+
id={hintId}
|
|
501
|
+
class="mt-2 hidden text-xs text-description group-has-[:focus-visible]/media:block"
|
|
502
|
+
data-media-grid-hint
|
|
503
|
+
>
|
|
504
|
+
{text.keyboardHint}
|
|
505
|
+
</p>
|
|
506
|
+
{/if}
|
|
507
|
+
|
|
508
|
+
<div class="sr-only" role="status" aria-live="polite" aria-atomic="true">
|
|
509
|
+
{loading ? text.loading : ""}
|
|
510
|
+
</div>
|
|
511
|
+
</div>
|
|
512
|
+
|
|
513
|
+
<style>
|
|
514
|
+
/*
|
|
515
|
+
* The remove button's pressed state. Hover only changed its text colour,
|
|
516
|
+
* so on a touch screen a press showed nothing. The button is an opaque
|
|
517
|
+
* plate over an image: a translucent pressed tint would let the image
|
|
518
|
+
* through and read as weaker, so it steps to the next opaque overlay fill
|
|
519
|
+
* and takes the error text colour. Written here, not as utilities: the
|
|
520
|
+
* resting `bg-surface-overlay` and `text-body` are unlayered classes in
|
|
521
|
+
* app.css, which a generated `active:` variant cannot beat.
|
|
522
|
+
*/
|
|
523
|
+
.media-grid-delete:enabled:active {
|
|
524
|
+
background-color: var(--color-surface-overlay-hover);
|
|
525
|
+
color: var(--color-error-text);
|
|
526
|
+
}
|
|
527
|
+
</style>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { type Snippet } from "svelte";
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
import type { CollapsibleHeadingLevel } from "../util/collapsible.js";
|
|
4
|
+
import { type MediaGridItemType, type MediaGridKey, type MediaGridSelectDetail, type MediaGridStrings } from "../util/media-grid.js";
|
|
5
|
+
declare function $$render<T>(): {
|
|
6
|
+
props: Omit<HTMLAttributes<HTMLDivElement>, "class" | "aria-label" | "aria-labelledby" | "onselect"> & {
|
|
7
|
+
/** Extra classes for the host element. */
|
|
8
|
+
class?: string;
|
|
9
|
+
items: T[];
|
|
10
|
+
/** Stable identity of an item. Tiles are keyed by it, and selection is stored as keys. */
|
|
11
|
+
getKey: (item: T) => MediaGridKey;
|
|
12
|
+
/** Name of an item: the image's `alt`, and part of the delete button's name. */
|
|
13
|
+
getLabel: (item: T) => string;
|
|
14
|
+
/** Address of the image or video. */
|
|
15
|
+
getUrl: (item: T) => string;
|
|
16
|
+
/** Whether an item is a video. Without it, or when it returns nothing, the URL's extension decides. */
|
|
17
|
+
getType?: (item: T) => MediaGridItemType | undefined;
|
|
18
|
+
/** A still image for a video, shown instead of loading the video's first frame. */
|
|
19
|
+
getPoster?: (item: T) => string | undefined;
|
|
20
|
+
/** Key of the selected item, or `null`; supports `bind:selected`. Used unless `multiple`. */
|
|
21
|
+
selected?: MediaGridKey | null;
|
|
22
|
+
/** Any number of items can be selected; the keys are in `selectedKeys`. */
|
|
23
|
+
multiple?: boolean;
|
|
24
|
+
/** Keys of the selected items; supports `bind:selectedKeys`. Used when `multiple`. */
|
|
25
|
+
selectedKeys?: MediaGridKey[];
|
|
26
|
+
/** Called when an item is selected or its selection is cleared. */
|
|
27
|
+
onselect?: (detail: MediaGridSelectDetail<T>) => void;
|
|
28
|
+
/**
|
|
29
|
+
* Shows a delete button on each item and reports which one was
|
|
30
|
+
* activated. The grid neither confirms nor removes: confirm, then
|
|
31
|
+
* take the item out of `items`.
|
|
32
|
+
*/
|
|
33
|
+
ondelete?: (item: T) => void;
|
|
34
|
+
/** Leaves the delete button off one item. */
|
|
35
|
+
isItemDeletable?: (item: T) => boolean;
|
|
36
|
+
/** Adds placeholder tiles after the items, for a first load or a next page. */
|
|
37
|
+
loading?: boolean;
|
|
38
|
+
/** How many placeholder tiles `loading` shows. */
|
|
39
|
+
loadingCount?: number;
|
|
40
|
+
/** Replaces the built-in empty state. */
|
|
41
|
+
empty?: Snippet;
|
|
42
|
+
/**
|
|
43
|
+
* Heading element of the built-in empty state's title. 3 by default:
|
|
44
|
+
* the grid usually sits under a Modal or section title.
|
|
45
|
+
*/
|
|
46
|
+
emptyHeadingLevel?: CollapsibleHeadingLevel;
|
|
47
|
+
/** Disables selecting and deleting. */
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
/** Smallest tile edge, as px or any CSS length. The grid fits as many columns as that allows. */
|
|
50
|
+
minTileSize?: number | string;
|
|
51
|
+
/** Overrides for the built-in strings. */
|
|
52
|
+
strings?: Partial<MediaGridStrings>;
|
|
53
|
+
"aria-label"?: string;
|
|
54
|
+
"aria-labelledby"?: string;
|
|
55
|
+
};
|
|
56
|
+
exports: {};
|
|
57
|
+
bindings: "selected" | "selectedKeys";
|
|
58
|
+
slots: {};
|
|
59
|
+
events: {};
|
|
60
|
+
};
|
|
61
|
+
declare class __sveltets_Render<T> {
|
|
62
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
63
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
64
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
65
|
+
bindings(): "selected" | "selectedKeys";
|
|
66
|
+
exports(): {};
|
|
67
|
+
}
|
|
68
|
+
interface $$IsomorphicComponent {
|
|
69
|
+
new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
70
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
71
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
72
|
+
<T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
73
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
74
|
+
}
|
|
75
|
+
declare const MediaGrid: $$IsomorphicComponent;
|
|
76
|
+
type MediaGrid<T> = InstanceType<typeof MediaGrid<T>>;
|
|
77
|
+
export default MediaGrid;
|