zabi-components 8.0.0 → 9.0.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 +2935 -0
- package/README.md +243 -10
- package/THEME.md +162 -332
- package/THEMING.md +1419 -259
- package/dist/atoms/ActionPanel.svelte +15 -5
- package/dist/atoms/Avatar.svelte +120 -0
- package/dist/atoms/Avatar.svelte.d.ts +46 -0
- package/dist/atoms/Badge.svelte +71 -13
- package/dist/atoms/Badge.svelte.d.ts +8 -2
- package/dist/atoms/Block.svelte +74 -0
- package/dist/atoms/Block.svelte.d.ts +29 -0
- package/dist/atoms/Button.svelte +217 -30
- package/dist/atoms/Button.svelte.d.ts +33 -4
- package/dist/atoms/Card.svelte +182 -26
- package/dist/atoms/Card.svelte.d.ts +55 -3
- package/dist/atoms/CardHeader.svelte +16 -4
- package/dist/atoms/Checkbox.svelte +81 -21
- package/dist/atoms/Checkbox.svelte.d.ts +31 -2
- package/dist/atoms/Chip.svelte +351 -0
- package/dist/atoms/Chip.svelte.d.ts +57 -0
- package/dist/atoms/CodeBlock.svelte +20 -2
- package/dist/atoms/CodeBlock.svelte.d.ts +4 -0
- package/dist/atoms/CodeField.svelte +385 -0
- package/dist/atoms/CodeField.svelte.d.ts +61 -0
- package/dist/atoms/ColorPicker.svelte +260 -49
- package/dist/atoms/ColorPicker.svelte.d.ts +3 -0
- package/dist/atoms/Container.svelte +16 -8
- package/dist/atoms/DateField.svelte +145 -0
- package/dist/atoms/DateField.svelte.d.ts +81 -0
- package/dist/atoms/FieldMessages.svelte +69 -0
- package/dist/atoms/FieldMessages.svelte.d.ts +23 -0
- package/dist/atoms/FloatingActionButton.svelte +176 -0
- package/dist/atoms/FloatingActionButton.svelte.d.ts +51 -0
- package/dist/atoms/Heading.svelte +63 -5
- package/dist/atoms/Heading.svelte.d.ts +14 -0
- package/dist/atoms/IconButton.svelte +294 -26
- package/dist/atoms/IconButton.svelte.d.ts +62 -4
- package/dist/atoms/Input.svelte +259 -53
- package/dist/atoms/Input.svelte.d.ts +33 -2
- package/dist/atoms/List.svelte +5 -1
- package/dist/atoms/ListItem.svelte +70 -13
- package/dist/atoms/ListItem.svelte.d.ts +11 -0
- package/dist/atoms/PickerField.svelte +239 -0
- package/dist/atoms/PickerField.svelte.d.ts +67 -0
- package/dist/atoms/Progress.svelte +99 -19
- package/dist/atoms/Progress.svelte.d.ts +15 -1
- package/dist/atoms/Radio.svelte +51 -3
- package/dist/atoms/Radio.svelte.d.ts +30 -2
- package/dist/atoms/Rating.svelte +620 -0
- package/dist/atoms/Rating.svelte.d.ts +77 -0
- package/dist/atoms/Select.svelte +438 -98
- package/dist/atoms/Select.svelte.d.ts +61 -3
- package/dist/atoms/SelectionControl.svelte +193 -23
- package/dist/atoms/SelectionControl.svelte.d.ts +29 -3
- package/dist/atoms/Skeleton.svelte +114 -18
- package/dist/atoms/Skeleton.svelte.d.ts +30 -3
- package/dist/atoms/SkeletonGroup.svelte +43 -0
- package/dist/atoms/SkeletonGroup.svelte.d.ts +20 -0
- package/dist/atoms/Slider.svelte +344 -0
- package/dist/atoms/Slider.svelte.d.ts +40 -0
- package/dist/atoms/Sparkline.svelte +109 -0
- package/dist/atoms/Sparkline.svelte.d.ts +27 -0
- package/dist/atoms/SpeechBubble.svelte +187 -0
- package/dist/atoms/SpeechBubble.svelte.d.ts +33 -0
- package/dist/atoms/Spinner.svelte +57 -0
- package/dist/atoms/Spinner.svelte.d.ts +19 -0
- package/dist/atoms/Stat.svelte +72 -0
- package/dist/atoms/Stat.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 +472 -0
- package/dist/atoms/TemporalField.svelte.d.ts +48 -0
- package/dist/atoms/Text.svelte +23 -11
- package/dist/atoms/Text.svelte.d.ts +6 -3
- package/dist/atoms/Textarea.svelte +150 -44
- package/dist/atoms/Textarea.svelte.d.ts +35 -2
- package/dist/atoms/ThemeToggle.svelte +223 -44
- package/dist/atoms/ThemeToggle.svelte.d.ts +26 -3
- package/dist/atoms/TimeColumn.svelte +150 -0
- package/dist/atoms/TimeColumn.svelte.d.ts +25 -0
- package/dist/atoms/TimeField.svelte +151 -0
- package/dist/atoms/TimeField.svelte.d.ts +85 -0
- package/dist/atoms/Toast.svelte +22 -8
- package/dist/atoms/Toast.svelte.d.ts +2 -0
- package/dist/atoms/Toggle.svelte +98 -14
- package/dist/atoms/Toggle.svelte.d.ts +26 -2
- package/dist/atoms/Tooltip.svelte +528 -24
- package/dist/atoms/Tooltip.svelte.d.ts +18 -0
- package/dist/atoms/ZabiStringsProvider.svelte +45 -0
- package/dist/atoms/ZabiStringsProvider.svelte.d.ts +27 -0
- package/dist/atoms/index.d.ts +38 -0
- package/dist/atoms/index.js +29 -0
- package/dist/atoms/selection-control.styles.d.ts +47 -5
- package/dist/atoms/selection-control.styles.js +52 -10
- package/dist/components/atoms/index.d.ts +38 -0
- package/dist/components/atoms/index.d.ts.map +1 -1
- package/dist/components/atoms/selection-control.styles.d.ts +47 -5
- package/dist/components/atoms/selection-control.styles.d.ts.map +1 -1
- package/dist/components/index.d.ts +104 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/map.d.ts +11 -0
- package/dist/components/map.d.ts.map +1 -0
- package/dist/components/molecules/index.d.ts +51 -0
- package/dist/components/molecules/index.d.ts.map +1 -1
- package/dist/components/molecules/toast-store.d.ts +83 -7
- package/dist/components/molecules/toast-store.d.ts.map +1 -1
- package/dist/components/organisms/index.d.ts +10 -0
- package/dist/components/organisms/index.d.ts.map +1 -1
- package/dist/components/types/variants.d.ts +24 -1
- package/dist/components/types/variants.d.ts.map +1 -1
- package/dist/components/util/app-shell.d.ts +79 -0
- package/dist/components/util/app-shell.d.ts.map +1 -0
- package/dist/components/util/avatar.d.ts +39 -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 +41 -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/chart.d.ts +140 -0
- package/dist/components/util/chart.d.ts.map +1 -0
- package/dist/components/util/chip.d.ts +66 -0
- package/dist/components/util/chip.d.ts.map +1 -0
- package/dist/components/util/cn.d.ts.map +1 -1
- package/dist/components/util/code-field.d.ts +63 -0
- package/dist/components/util/code-field.d.ts.map +1 -0
- package/dist/components/util/collapsible.d.ts +64 -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/display-voice.d.ts +10 -0
- package/dist/components/util/display-voice.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-control.d.ts +30 -0
- package/dist/components/util/field-control.d.ts.map +1 -0
- package/dist/components/util/field.d.ts +45 -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 +77 -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/map-view.d.ts +144 -0
- package/dist/components/util/map-view.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 +234 -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/progress.d.ts +19 -0
- package/dist/components/util/progress.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-edge.d.ts +33 -0
- package/dist/components/util/scroll-edge.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 +84 -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/skeleton.d.ts +15 -0
- package/dist/components/util/skeleton.d.ts.map +1 -0
- package/dist/components/util/sliding-indicator.d.ts +25 -0
- package/dist/components/util/sliding-indicator.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/temporal-field.d.ts +89 -0
- package/dist/components/util/temporal-field.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 +133 -0
- package/dist/components/util/toaster.d.ts.map +1 -0
- package/dist/components/util/tone-surface.d.ts +34 -0
- package/dist/components/util/tone-surface.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 +35 -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 +142 -0
- package/dist/components/util/zabi-strings.d.ts.map +1 -0
- package/dist/create-theme/cli.js +173 -0
- package/dist/create-theme/index.d.ts +190 -0
- package/dist/create-theme/index.js +1034 -0
- package/dist/create-theme/lib/ramp-math.js +128 -0
- package/dist/create-theme/lib/resolve.js +238 -0
- package/dist/create-theme/theme-data.js +2644 -0
- package/dist/index.d.ts +104 -2
- package/dist/index.js +75 -2
- package/dist/map.d.ts +10 -0
- package/dist/map.js +9 -0
- package/dist/molecules/Alert.svelte +63 -16
- package/dist/molecules/Alert.svelte.d.ts +14 -1
- package/dist/molecules/AppBar.svelte +674 -0
- package/dist/molecules/AppBar.svelte.d.ts +131 -0
- package/dist/molecules/AvatarGroup.svelte +116 -0
- package/dist/molecules/AvatarGroup.svelte.d.ts +40 -0
- package/dist/molecules/BarChart.svelte +381 -0
- package/dist/molecules/BarChart.svelte.d.ts +45 -0
- package/dist/molecules/BottomSheet.svelte +608 -0
- package/dist/molecules/BottomSheet.svelte.d.ts +109 -0
- package/dist/molecules/BottomTabBar.svelte +403 -0
- package/dist/molecules/BottomTabBar.svelte.d.ts +98 -0
- package/dist/molecules/Calendar.svelte +597 -0
- package/dist/molecules/Calendar.svelte.d.ts +72 -0
- package/dist/molecules/ChipGroup.svelte +325 -0
- package/dist/molecules/ChipGroup.svelte.d.ts +41 -0
- package/dist/molecules/Collapsible.svelte +347 -0
- package/dist/molecules/Collapsible.svelte.d.ts +61 -0
- package/dist/molecules/CollapsibleGroup.svelte +136 -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 +257 -0
- package/dist/molecules/ConfirmDialog.svelte.d.ts +58 -0
- package/dist/molecules/ContactForm.svelte +34 -17
- package/dist/molecules/ContactForm.svelte.d.ts +8 -0
- package/dist/molecules/Drawer.svelte +415 -0
- package/dist/molecules/Drawer.svelte.d.ts +55 -0
- package/dist/molecules/Dropdown.svelte +517 -48
- package/dist/molecules/Dropdown.svelte.d.ts +47 -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 +492 -69
- package/dist/molecules/ImageUpload.svelte.d.ts +82 -2
- package/dist/molecules/LineChart.svelte +459 -0
- package/dist/molecules/LineChart.svelte.d.ts +53 -0
- package/dist/molecules/MapView.svelte +565 -0
- package/dist/molecules/MapView.svelte.d.ts +88 -0
- package/dist/molecules/MediaGrid.svelte +527 -0
- package/dist/molecules/MediaGrid.svelte.d.ts +77 -0
- package/dist/molecules/Modal.svelte +293 -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 +523 -0
- package/dist/molecules/PhotoGrid.svelte.d.ts +73 -0
- package/dist/molecules/PhotoViewer.svelte +1213 -0
- package/dist/molecules/PhotoViewer.svelte.d.ts +74 -0
- package/dist/molecules/PieChart.svelte +421 -0
- package/dist/molecules/PieChart.svelte.d.ts +51 -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 +72 -18
- package/dist/molecules/RadioGroup.svelte.d.ts +19 -3
- package/dist/molecules/Section.svelte +1 -1
- package/dist/molecules/SegmentedControl.svelte +456 -0
- package/dist/molecules/SegmentedControl.svelte.d.ts +40 -0
- package/dist/molecules/SidebarBrandHeader.svelte +11 -1
- package/dist/molecules/SidebarBrandHeader.svelte.d.ts +3 -0
- package/dist/molecules/SidebarFooter.svelte +30 -7
- package/dist/molecules/SidebarFooter.svelte.d.ts +3 -0
- package/dist/molecules/SidebarNavSection.svelte +1 -1
- package/dist/molecules/SlideUp.svelte +220 -61
- package/dist/molecules/SlideUp.svelte.d.ts +26 -0
- package/dist/molecules/SortableList.svelte +633 -0
- package/dist/molecules/SortableList.svelte.d.ts +69 -0
- package/dist/molecules/Stepper.svelte +562 -0
- package/dist/molecules/Stepper.svelte.d.ts +39 -0
- package/dist/molecules/StickyActionBar.svelte +250 -0
- package/dist/molecules/StickyActionBar.svelte.d.ts +78 -0
- package/dist/molecules/SwipeableListItem.svelte +334 -0
- package/dist/molecules/SwipeableListItem.svelte.d.ts +54 -0
- package/dist/molecules/Tabs.svelte +310 -9
- package/dist/molecules/Tabs.svelte.d.ts +7 -0
- package/dist/molecules/Toaster.svelte +514 -5
- package/dist/molecules/Toaster.svelte.d.ts +153 -2
- package/dist/molecules/ToasterToast.svelte +495 -77
- package/dist/molecules/ToasterToast.svelte.d.ts +15 -0
- package/dist/molecules/UnsavedChangesBar.svelte +223 -0
- package/dist/molecules/UnsavedChangesBar.svelte.d.ts +43 -0
- package/dist/molecules/index.d.ts +51 -0
- package/dist/molecules/index.js +31 -0
- package/dist/molecules/toast-store.d.ts +83 -7
- package/dist/molecules/toast-store.js +25 -1
- package/dist/organisms/AppNavigation.svelte +440 -0
- package/dist/organisms/AppNavigation.svelte.d.ts +72 -0
- package/dist/organisms/AppShell.svelte +696 -0
- package/dist/organisms/AppShell.svelte.d.ts +157 -0
- package/dist/organisms/AppShellHeaderPart.svelte +163 -0
- package/dist/organisms/AppShellHeaderPart.svelte.d.ts +57 -0
- package/dist/organisms/SidebarAccountPanel.svelte +107 -12
- package/dist/organisms/SidebarAccountPanel.svelte.d.ts +19 -0
- package/dist/organisms/SidebarNavigation.svelte +111 -43
- package/dist/organisms/SidebarNavigation.svelte.d.ts +29 -1
- package/dist/organisms/SidebarPanel.svelte +34 -17
- package/dist/organisms/SidebarPanel.svelte.d.ts +1 -0
- package/dist/organisms/SidebarShell.svelte +164 -14
- package/dist/organisms/SidebarShell.svelte.d.ts +56 -2
- package/dist/organisms/TopNavbar.svelte +353 -39
- package/dist/organisms/TopNavbar.svelte.d.ts +47 -0
- package/dist/organisms/TopNavbarSheetContent.svelte +145 -0
- package/dist/organisms/TopNavbarSheetContent.svelte.d.ts +24 -0
- package/dist/organisms/index.d.ts +10 -0
- package/dist/organisms/index.js +7 -0
- package/dist/types/components.d.ts +21 -1
- package/dist/types/components.d.ts.map +1 -1
- package/dist/types/index.d.ts +1093 -26
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/variants.ts +28 -1
- package/dist/util/app-shell.d.ts +78 -0
- package/dist/util/app-shell.js +97 -0
- package/dist/util/avatar.d.ts +38 -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 +40 -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/chart.d.ts +139 -0
- package/dist/util/chart.js +305 -0
- package/dist/util/chip.d.ts +65 -0
- package/dist/util/chip.js +58 -0
- package/dist/util/cn.js +28 -2
- package/dist/util/code-field.d.ts +62 -0
- package/dist/util/code-field.js +112 -0
- package/dist/util/collapsible.d.ts +63 -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/display-voice.d.ts +9 -0
- package/dist/util/display-voice.js +9 -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-control.d.ts +29 -0
- package/dist/util/field-control.js +32 -0
- package/dist/util/field.d.ts +44 -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 +77 -2
- package/dist/util/focus-utils.js +186 -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/map-view.d.ts +143 -0
- package/dist/util/map-view.js +121 -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 +233 -0
- package/dist/util/photo.js +209 -0
- package/dist/util/portal.d.ts +22 -0
- package/dist/util/portal.js +50 -0
- package/dist/util/progress.d.ts +18 -0
- package/dist/util/progress.js +17 -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-edge.d.ts +32 -0
- package/dist/util/scroll-edge.js +87 -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 +83 -0
- package/dist/util/sheet-drag.js +311 -0
- package/dist/util/sidebar.d.ts +62 -0
- package/dist/util/sidebar.js +38 -0
- package/dist/util/skeleton.d.ts +14 -0
- package/dist/util/skeleton.js +22 -0
- package/dist/util/sliding-indicator.d.ts +24 -0
- package/dist/util/sliding-indicator.js +44 -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/temporal-field.d.ts +88 -0
- package/dist/util/temporal-field.js +136 -0
- package/dist/util/theme-mode.d.ts +79 -0
- package/dist/util/theme-mode.js +121 -0
- package/dist/util/toaster.d.ts +132 -0
- package/dist/util/toaster.js +130 -0
- package/dist/util/tone-surface.d.ts +33 -0
- package/dist/util/tone-surface.js +35 -0
- package/dist/util/tooltip.d.ts +9 -0
- package/dist/util/tooltip.js +18 -0
- package/dist/util/top-navbar.d.ts +34 -0
- package/dist/util/top-navbar.js +28 -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 +141 -0
- package/dist/util/zabi-strings.js +105 -0
- package/dist/zabi-components-colors.css +11 -2
- package/dist/zabi-components-theme-dark-only.css +870 -258
- package/dist/zabi-components-theme-dark.css +870 -258
- package/dist/zabi-components-theme-only.css +2419 -368
- package/dist/zabi-components-theme.css +2419 -368
- package/dist/zabi-components.css +6032 -711
- package/docs/lucide-icons.md +40 -17
- package/docs/theme-imports.md +81 -11
- package/package.json +45 -8
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
const linkClasses =
|
|
41
|
-
"focus-ring focus-ring--nav block cursor-pointer rounded-control px-4 py-2 text-sm text-nav-menu-item no-underline transition-colors duration-150 outline-none hover:bg-nav-menu-hover hover:text-nav-menu-item-hover {className}";
|
|
41
|
+
"focus-ring focus-ring--nav block cursor-pointer rounded-control px-4 py-2 pointer-coarse:py-3 text-sm text-nav-menu-item no-underline transition-colors duration-150 outline-none hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-nav-menu-active {className}";
|
|
42
42
|
</script>
|
|
43
43
|
|
|
44
44
|
{#if asChild}
|
|
@@ -21,10 +21,17 @@
|
|
|
21
21
|
const className = $derived(cn(`${classAttr} ${legacyClass}`));
|
|
22
22
|
</script>
|
|
23
23
|
|
|
24
|
+
<!-- A list of links and disclosure buttons inside the `<nav>`, which is what
|
|
25
|
+
the WAI-ARIA practices recommend for site navigation. It was `role="menubar"`,
|
|
26
|
+
which promises arrow keys between the items and `menuitem` children; it had
|
|
27
|
+
neither, so a screen reader announced a menu bar that did not behave like one.
|
|
28
|
+
`role="list"` restates what `list-none` makes Safari forget.
|
|
29
|
+
|
|
30
|
+
`flex-wrap`: a list that does not fit goes onto a further row. It used
|
|
31
|
+
to run out of its container (310px of items in a 215px box at 375px). -->
|
|
24
32
|
<ul
|
|
25
|
-
class={cn("flex flex-row items-center gap-1 list-none m-0 p-0", className)}
|
|
26
|
-
role="
|
|
27
|
-
aria-orientation="horizontal"
|
|
33
|
+
class={cn("flex flex-row flex-wrap items-center gap-1 list-none m-0 p-0", className)}
|
|
34
|
+
role="list"
|
|
28
35
|
data-navigation-menu-list
|
|
29
36
|
{...restProps}
|
|
30
37
|
>
|
|
@@ -67,10 +67,10 @@
|
|
|
67
67
|
<button
|
|
68
68
|
type="button"
|
|
69
69
|
class={cn(
|
|
70
|
-
"focus-ring focus-ring--nav inline-flex cursor-pointer items-center justify-center gap-2 rounded-control bg-transparent px-4 py-2 text-sm font-medium transition-colors duration-150 outline-none",
|
|
70
|
+
"focus-ring focus-ring--nav inline-flex cursor-pointer items-center justify-center gap-2 rounded-control bg-transparent px-4 py-2 pointer-coarse:py-3 text-sm font-medium transition-colors duration-150 outline-none",
|
|
71
71
|
isActive
|
|
72
72
|
? "bg-nav-menu-active text-nav-menu-item-active"
|
|
73
|
-
: "text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover",
|
|
73
|
+
: "text-nav-menu-item hover:bg-nav-menu-hover hover:text-nav-menu-item-hover active:bg-nav-menu-active",
|
|
74
74
|
className,
|
|
75
75
|
)}
|
|
76
76
|
aria-expanded={isActive ? 'true' : 'false'}
|
|
@@ -1,21 +1,51 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
|
+
import { isInsideAppShell } from "../util/app-shell.js";
|
|
3
4
|
import { cn } from "../util/cn.js";
|
|
4
5
|
|
|
5
6
|
interface Props {
|
|
6
7
|
children?: Snippet;
|
|
8
|
+
/**
|
|
9
|
+
* Keeps the content out from under a notch at the side (a phone held
|
|
10
|
+
* sideways) and the home indicator at the bottom, by padding the page
|
|
11
|
+
* with the safe-area insets. They are zero on a screen without them,
|
|
12
|
+
* and until the app asks for them with `viewport-fit=cover`.
|
|
13
|
+
*
|
|
14
|
+
* Inside an `AppShell` the page adds nothing, whatever this says: the
|
|
15
|
+
* shell has already kept its content clear. Set it to false when
|
|
16
|
+
* something else of yours has, such as padding on `<body>`.
|
|
17
|
+
*/
|
|
18
|
+
safeArea?: boolean;
|
|
7
19
|
class?: string;
|
|
8
20
|
/** @deprecated use `class`. */
|
|
9
21
|
className?: string;
|
|
10
22
|
}
|
|
11
23
|
|
|
12
|
-
let {
|
|
24
|
+
let {
|
|
25
|
+
children,
|
|
26
|
+
safeArea = true,
|
|
27
|
+
class: classAttr = "",
|
|
28
|
+
className: legacyClass = "",
|
|
29
|
+
}: Props = $props();
|
|
30
|
+
|
|
31
|
+
const inShell = isInsideAppShell();
|
|
13
32
|
|
|
14
33
|
/** `class` is the public prop; `className` is a deprecated alias. */
|
|
15
34
|
const className = $derived(cn(`${classAttr} ${legacyClass}`));
|
|
16
35
|
</script>
|
|
17
36
|
|
|
18
|
-
<div
|
|
37
|
+
<div
|
|
38
|
+
class={cn(
|
|
39
|
+
"mx-auto w-full space-y-10",
|
|
40
|
+
// Left and right, not start and end: a notch is on a side of the
|
|
41
|
+
// device, whichever way the text runs.
|
|
42
|
+
safeArea &&
|
|
43
|
+
!inShell &&
|
|
44
|
+
"pb-[env(safe-area-inset-bottom,0px)] pl-[env(safe-area-inset-left,0px)] pr-[env(safe-area-inset-right,0px)]",
|
|
45
|
+
className,
|
|
46
|
+
)}
|
|
47
|
+
data-safe-area={safeArea && !inShell ? "" : undefined}
|
|
48
|
+
>
|
|
19
49
|
{#if children}
|
|
20
50
|
{@render children()}
|
|
21
51
|
{/if}
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import type { Snippet } from "svelte";
|
|
2
2
|
interface Props {
|
|
3
3
|
children?: Snippet;
|
|
4
|
+
/**
|
|
5
|
+
* Keeps the content out from under a notch at the side (a phone held
|
|
6
|
+
* sideways) and the home indicator at the bottom, by padding the page
|
|
7
|
+
* with the safe-area insets. They are zero on a screen without them,
|
|
8
|
+
* and until the app asks for them with `viewport-fit=cover`.
|
|
9
|
+
*
|
|
10
|
+
* Inside an `AppShell` the page adds nothing, whatever this says: the
|
|
11
|
+
* shell has already kept its content clear. Set it to false when
|
|
12
|
+
* something else of yours has, such as padding on `<body>`.
|
|
13
|
+
*/
|
|
14
|
+
safeArea?: boolean;
|
|
4
15
|
class?: string;
|
|
5
16
|
/** @deprecated use `class`. */
|
|
6
17
|
className?: string;
|
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { mergeStrings } from "../util/ready-made-strings.js";
|
|
3
|
+
import { zabiStringsFor } from "../util/zabi-strings.js";
|
|
4
|
+
import { untrack } from "svelte";
|
|
5
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
6
|
+
import Check from "@lucide/svelte/icons/check";
|
|
7
|
+
import CircleAlert from "@lucide/svelte/icons/circle-alert";
|
|
8
|
+
import ImageOff from "@lucide/svelte/icons/image-off";
|
|
9
|
+
import Maximize2 from "@lucide/svelte/icons/maximize-2";
|
|
10
|
+
import Plus from "@lucide/svelte/icons/plus";
|
|
11
|
+
import RotateCw from "@lucide/svelte/icons/rotate-cw";
|
|
12
|
+
import Skeleton from "../atoms/Skeleton.svelte";
|
|
13
|
+
import { isDevBuild } from "../util/app-shell.js";
|
|
14
|
+
import { cn } from "../util/cn.js";
|
|
15
|
+
import { columnCount, moveForKey, moveIndex } from "../util/media-grid.js";
|
|
16
|
+
import {
|
|
17
|
+
PHOTO_GRID_STRINGS,
|
|
18
|
+
photoKey,
|
|
19
|
+
visibleCount,
|
|
20
|
+
type Photo,
|
|
21
|
+
type PhotoGridSelectDetail,
|
|
22
|
+
type PhotoGridStrings,
|
|
23
|
+
type PhotoKey,
|
|
24
|
+
} from "../util/photo.js";
|
|
25
|
+
import { generateId } from "../util/ssr-safe.js";
|
|
26
|
+
import { TOUCH_HIT_AREA } from "../util/touch-target.js";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A grid of square photo thumbnails. A press on one tells you which, so
|
|
30
|
+
* you can open it in a `PhotoViewer`:
|
|
31
|
+
*
|
|
32
|
+
* ```svelte
|
|
33
|
+
* <PhotoGrid {photos} max={9} onopen={(at) => { index = at; open = true; }} onadd={pickFiles} />
|
|
34
|
+
* <PhotoViewer {photos} bind:index bind:isOpen={open} />
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* With `selectable` a press selects instead, for choosing a cover photo
|
|
38
|
+
* (`single`) or photos to delete (`multiple`); each tile then has a small
|
|
39
|
+
* button of its own that opens it. In `single` a press on the selected
|
|
40
|
+
* photo changes nothing: the selection is never cleared by pressing it
|
|
41
|
+
* again.
|
|
42
|
+
*
|
|
43
|
+
* The grid is one Tab stop; the arrow keys move between the tiles.
|
|
44
|
+
*
|
|
45
|
+
* Other attributes (`id`, `data-*`, ...) land on the host `<div>`; the
|
|
46
|
+
* two ARIA names go to the list.
|
|
47
|
+
*/
|
|
48
|
+
type Props = Omit<
|
|
49
|
+
HTMLAttributes<HTMLDivElement>,
|
|
50
|
+
// `onselect` is also a native event (text selection); ours replaces it.
|
|
51
|
+
"class" | "aria-label" | "aria-labelledby" | "onselect"
|
|
52
|
+
> & {
|
|
53
|
+
photos: Photo[];
|
|
54
|
+
/**
|
|
55
|
+
* How many columns. Left out, it follows the width of the grid: 3
|
|
56
|
+
* under 480px, 4 from there and 5 from 768px.
|
|
57
|
+
*/
|
|
58
|
+
columns?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Shows no more than this many photos. The last tile then says how
|
|
61
|
+
* many more there are ("+12") and opens the viewer at its photo.
|
|
62
|
+
*/
|
|
63
|
+
max?: number;
|
|
64
|
+
/** Called with the photo's place in `photos` when one is opened. */
|
|
65
|
+
onopen?: (index: number) => void;
|
|
66
|
+
/**
|
|
67
|
+
* Makes a press select: `single` for one photo, `multiple` for any
|
|
68
|
+
* number, with a check mark on each selected tile.
|
|
69
|
+
*/
|
|
70
|
+
selectable?: "single" | "multiple";
|
|
71
|
+
/** Key of the selected photo, or `null`; supports `bind:selected`. Used with `selectable="single"`. */
|
|
72
|
+
selected?: PhotoKey | null;
|
|
73
|
+
/** Keys of the selected photos; supports `bind:selectedKeys`. Used with `selectable="multiple"`. */
|
|
74
|
+
selectedKeys?: PhotoKey[];
|
|
75
|
+
/** Called when a photo is selected or, in `multiple`, its selection is cleared. */
|
|
76
|
+
onselect?: (detail: PhotoGridSelectDetail) => void;
|
|
77
|
+
/**
|
|
78
|
+
* Puts an "Add photo" tile first and is called when it is pressed.
|
|
79
|
+
* Open your own file picker or an ImageUpload from it.
|
|
80
|
+
*/
|
|
81
|
+
onadd?: () => void;
|
|
82
|
+
/**
|
|
83
|
+
* Called once when a thumbnail cannot be loaded, so the page can answer
|
|
84
|
+
* with a new address (a signed one that has run out). A new `thumbSrc`
|
|
85
|
+
* or `src` for the same photo is tried at once.
|
|
86
|
+
*/
|
|
87
|
+
onloaderror?: (photo: Photo, index: number) => void;
|
|
88
|
+
/**
|
|
89
|
+
* Puts a "Try again" button on each photo whose `status` is `error`
|
|
90
|
+
* and is called when it is pressed.
|
|
91
|
+
*/
|
|
92
|
+
onretry?: (photo: Photo, index: number) => void;
|
|
93
|
+
/** Overrides for the built-in strings. */
|
|
94
|
+
strings?: Partial<PhotoGridStrings>;
|
|
95
|
+
/** Extra classes for the host element. */
|
|
96
|
+
class?: string;
|
|
97
|
+
"aria-label"?: string;
|
|
98
|
+
"aria-labelledby"?: string;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
let {
|
|
102
|
+
photos,
|
|
103
|
+
columns,
|
|
104
|
+
max,
|
|
105
|
+
onopen,
|
|
106
|
+
selectable,
|
|
107
|
+
selected = $bindable<Exclude<Props["selected"], undefined>>(),
|
|
108
|
+
selectedKeys = $bindable<Exclude<Props["selectedKeys"], undefined>>(),
|
|
109
|
+
onselect,
|
|
110
|
+
onadd,
|
|
111
|
+
onloaderror,
|
|
112
|
+
onretry,
|
|
113
|
+
strings,
|
|
114
|
+
class: className = "",
|
|
115
|
+
"aria-label": ariaLabel,
|
|
116
|
+
"aria-labelledby": ariaLabelledby,
|
|
117
|
+
...restProps
|
|
118
|
+
}: Props = $props();
|
|
119
|
+
|
|
120
|
+
// No fallback on a bindable prop: Svelte refuses `bind:…={undefined}` on
|
|
121
|
+
// one that has a fallback (`props_invalid_value`), and a page that throws
|
|
122
|
+
// while it hydrates never becomes interactive. The default is applied
|
|
123
|
+
// here instead: at once, for the server and the first render, and again
|
|
124
|
+
// whenever a parent hands back `undefined`.
|
|
125
|
+
const applyDefaults = () => {
|
|
126
|
+
if (selected === undefined) selected = null;
|
|
127
|
+
if (selectedKeys === undefined) selectedKeys = [];
|
|
128
|
+
};
|
|
129
|
+
applyDefaults();
|
|
130
|
+
$effect.pre(applyDefaults);
|
|
131
|
+
|
|
132
|
+
/** The add tile's place among the tile keys. No photo has this key: it is not an address. */
|
|
133
|
+
const ADD_KEY = "\u0000add";
|
|
134
|
+
type TileKey = PhotoKey;
|
|
135
|
+
|
|
136
|
+
/** The app-wide words for this component, from a `ZabiStringsProvider` above it, if there is one. */
|
|
137
|
+
const provided = zabiStringsFor("photoGrid");
|
|
138
|
+
const text = $derived(mergeStrings(PHOTO_GRID_STRINGS, provided(), strings));
|
|
139
|
+
const hintId = generateId("photo-grid-hint");
|
|
140
|
+
|
|
141
|
+
const counts = $derived(visibleCount(photos.length, max));
|
|
142
|
+
const shown = $derived(photos.slice(0, counts.shown));
|
|
143
|
+
const multiple = $derived(selectable === "multiple");
|
|
144
|
+
|
|
145
|
+
/** Every tile in order, by key: the add tile, then the photos shown. */
|
|
146
|
+
const tileKeys = $derived<TileKey[]>([...(onadd ? [ADD_KEY] : []), ...shown.map(photoKey)]);
|
|
147
|
+
|
|
148
|
+
$effect(() => {
|
|
149
|
+
if (!isDevBuild()) return;
|
|
150
|
+
const unnamed = photos.filter((photo) => !photo.alt?.trim()).length;
|
|
151
|
+
if (unnamed > 0) {
|
|
152
|
+
console.warn(
|
|
153
|
+
`[zabi-components] PhotoGrid: ${unnamed} of ${photos.length} photos have no alt text. ` +
|
|
154
|
+
'Each is named "Photo 3 of 12" until it has one.',
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
function nameOf(photo: Photo, index: number): string {
|
|
160
|
+
return photo.alt?.trim() || text.photoName(index + 1, photos.length);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let host: HTMLDivElement | undefined = $state();
|
|
164
|
+
/** Thumbnails that have loaded, and those that failed; a changed address is tried again. */
|
|
165
|
+
let loaded = $state<string[]>([]);
|
|
166
|
+
let failed = $state<string[]>([]);
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* An image that was already there when the page started running (the
|
|
170
|
+
* server sent it, the cache had it) fired its `load` before anything was
|
|
171
|
+
* listening.
|
|
172
|
+
*/
|
|
173
|
+
function settled(image: HTMLImageElement) {
|
|
174
|
+
if (!image.complete) return;
|
|
175
|
+
const address = image.getAttribute("src") ?? "";
|
|
176
|
+
// Untracked: this runs as an attachment, which would otherwise run
|
|
177
|
+
// again for every image each time one of the lists changes.
|
|
178
|
+
untrack(() => {
|
|
179
|
+
if (image.naturalWidth > 0) markLoaded(address);
|
|
180
|
+
else if (markFailed(address)) reportFailed(address);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Tells the page which photo an address that failed belongs to. */
|
|
185
|
+
function reportFailed(address: string) {
|
|
186
|
+
if (!onloaderror) return;
|
|
187
|
+
const index = photos.findIndex((photo) => (photo.thumbSrc ?? photo.src) === address);
|
|
188
|
+
if (index !== -1) onloaderror(photos[index], index);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function markLoaded(address: string) {
|
|
192
|
+
if (!loaded.includes(address)) loaded = [...loaded, address];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** True the first time an address fails, so the page is told once. */
|
|
196
|
+
function markFailed(address: string): boolean {
|
|
197
|
+
if (failed.includes(address)) return false;
|
|
198
|
+
failed = [...failed, address];
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function nameWithStatus(photo: Photo, name: string): string {
|
|
203
|
+
if (photo.status === "uploading") return `${name}, ${text.uploading}`;
|
|
204
|
+
if (photo.status === "error") return `${name}, ${text.uploadFailed}`;
|
|
205
|
+
return name;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** "40%" for an upload that says how far it has come; nothing for one that does not. */
|
|
209
|
+
function percent(progress: number | undefined): string {
|
|
210
|
+
if (progress === undefined || !Number.isFinite(progress)) return "";
|
|
211
|
+
return `${Math.round(Math.min(Math.max(progress, 0), 1) * 100)}%`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The tile the arrow keys last reached. Only its buttons are in the Tab
|
|
216
|
+
* order (a roving tabindex), so the grid is one stop however many photos
|
|
217
|
+
* it holds.
|
|
218
|
+
*/
|
|
219
|
+
let activeKey = $state<TileKey | null>(null);
|
|
220
|
+
const tabbableKey = $derived.by(() => {
|
|
221
|
+
if (activeKey !== null && tileKeys.includes(activeKey)) return activeKey;
|
|
222
|
+
if (selectable === "single" && selected !== null && tileKeys.includes(selected)) return selected;
|
|
223
|
+
return tileKeys[0] ?? null;
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
/** As in MediaGrid: the hint describes the tile focus arrives on, and only that one. */
|
|
227
|
+
let focusInside = $state(false);
|
|
228
|
+
let entryKey = $state<TileKey | null>(null);
|
|
229
|
+
const describedKey = $derived(focusInside ? entryKey : tabbableKey);
|
|
230
|
+
|
|
231
|
+
function handleTileFocus(key: TileKey) {
|
|
232
|
+
// Runs before the host's own focusin, which sets `focusInside`.
|
|
233
|
+
if (!focusInside) entryKey = key;
|
|
234
|
+
activeKey = key;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function handleHostFocusOut(event: FocusEvent) {
|
|
238
|
+
const next = event.relatedTarget;
|
|
239
|
+
if (!(next instanceof Node) || !host?.contains(next)) focusInside = false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function isSelected(key: PhotoKey): boolean {
|
|
243
|
+
if (!selectable) return false;
|
|
244
|
+
return multiple ? selectedKeys.includes(key) : selected === key;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function toggle(photo: Photo, index: number) {
|
|
248
|
+
const key = photoKey(photo);
|
|
249
|
+
const on = !isSelected(key);
|
|
250
|
+
if (multiple) {
|
|
251
|
+
selectedKeys = on
|
|
252
|
+
? [...selectedKeys, key]
|
|
253
|
+
: selectedKeys.filter((candidate) => candidate !== key);
|
|
254
|
+
onselect?.({ photo, index, selected: on, keys: selectedKeys });
|
|
255
|
+
} else {
|
|
256
|
+
// A single choice is never cleared by pressing it again; only the
|
|
257
|
+
// checkbox-like `multiple` mode toggles off.
|
|
258
|
+
if (!on) return;
|
|
259
|
+
selected = key;
|
|
260
|
+
onselect?.({ photo, index, selected: true, keys: [key] });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** The tile that stands for the photos not shown always opens: it is not one photo to select. */
|
|
265
|
+
function press(photo: Photo, index: number, standsForMore: boolean) {
|
|
266
|
+
if (selectable && !standsForMore) toggle(photo, index);
|
|
267
|
+
else onopen?.(index);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function mainButtons(): HTMLElement[] {
|
|
271
|
+
return host ? Array.from(host.querySelectorAll<HTMLElement>("[data-photo-grid-item]")) : [];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
275
|
+
const tile = (event.target as HTMLElement).closest<HTMLElement>("[data-photo-grid-tile]");
|
|
276
|
+
if (!tile || event.altKey || event.metaKey) return;
|
|
277
|
+
const buttons = mainButtons();
|
|
278
|
+
const index = buttons.findIndex((button) => tile.contains(button));
|
|
279
|
+
if (index === -1) return;
|
|
280
|
+
// The arrows follow what is on screen, which is mirrored in RTL.
|
|
281
|
+
const move = moveForKey(event, getComputedStyle(tile).direction === "rtl");
|
|
282
|
+
if (!move) return;
|
|
283
|
+
// Also keeps the arrow keys from scrolling the page under the grid.
|
|
284
|
+
event.preventDefault();
|
|
285
|
+
const perRow = columnCount(buttons.map((button) => button.getBoundingClientRect().top));
|
|
286
|
+
const target = moveIndex(index, move, perRow, buttons.length);
|
|
287
|
+
if (target !== index) buttons[target]?.focus();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* The tile's radius is the container role's. What sits 4px inside a
|
|
292
|
+
* corner of it (the check mark, the open button) takes that radius less
|
|
293
|
+
* the 4px, so the two curves stay parallel.
|
|
294
|
+
*/
|
|
295
|
+
const cornerMark =
|
|
296
|
+
"absolute flex items-center justify-center rounded-[calc(var(--radius-container)-4px)]";
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The tile's edge is an outline drawn inside it, not a border: a border
|
|
300
|
+
* would move everything in the tile in by its own width, and by another
|
|
301
|
+
* pixel when the tile is selected, and the marks would no longer be 4px
|
|
302
|
+
* from the corner they are rounded for.
|
|
303
|
+
*/
|
|
304
|
+
const tileButton =
|
|
305
|
+
"focus-ring relative block size-full cursor-pointer overflow-hidden rounded-container bg-(--color-surface-2) transition-[outline-color,scale] duration-150 motion-reduce:transition-none";
|
|
306
|
+
</script>
|
|
307
|
+
|
|
308
|
+
<div
|
|
309
|
+
bind:this={host}
|
|
310
|
+
class={cn("group/photos @container relative", className)}
|
|
311
|
+
data-photo-grid
|
|
312
|
+
onfocusin={() => (focusInside = true)}
|
|
313
|
+
onfocusout={handleHostFocusOut}
|
|
314
|
+
{...restProps}
|
|
315
|
+
>
|
|
316
|
+
<!-- The reset removes the markers, and Safari then drops the list role. -->
|
|
317
|
+
<!-- The gap is in px: it is the line between two photos, not text spacing. -->
|
|
318
|
+
<!-- svelte-ignore a11y_no_redundant_roles, a11y_no_noninteractive_element_interactions -->
|
|
319
|
+
<ul
|
|
320
|
+
role="list"
|
|
321
|
+
class={cn(
|
|
322
|
+
"m-0 grid list-none gap-[4px] p-0",
|
|
323
|
+
// By the width of the grid itself, so it is right in a sheet or a
|
|
324
|
+
// sidebar too. In px: the count must not change with the text size.
|
|
325
|
+
columns === undefined && "grid-cols-3 @[480px]:grid-cols-4 @[768px]:grid-cols-5",
|
|
326
|
+
)}
|
|
327
|
+
style:grid-template-columns={columns === undefined
|
|
328
|
+
? undefined
|
|
329
|
+
: `repeat(${Math.max(1, Math.floor(columns))}, minmax(0, 1fr))`}
|
|
330
|
+
aria-label={ariaLabel}
|
|
331
|
+
aria-labelledby={ariaLabelledby}
|
|
332
|
+
onkeydown={handleKeydown}
|
|
333
|
+
>
|
|
334
|
+
{#if onadd}
|
|
335
|
+
<li
|
|
336
|
+
class="@container relative aspect-square"
|
|
337
|
+
data-photo-grid-tile
|
|
338
|
+
onfocusin={() => handleTileFocus(ADD_KEY)}
|
|
339
|
+
>
|
|
340
|
+
<!-- A dashed edge the tile depends on to be seen, so in the
|
|
341
|
+
control-border colour; the label is shown, not only read out.
|
|
342
|
+
Nothing leaves the tile: the label is cut at two lines (the
|
|
343
|
+
name keeps the whole text), and the plus is drawn only where
|
|
344
|
+
the tile has room for it above two lines, which at 200% text
|
|
345
|
+
on a phone it has not (Z-136). -->
|
|
346
|
+
<button
|
|
347
|
+
type="button"
|
|
348
|
+
class="focus-ring flex size-full cursor-pointer flex-col items-center justify-center gap-1 overflow-hidden rounded-container border border-dashed border-control-border bg-transparent p-1 text-center text-xs font-medium text-body transition-colors duration-150 hover:bg-surface-hover active:bg-surface-active motion-reduce:transition-none"
|
|
349
|
+
aria-label={text.addPhoto}
|
|
350
|
+
aria-describedby={describedKey === ADD_KEY ? hintId : undefined}
|
|
351
|
+
tabindex={tabbableKey === ADD_KEY ? 0 : -1}
|
|
352
|
+
data-photo-grid-item
|
|
353
|
+
data-photo-grid-add
|
|
354
|
+
onclick={() => onadd?.()}
|
|
355
|
+
>
|
|
356
|
+
<Plus size={24} class="hidden shrink-0 @min-[calc(24px+3.5rem)]:block" aria-hidden="true" />
|
|
357
|
+
<span class="line-clamp-2 [overflow-wrap:anywhere]">{text.addPhoto}</span>
|
|
358
|
+
</button>
|
|
359
|
+
</li>
|
|
360
|
+
{/if}
|
|
361
|
+
{#each shown as photo, index (photoKey(photo))}
|
|
362
|
+
{@const key = photoKey(photo)}
|
|
363
|
+
{@const name = nameOf(photo, index)}
|
|
364
|
+
{@const source = photo.thumbSrc ?? photo.src}
|
|
365
|
+
{@const more = counts.hidden > 0 && index === shown.length - 1}
|
|
366
|
+
{@const on = isSelected(key)}
|
|
367
|
+
{@const tabindex = key === tabbableKey ? 0 : -1}
|
|
368
|
+
{@const selects = !!selectable && !more}
|
|
369
|
+
<li
|
|
370
|
+
class="relative aspect-square"
|
|
371
|
+
data-photo-grid-tile
|
|
372
|
+
data-selected={on ? "" : undefined}
|
|
373
|
+
onfocusin={() => handleTileFocus(key)}
|
|
374
|
+
>
|
|
375
|
+
<!-- Selection is a thicker edge and a check mark, not a colour
|
|
376
|
+
alone. `data-photo-key` is how a PhotoViewer finds the tile of
|
|
377
|
+
the photo it shows, to give focus back to it. -->
|
|
378
|
+
<button
|
|
379
|
+
type="button"
|
|
380
|
+
class={cn(
|
|
381
|
+
tileButton,
|
|
382
|
+
on
|
|
383
|
+
? "outline-2 -outline-offset-2 outline-(--color-action-primary)"
|
|
384
|
+
: "outline-1 -outline-offset-1 outline-(--color-border) hover:outline-(--color-border-medium) active:scale-[0.98] motion-reduce:active:scale-100",
|
|
385
|
+
)}
|
|
386
|
+
aria-label={more ? `${name}, ${text.morePhotos(counts.hidden)}` : nameWithStatus(photo, name)}
|
|
387
|
+
aria-pressed={selects ? on : undefined}
|
|
388
|
+
aria-describedby={key === describedKey ? hintId : undefined}
|
|
389
|
+
{tabindex}
|
|
390
|
+
data-photo-grid-item
|
|
391
|
+
data-photo-key={key}
|
|
392
|
+
onclick={() => press(photo, index, more)}
|
|
393
|
+
>
|
|
394
|
+
{#if failed.includes(source)}
|
|
395
|
+
<span
|
|
396
|
+
class="flex size-full items-center justify-center text-description"
|
|
397
|
+
data-photo-grid-fallback
|
|
398
|
+
>
|
|
399
|
+
<ImageOff size={24} aria-hidden="true" />
|
|
400
|
+
</span>
|
|
401
|
+
{:else}
|
|
402
|
+
{#if !loaded.includes(source)}
|
|
403
|
+
<span class="absolute inset-0" aria-hidden="true" data-photo-grid-skeleton>
|
|
404
|
+
<Skeleton variant="block" class="h-full rounded-none" />
|
|
405
|
+
</span>
|
|
406
|
+
{/if}
|
|
407
|
+
<!-- The button carries the name; the image would only repeat it. -->
|
|
408
|
+
<img
|
|
409
|
+
src={source}
|
|
410
|
+
alt=""
|
|
411
|
+
loading="lazy"
|
|
412
|
+
decoding="async"
|
|
413
|
+
draggable="false"
|
|
414
|
+
class={cn(
|
|
415
|
+
"relative size-full object-cover transition-opacity duration-200 motion-reduce:transition-none",
|
|
416
|
+
loaded.includes(source) ? "opacity-100" : "opacity-0",
|
|
417
|
+
)}
|
|
418
|
+
onload={() => markLoaded(source)}
|
|
419
|
+
onerror={() => {
|
|
420
|
+
if (markFailed(source)) onloaderror?.(photo, index);
|
|
421
|
+
}}
|
|
422
|
+
{@attach settled}
|
|
423
|
+
/>
|
|
424
|
+
{/if}
|
|
425
|
+
{#if on}
|
|
426
|
+
<span
|
|
427
|
+
class={cn(cornerMark, "start-[4px] top-[4px] size-6 bg-action-primary text-action-primary")}
|
|
428
|
+
data-photo-grid-check
|
|
429
|
+
>
|
|
430
|
+
<Check size={14} aria-hidden="true" />
|
|
431
|
+
</span>
|
|
432
|
+
{/if}
|
|
433
|
+
{#if photo.status && !more}
|
|
434
|
+
<!-- On its way, or not arrived: the photo is dimmed and
|
|
435
|
+
the mark sits on an opaque plate, as the "+12" does. The
|
|
436
|
+
name says it in words. -->
|
|
437
|
+
<span
|
|
438
|
+
class="absolute inset-0 flex items-center justify-center bg-overlay"
|
|
439
|
+
aria-hidden="true"
|
|
440
|
+
data-photo-grid-status={photo.status}
|
|
441
|
+
>
|
|
442
|
+
<span
|
|
443
|
+
class="flex min-h-8 min-w-8 items-center justify-center rounded-control border border-control-border bg-surface-overlay px-1 text-xs font-semibold text-headline tabular-nums"
|
|
444
|
+
>
|
|
445
|
+
{#if photo.status === "error"}
|
|
446
|
+
<CircleAlert size={18} class="text-error-text" />
|
|
447
|
+
{:else if percent(photo.progress)}
|
|
448
|
+
{percent(photo.progress)}
|
|
449
|
+
{:else}
|
|
450
|
+
<span
|
|
451
|
+
class="inline-block size-4 shrink-0 animate-spin rounded-full border-2 border-current border-t-transparent motion-reduce:animate-pulse"
|
|
452
|
+
></span>
|
|
453
|
+
{/if}
|
|
454
|
+
</span>
|
|
455
|
+
</span>
|
|
456
|
+
{/if}
|
|
457
|
+
{#if more}
|
|
458
|
+
<!-- The photo is dimmed and the number sits on an opaque
|
|
459
|
+
plate, so it reads on any photo in either theme. The
|
|
460
|
+
name says it in words. -->
|
|
461
|
+
<span
|
|
462
|
+
class="absolute inset-0 flex items-center justify-center bg-overlay"
|
|
463
|
+
aria-hidden="true"
|
|
464
|
+
data-photo-grid-more
|
|
465
|
+
>
|
|
466
|
+
<span
|
|
467
|
+
class="rounded-control border border-control-border bg-surface-overlay px-2 py-1 text-base font-semibold text-headline"
|
|
468
|
+
>
|
|
469
|
+
+{counts.hidden}
|
|
470
|
+
</span>
|
|
471
|
+
</span>
|
|
472
|
+
{/if}
|
|
473
|
+
</button>
|
|
474
|
+
{#if photo.status === "error" && onretry && !more}
|
|
475
|
+
<!-- Beside the tile's button, in the other bottom corner
|
|
476
|
+
from the one that opens. -->
|
|
477
|
+
<button
|
|
478
|
+
type="button"
|
|
479
|
+
class={cn(
|
|
480
|
+
cornerMark,
|
|
481
|
+
TOUCH_HIT_AREA,
|
|
482
|
+
"focus-ring bottom-[4px] start-[4px] size-7 cursor-pointer border border-control-border bg-surface-overlay text-body transition-colors duration-150 hover:text-headline motion-reduce:transition-none",
|
|
483
|
+
)}
|
|
484
|
+
aria-label={text.retryUpload(name)}
|
|
485
|
+
{tabindex}
|
|
486
|
+
data-photo-grid-retry
|
|
487
|
+
onclick={() => onretry?.(photo, index)}
|
|
488
|
+
>
|
|
489
|
+
<RotateCw size={14} aria-hidden="true" />
|
|
490
|
+
</button>
|
|
491
|
+
{/if}
|
|
492
|
+
{#if selects && onopen}
|
|
493
|
+
<!-- Beside the tile's button, not inside it. An opaque
|
|
494
|
+
plate, so it reads over any photo; on a coarse pointer an
|
|
495
|
+
invisible layer widens what takes the tap to 44px. -->
|
|
496
|
+
<button
|
|
497
|
+
type="button"
|
|
498
|
+
class={cn(
|
|
499
|
+
cornerMark,
|
|
500
|
+
TOUCH_HIT_AREA,
|
|
501
|
+
"focus-ring bottom-[4px] end-[4px] size-7 cursor-pointer border border-control-border bg-surface-overlay text-body transition-colors duration-150 hover:text-headline motion-reduce:transition-none",
|
|
502
|
+
)}
|
|
503
|
+
aria-label={text.openPhoto(name)}
|
|
504
|
+
{tabindex}
|
|
505
|
+
data-photo-grid-open
|
|
506
|
+
onclick={() => onopen?.(index)}
|
|
507
|
+
>
|
|
508
|
+
<Maximize2 size={14} aria-hidden="true" />
|
|
509
|
+
</button>
|
|
510
|
+
{/if}
|
|
511
|
+
</li>
|
|
512
|
+
{/each}
|
|
513
|
+
</ul>
|
|
514
|
+
<!-- Hidden until the grid has keyboard focus: a sighted keyboard user
|
|
515
|
+
needs it as much as a screen reader user, and a pointer user does not. -->
|
|
516
|
+
<p
|
|
517
|
+
id={hintId}
|
|
518
|
+
class="mt-2 hidden text-xs text-description group-has-[:focus-visible]/photos:block"
|
|
519
|
+
data-photo-grid-hint
|
|
520
|
+
>
|
|
521
|
+
{text.keyboardHint}
|
|
522
|
+
</p>
|
|
523
|
+
</div>
|