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
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { DEFAULT_IMAGE_UPLOAD_TEXTS, zabiStringsFor } from "../util/zabi-strings.js";
|
|
3
|
+
import { onDestroy, tick, untrack, type Snippet } from "svelte";
|
|
3
4
|
import type { HTMLAttributes } from "svelte/elements";
|
|
4
5
|
import Button from "../atoms/Button.svelte";
|
|
5
|
-
import
|
|
6
|
+
import Image from "@lucide/svelte/icons/image";
|
|
6
7
|
import { cn } from "../util/cn.js";
|
|
8
|
+
import { isVideoPath, isVideoUrl } from "../util/media.js";
|
|
9
|
+
import { generateId } from "../util/ssr-safe.js";
|
|
10
|
+
import type { ButtonSize, ButtonVariant } from "../types/variants.js";
|
|
7
11
|
|
|
8
12
|
export type ImageUploadFileDetail = {
|
|
9
13
|
/** Selected file, or `null` when the image is removed. */
|
|
@@ -12,54 +16,256 @@
|
|
|
12
16
|
url: string | null;
|
|
13
17
|
};
|
|
14
18
|
|
|
15
|
-
type
|
|
19
|
+
export type ImageUploadPreviewType = "image" | "video";
|
|
20
|
+
|
|
21
|
+
export type ImageUploadActionsPlacement = "overlay" | "strip";
|
|
22
|
+
|
|
23
|
+
export type ImageUploadVariant = "dropzone" | "button";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Why a file was turned away: `accept` does not allow it, it is over
|
|
27
|
+
* `maxSize`, or (only with `multiple` and `maxFiles`) it is one too many.
|
|
28
|
+
*/
|
|
29
|
+
export type ImageUploadRejectReason = "type" | "size" | "count";
|
|
30
|
+
|
|
31
|
+
export type ImageUploadPreviewDetail = {
|
|
32
|
+
/** The current `value`. */
|
|
33
|
+
url: string;
|
|
34
|
+
/** What the built-in preview would have rendered. */
|
|
35
|
+
type: ImageUploadPreviewType;
|
|
36
|
+
alt: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type Props = Omit<HTMLAttributes<HTMLDivElement>, "onchange" | "onclick" | "id"> & {
|
|
16
40
|
/** Extra classes for the host element. */
|
|
17
41
|
class?: string;
|
|
42
|
+
/**
|
|
43
|
+
* `dropzone` (default): the drop area, and the preview with Change and
|
|
44
|
+
* Remove once there is a value. `button`: only a button that opens the
|
|
45
|
+
* file chooser, for a row of choices; no drop area, no preview and no
|
|
46
|
+
* Remove. The picked file comes through `onfileselect` and `bind:value`,
|
|
47
|
+
* and the app shows it where it wants.
|
|
48
|
+
*/
|
|
49
|
+
variant?: ImageUploadVariant;
|
|
50
|
+
/** `variant="button"`: the trigger's `Button` variant. */
|
|
51
|
+
buttonVariant?: ButtonVariant;
|
|
52
|
+
/** `variant="button"`: the trigger's `Button` size. */
|
|
53
|
+
buttonSize?: ButtonSize;
|
|
54
|
+
/** The largest file taken, in bytes. A larger one goes to `onfilereject` with `"size"`. */
|
|
55
|
+
maxSize?: number;
|
|
56
|
+
/**
|
|
57
|
+
* The chooser and a drop take several files, handed over together
|
|
58
|
+
* through `onfiles`, in the order they were chosen. The component
|
|
59
|
+
* then keeps none of them: no preview, `value` is not set and
|
|
60
|
+
* `onfileselect` is not called. The page shows them where it wants
|
|
61
|
+
* (a `PhotoGrid`, say).
|
|
62
|
+
*/
|
|
63
|
+
multiple?: boolean;
|
|
64
|
+
/** With `multiple`: the most files taken in one go. The rest go to `onfilereject` with `"count"`. */
|
|
65
|
+
maxFiles?: number;
|
|
66
|
+
/** `variant="button"`: the trigger's content, in place of `label`. */
|
|
67
|
+
children?: Snippet;
|
|
68
|
+
/** Id of the control the label points at; generated when omitted. */
|
|
69
|
+
id?: string;
|
|
70
|
+
/** Visible label, associated with the dropzone. With `variant="button"` it is the button's text. */
|
|
71
|
+
label?: string;
|
|
18
72
|
/** Preview URL; `bind:value` to read it. */
|
|
19
73
|
value?: string | null;
|
|
20
74
|
disabled?: boolean;
|
|
21
75
|
accept?: string;
|
|
22
76
|
placeholder?: string;
|
|
77
|
+
/** Second line of the dropzone. */
|
|
78
|
+
browseText?: string;
|
|
79
|
+
changeText?: string;
|
|
80
|
+
removeText?: string;
|
|
81
|
+
/** Accessible name of the Change action; defaults to `changeText` plus `label`. */
|
|
82
|
+
changeLabel?: string;
|
|
83
|
+
/** Accessible name of the Remove action; defaults to `removeText` plus `label`. */
|
|
84
|
+
removeLabel?: string;
|
|
85
|
+
/** Text alternative for the preview. */
|
|
86
|
+
alt?: string;
|
|
87
|
+
/** Forces the preview kind when the URL has no extension to go by. */
|
|
88
|
+
previewType?: ImageUploadPreviewType;
|
|
89
|
+
/** Replaces the built-in `<img>` / `<video>` preview. */
|
|
90
|
+
preview?: Snippet<[ImageUploadPreviewDetail]>;
|
|
91
|
+
/**
|
|
92
|
+
* Where Change and Remove sit: over the whole preview, or in a strip
|
|
93
|
+
* along its top edge. Defaults to the strip for a video, whose controls
|
|
94
|
+
* an overlay would cover, and to the overlay otherwise.
|
|
95
|
+
*/
|
|
96
|
+
actionsPlacement?: ImageUploadActionsPlacement;
|
|
97
|
+
/** Announced to screen readers when a file or URL is set. */
|
|
98
|
+
selectedText?: string;
|
|
99
|
+
/** Announced to screen readers when the value is cleared. */
|
|
100
|
+
removedText?: string;
|
|
23
101
|
errorMessage?: string;
|
|
102
|
+
errorTitle?: string;
|
|
103
|
+
/** Closing line of the error block; `false` or `""` leaves it out. */
|
|
104
|
+
errorRecovery?: string | false;
|
|
24
105
|
/** Native `change` event from the hidden file input. */
|
|
25
106
|
onchange?: (event: Event) => void;
|
|
26
|
-
/** Fires
|
|
107
|
+
/** Fires before the file chooser opens; `event.preventDefault()` keeps it closed. */
|
|
27
108
|
onclick?: (event: Event) => void;
|
|
109
|
+
/** Replaces the native file chooser, for a media library or another source. */
|
|
110
|
+
onbrowse?: (event: Event) => void;
|
|
28
111
|
/** Selected file and preview URL; `{ file: null, url: null }` on remove. */
|
|
29
112
|
onfileselect?: (detail: ImageUploadFileDetail) => void;
|
|
113
|
+
/** A dropped file that `accept` does not allow, or a dropped or chosen file over `maxSize`. */
|
|
114
|
+
onfilereject?: (file: File, reason: ImageUploadRejectReason) => void;
|
|
115
|
+
/** With `multiple`: every file that was taken, in order. Not called with none. */
|
|
116
|
+
onfiles?: (files: File[]) => void;
|
|
30
117
|
};
|
|
31
118
|
|
|
32
119
|
let {
|
|
33
120
|
class: className = "",
|
|
34
|
-
|
|
121
|
+
variant = "dropzone",
|
|
122
|
+
buttonVariant = "secondary",
|
|
123
|
+
buttonSize = "md",
|
|
124
|
+
maxSize,
|
|
125
|
+
multiple = false,
|
|
126
|
+
maxFiles,
|
|
127
|
+
id: idProp,
|
|
128
|
+
label = "",
|
|
129
|
+
value = $bindable<Exclude<Props["value"], undefined>>(),
|
|
35
130
|
disabled = false,
|
|
36
131
|
accept = "image/*",
|
|
37
|
-
placeholder
|
|
132
|
+
placeholder: placeholderGiven,
|
|
133
|
+
browseText: browseTextGiven,
|
|
134
|
+
changeText: changeTextGiven,
|
|
135
|
+
removeText: removeTextGiven,
|
|
136
|
+
changeLabel,
|
|
137
|
+
removeLabel,
|
|
138
|
+
alt = "",
|
|
139
|
+
previewType,
|
|
140
|
+
preview,
|
|
141
|
+
actionsPlacement,
|
|
142
|
+
selectedText: selectedTextGiven,
|
|
143
|
+
removedText: removedTextGiven,
|
|
38
144
|
errorMessage = "",
|
|
145
|
+
errorTitle: errorTitleGiven,
|
|
146
|
+
errorRecovery: errorRecoveryGiven,
|
|
39
147
|
onchange,
|
|
40
148
|
onclick,
|
|
149
|
+
onbrowse,
|
|
41
150
|
onfileselect,
|
|
42
|
-
|
|
151
|
+
onfilereject,
|
|
152
|
+
onfiles,
|
|
153
|
+
children,
|
|
43
154
|
...restProps
|
|
44
155
|
}: Props = $props();
|
|
45
156
|
|
|
157
|
+
/** Each text: the prop, else the app-wide word from a `ZabiStringsProvider` above this one, else English. */
|
|
158
|
+
const provided = zabiStringsFor("imageUpload");
|
|
159
|
+
const placeholder = $derived(placeholderGiven ?? provided()?.placeholder ?? DEFAULT_IMAGE_UPLOAD_TEXTS.placeholder);
|
|
160
|
+
const browseText = $derived(browseTextGiven ?? provided()?.browseText ?? DEFAULT_IMAGE_UPLOAD_TEXTS.browseText);
|
|
161
|
+
const changeText = $derived(changeTextGiven ?? provided()?.changeText ?? DEFAULT_IMAGE_UPLOAD_TEXTS.changeText);
|
|
162
|
+
const removeText = $derived(removeTextGiven ?? provided()?.removeText ?? DEFAULT_IMAGE_UPLOAD_TEXTS.removeText);
|
|
163
|
+
const selectedText = $derived(selectedTextGiven ?? provided()?.selectedText ?? DEFAULT_IMAGE_UPLOAD_TEXTS.selectedText);
|
|
164
|
+
const removedText = $derived(removedTextGiven ?? provided()?.removedText ?? DEFAULT_IMAGE_UPLOAD_TEXTS.removedText);
|
|
165
|
+
const errorTitle = $derived(errorTitleGiven ?? provided()?.errorTitle ?? DEFAULT_IMAGE_UPLOAD_TEXTS.errorTitle);
|
|
166
|
+
const errorRecovery = $derived(errorRecoveryGiven ?? provided()?.errorRecovery ?? DEFAULT_IMAGE_UPLOAD_TEXTS.errorRecovery);
|
|
167
|
+
|
|
168
|
+
// No fallback on a bindable prop: Svelte refuses `bind:…={undefined}` on
|
|
169
|
+
// one that has a fallback (`props_invalid_value`), and a page that throws
|
|
170
|
+
// while it hydrates never becomes interactive. The default is applied
|
|
171
|
+
// here instead: at once, for the server and the first render, and again
|
|
172
|
+
// whenever a parent hands back `undefined`.
|
|
173
|
+
const applyDefaults = () => {
|
|
174
|
+
if (value === undefined) value = null;
|
|
175
|
+
};
|
|
176
|
+
applyDefaults();
|
|
177
|
+
$effect.pre(applyDefaults);
|
|
178
|
+
|
|
179
|
+
const fallbackId = generateId("image-upload");
|
|
180
|
+
const controlId = $derived(idProp ?? fallbackId);
|
|
181
|
+
const labelId = $derived(`${controlId}-label`);
|
|
182
|
+
const hintId = $derived(`${controlId}-hint`);
|
|
183
|
+
const errorId = $derived(`${controlId}-error`);
|
|
184
|
+
|
|
185
|
+
let host = $state<HTMLDivElement>();
|
|
46
186
|
let fileInput = $state<HTMLInputElement>();
|
|
47
187
|
let currentObjectUrl = $state<string | null>(null);
|
|
188
|
+
// An object URL has no extension, so the kind is kept from the file itself.
|
|
189
|
+
let currentObjectType = $state<ImageUploadPreviewType | null>(null);
|
|
190
|
+
// dragenter/dragleave also fire for children; count them instead of toggling.
|
|
191
|
+
let dragDepth = $state(0);
|
|
192
|
+
|
|
193
|
+
const isDragOver = $derived(dragDepth > 0);
|
|
194
|
+
|
|
195
|
+
let statusMessage = $state("");
|
|
196
|
+
let previousValue: string | null | undefined;
|
|
197
|
+
// Set by Change, Remove and the dropzone: the next swap is this control's doing.
|
|
198
|
+
let focusReturnArmed = false;
|
|
199
|
+
|
|
200
|
+
const resolvedPreviewType = $derived.by<ImageUploadPreviewType>(() => {
|
|
201
|
+
if (previewType) return previewType;
|
|
202
|
+
if (!value) return "image";
|
|
203
|
+
if (value === currentObjectUrl && currentObjectType) return currentObjectType;
|
|
204
|
+
return isVideoUrl(value) ? "video" : "image";
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const actionsInStrip = $derived(
|
|
208
|
+
(actionsPlacement ?? (resolvedPreviewType === "video" ? "strip" : "overlay")) ===
|
|
209
|
+
"strip",
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
function announce(message: string) {
|
|
213
|
+
// An unchanged live region is not read again; a trailing no-break
|
|
214
|
+
// space makes a repeat a change without altering what is heard.
|
|
215
|
+
statusMessage = message === statusMessage ? `${message}\u00a0` : message;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The dropzone and the actions are different nodes, so filling or clearing
|
|
220
|
+
* the value unmounts whichever had focus and the browser drops focus on
|
|
221
|
+
* `<body>`. Hand it to the control that now carries the id, but only when
|
|
222
|
+
* focus was in here or the swap came from one of this component's own
|
|
223
|
+
* controls, and never when it has since settled somewhere else.
|
|
224
|
+
*/
|
|
225
|
+
$effect.pre(() => {
|
|
226
|
+
const current = value;
|
|
227
|
+
untrack(() => {
|
|
228
|
+
if (previousValue === undefined) {
|
|
229
|
+
previousValue = current ?? null;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if ((current ?? null) === previousValue) return;
|
|
233
|
+
|
|
234
|
+
const swapped = Boolean(current) !== Boolean(previousValue);
|
|
235
|
+
previousValue = current ?? null;
|
|
236
|
+
announce(current ? selectedText : removedText);
|
|
237
|
+
if (!swapped) return;
|
|
238
|
+
|
|
239
|
+
const hadFocus = Boolean(host?.contains(document.activeElement));
|
|
240
|
+
const armed = focusReturnArmed;
|
|
241
|
+
focusReturnArmed = false;
|
|
242
|
+
if (!hadFocus && !armed) return;
|
|
243
|
+
|
|
244
|
+
void tick().then(() => {
|
|
245
|
+
const active = document.activeElement;
|
|
246
|
+
if (active && active !== document.body) return;
|
|
247
|
+
document.getElementById(controlId)?.focus();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const changeName = $derived(
|
|
253
|
+
changeLabel ?? (label ? `${changeText} ${label}` : undefined),
|
|
254
|
+
);
|
|
255
|
+
const removeName = $derived(
|
|
256
|
+
removeLabel ?? (label ? `${removeText} ${label}` : undefined),
|
|
257
|
+
);
|
|
48
258
|
|
|
49
259
|
function revokeCurrentObjectUrl() {
|
|
50
260
|
if (currentObjectUrl && typeof URL !== "undefined" && URL.revokeObjectURL) {
|
|
51
261
|
URL.revokeObjectURL(currentObjectUrl);
|
|
52
262
|
currentObjectUrl = null;
|
|
263
|
+
currentObjectType = null;
|
|
53
264
|
}
|
|
54
265
|
}
|
|
55
266
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const input = event.target as HTMLInputElement;
|
|
60
|
-
if (!input.files || input.files.length === 0) return;
|
|
61
|
-
|
|
62
|
-
const file = input.files[0];
|
|
267
|
+
/** Shared by the file chooser and drag and drop. */
|
|
268
|
+
function selectFile(file: File): string | null {
|
|
63
269
|
let url: string | null = null;
|
|
64
270
|
|
|
65
271
|
if (typeof URL !== "undefined" && URL.createObjectURL) {
|
|
@@ -67,15 +273,64 @@
|
|
|
67
273
|
url = URL.createObjectURL(file);
|
|
68
274
|
value = url;
|
|
69
275
|
currentObjectUrl = url;
|
|
276
|
+
currentObjectType =
|
|
277
|
+
file.type.startsWith("video/") || (!file.type && isVideoPath(file.name))
|
|
278
|
+
? "video"
|
|
279
|
+
: "image";
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return url;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function handleFileSelect(event: Event) {
|
|
286
|
+
if (disabled) return;
|
|
287
|
+
|
|
288
|
+
const input = event.target as HTMLInputElement;
|
|
289
|
+
if (!input.files || input.files.length === 0) return;
|
|
290
|
+
|
|
291
|
+
if (multiple) {
|
|
292
|
+
const files = Array.from(input.files);
|
|
293
|
+
onchange?.(event);
|
|
294
|
+
// Nothing here holds the files: cleared so the same ones can be chosen again.
|
|
295
|
+
input.value = "";
|
|
296
|
+
takeFiles(files, false);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const file = input.files[0];
|
|
301
|
+
if (tooLarge(file)) {
|
|
302
|
+
// Cleared so the same file can be chosen again after the app has said why.
|
|
303
|
+
input.value = "";
|
|
304
|
+
onfilereject?.(file, "size");
|
|
305
|
+
return;
|
|
70
306
|
}
|
|
307
|
+
const url = selectFile(file);
|
|
71
308
|
|
|
72
309
|
onchange?.(event);
|
|
73
310
|
onfileselect?.({ file, url });
|
|
74
311
|
}
|
|
75
312
|
|
|
313
|
+
/**
|
|
314
|
+
* `multiple`: the files that pass go to `onfiles` together; each of the
|
|
315
|
+
* others goes to `onfilereject` with why. A drop is checked against
|
|
316
|
+
* `accept` here, as the chooser has already done for its own.
|
|
317
|
+
*/
|
|
318
|
+
function takeFiles(files: File[], checkType: boolean) {
|
|
319
|
+
const limit = maxFiles !== undefined && maxFiles >= 0 ? Math.floor(maxFiles) : Infinity;
|
|
320
|
+
const taken: File[] = [];
|
|
321
|
+
for (const file of files) {
|
|
322
|
+
if (checkType && !matchesAccept(file)) onfilereject?.(file, "type");
|
|
323
|
+
else if (tooLarge(file)) onfilereject?.(file, "size");
|
|
324
|
+
else if (taken.length >= limit) onfilereject?.(file, "count");
|
|
325
|
+
else taken.push(file);
|
|
326
|
+
}
|
|
327
|
+
if (taken.length > 0) onfiles?.(taken);
|
|
328
|
+
}
|
|
329
|
+
|
|
76
330
|
function removeImage() {
|
|
77
331
|
if (disabled) return;
|
|
78
332
|
|
|
333
|
+
focusReturnArmed = true;
|
|
79
334
|
revokeCurrentObjectUrl();
|
|
80
335
|
value = null;
|
|
81
336
|
if (fileInput) fileInput.value = "";
|
|
@@ -85,14 +340,85 @@
|
|
|
85
340
|
function triggerFileSelect(event: Event) {
|
|
86
341
|
if (disabled) return;
|
|
87
342
|
onclick?.(event);
|
|
343
|
+
if (event.defaultPrevented) return;
|
|
344
|
+
focusReturnArmed = true;
|
|
345
|
+
if (onbrowse) {
|
|
346
|
+
onbrowse(event);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
88
349
|
fileInput?.click();
|
|
89
350
|
}
|
|
90
351
|
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
352
|
+
function tooLarge(file: File) {
|
|
353
|
+
return maxSize !== undefined && file.size > maxSize;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Same rules the browser applies to the `accept` attribute. */
|
|
357
|
+
function matchesAccept(file: File) {
|
|
358
|
+
const tokens = accept
|
|
359
|
+
.split(",")
|
|
360
|
+
.map((token) => token.trim().toLowerCase())
|
|
361
|
+
.filter(Boolean);
|
|
362
|
+
if (tokens.length === 0) return true;
|
|
363
|
+
|
|
364
|
+
const type = file.type.toLowerCase();
|
|
365
|
+
const name = file.name.toLowerCase();
|
|
366
|
+
return tokens.some((token) => {
|
|
367
|
+
if (token.startsWith(".")) return name.endsWith(token);
|
|
368
|
+
if (token.endsWith("/*")) return type.startsWith(token.slice(0, -1));
|
|
369
|
+
return type === token;
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function carriesFiles(event: DragEvent) {
|
|
374
|
+
return Array.from(event.dataTransfer?.types ?? []).includes("Files");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// A disabled upload still cancels a file drag: left alone, the browser
|
|
378
|
+
// opens the dropped file in the tab and the surrounding form is lost.
|
|
379
|
+
function handleDragEnter(event: DragEvent) {
|
|
380
|
+
if (!carriesFiles(event)) return;
|
|
381
|
+
event.preventDefault();
|
|
382
|
+
if (!disabled) dragDepth += 1;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function handleDragOver(event: DragEvent) {
|
|
386
|
+
if (!carriesFiles(event)) return;
|
|
387
|
+
// Without this the browser refuses the drop and opens the file instead.
|
|
388
|
+
event.preventDefault();
|
|
389
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = disabled ? "none" : "copy";
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function handleDragLeave() {
|
|
393
|
+
if (dragDepth > 0) dragDepth -= 1;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function handleDrop(event: DragEvent) {
|
|
397
|
+
dragDepth = 0;
|
|
398
|
+
if (!carriesFiles(event)) return;
|
|
399
|
+
event.preventDefault();
|
|
400
|
+
if (disabled) return;
|
|
401
|
+
|
|
402
|
+
if (multiple) {
|
|
403
|
+
takeFiles(Array.from(event.dataTransfer?.files ?? []), true);
|
|
404
|
+
return;
|
|
95
405
|
}
|
|
406
|
+
|
|
407
|
+
const file = event.dataTransfer?.files?.[0];
|
|
408
|
+
if (!file) return;
|
|
409
|
+
if (!matchesAccept(file)) {
|
|
410
|
+
onfilereject?.(file, "type");
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (tooLarge(file)) {
|
|
414
|
+
onfilereject?.(file, "size");
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const url = selectFile(file);
|
|
419
|
+
// The input did not take part; clear it so the same file can be picked again.
|
|
420
|
+
if (fileInput) fileInput.value = "";
|
|
421
|
+
onfileselect?.({ file, url });
|
|
96
422
|
}
|
|
97
423
|
|
|
98
424
|
onDestroy(() => {
|
|
@@ -100,61 +426,150 @@
|
|
|
100
426
|
});
|
|
101
427
|
</script>
|
|
102
428
|
|
|
103
|
-
<div
|
|
104
|
-
{
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
</Button>
|
|
123
|
-
<Button
|
|
124
|
-
variant="danger"
|
|
125
|
-
size="sm"
|
|
126
|
-
onclick={removeImage}
|
|
127
|
-
{disabled}
|
|
128
|
-
>
|
|
129
|
-
Remove
|
|
130
|
-
</Button>
|
|
131
|
-
</div>
|
|
132
|
-
</div>
|
|
133
|
-
</div>
|
|
429
|
+
<div
|
|
430
|
+
bind:this={host}
|
|
431
|
+
class={cn(variant === "button" ? "inline-block space-y-2" : "space-y-3", className)}
|
|
432
|
+
data-variant={variant}
|
|
433
|
+
{...restProps}
|
|
434
|
+
>
|
|
435
|
+
{#if variant === "button"}
|
|
436
|
+
<!-- Only the trigger: one choice in a row of others. The value is kept
|
|
437
|
+
and announced as in the dropzone, but nothing is drawn for it. -->
|
|
438
|
+
<Button
|
|
439
|
+
id={controlId}
|
|
440
|
+
variant={buttonVariant}
|
|
441
|
+
size={buttonSize}
|
|
442
|
+
onclick={triggerFileSelect}
|
|
443
|
+
aria-describedby={errorMessage ? errorId : undefined}
|
|
444
|
+
{disabled}
|
|
445
|
+
>
|
|
446
|
+
{#if children}{@render children()}{:else}{label || browseText}{/if}
|
|
447
|
+
</Button>
|
|
134
448
|
{:else}
|
|
449
|
+
{#if label}
|
|
450
|
+
<label
|
|
451
|
+
id={labelId}
|
|
452
|
+
for={controlId}
|
|
453
|
+
class={cn("block text-sm font-medium text-label", disabled && "opacity-60")}
|
|
454
|
+
>
|
|
455
|
+
{label}
|
|
456
|
+
</label>
|
|
457
|
+
{/if}
|
|
458
|
+
|
|
459
|
+
<!-- Wraps both states so a drop is handled the same over either. -->
|
|
135
460
|
<div
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
aria-disabled={disabled}
|
|
461
|
+
role="presentation"
|
|
462
|
+
class={disabled ? "cursor-not-allowed" : undefined}
|
|
463
|
+
ondragenter={handleDragEnter}
|
|
464
|
+
ondragover={handleDragOver}
|
|
465
|
+
ondragleave={handleDragLeave}
|
|
466
|
+
ondrop={handleDrop}
|
|
467
|
+
data-drag-over={isDragOver ? "" : undefined}
|
|
144
468
|
>
|
|
145
|
-
|
|
469
|
+
{#if value}
|
|
146
470
|
<div
|
|
147
|
-
class="
|
|
471
|
+
class="relative group"
|
|
472
|
+
role={label ? "group" : undefined}
|
|
473
|
+
aria-labelledby={label ? labelId : undefined}
|
|
148
474
|
>
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
475
|
+
{#if preview}
|
|
476
|
+
{@render preview({ url: value, type: resolvedPreviewType, alt })}
|
|
477
|
+
{:else if resolvedPreviewType === "video"}
|
|
478
|
+
<!-- Never autoplays, so there is no motion to reduce; muted
|
|
479
|
+
until the viewer unmutes it from the native controls. -->
|
|
480
|
+
<video
|
|
481
|
+
src={value}
|
|
482
|
+
controls
|
|
483
|
+
muted
|
|
484
|
+
playsinline
|
|
485
|
+
preload="metadata"
|
|
486
|
+
aria-label={alt || undefined}
|
|
487
|
+
class="w-full h-32 object-cover rounded-container border-0"
|
|
488
|
+
></video>
|
|
489
|
+
{:else}
|
|
490
|
+
<img
|
|
491
|
+
src={value}
|
|
492
|
+
{alt}
|
|
493
|
+
class="w-full h-32 object-cover rounded-container border-0"
|
|
494
|
+
/>
|
|
495
|
+
{/if}
|
|
496
|
+
<!-- Hover cannot reveal the actions on a touch screen, so there
|
|
497
|
+
they stay visible as a strip that leaves the preview in
|
|
498
|
+
view. The layer itself never takes the pointer: only the
|
|
499
|
+
plate under the buttons does, so a video beneath stays
|
|
500
|
+
operable. The plate is opaque so the labels keep their
|
|
501
|
+
contrast whatever the image behind them. -->
|
|
502
|
+
<div
|
|
503
|
+
class={cn(
|
|
504
|
+
"absolute inset-0 p-1 border border-border-overlay material-thin opacity-0 group-hover:opacity-100 group-has-[:focus-visible]:opacity-100 transition-opacity motion-reduce:transition-none rounded-container flex items-center justify-center pointer-events-none",
|
|
505
|
+
"pointer-coarse:opacity-100 [@media(hover:none)]:opacity-100",
|
|
506
|
+
actionsInStrip
|
|
507
|
+
? "bottom-auto"
|
|
508
|
+
: "pointer-coarse:bottom-auto [@media(hover:none)]:bottom-auto",
|
|
509
|
+
isDragOver && "opacity-100 border-action-primary",
|
|
510
|
+
)}
|
|
511
|
+
data-testid="image-upload-actions"
|
|
512
|
+
>
|
|
513
|
+
<div
|
|
514
|
+
class="pointer-events-auto flex min-w-0 flex-wrap justify-center gap-2 rounded-control bg-surface-overlay p-1"
|
|
515
|
+
>
|
|
516
|
+
<Button
|
|
517
|
+
id={controlId}
|
|
518
|
+
variant="secondary"
|
|
519
|
+
size="sm"
|
|
520
|
+
class="min-w-0"
|
|
521
|
+
onclick={triggerFileSelect}
|
|
522
|
+
aria-label={changeName}
|
|
523
|
+
aria-describedby={errorMessage ? errorId : undefined}
|
|
524
|
+
{disabled}
|
|
525
|
+
>
|
|
526
|
+
<span class="truncate">{changeText}</span>
|
|
527
|
+
</Button>
|
|
528
|
+
<Button
|
|
529
|
+
variant="danger"
|
|
530
|
+
size="sm"
|
|
531
|
+
class="min-w-0"
|
|
532
|
+
onclick={removeImage}
|
|
533
|
+
aria-label={removeName}
|
|
534
|
+
{disabled}
|
|
535
|
+
>
|
|
536
|
+
<span class="truncate">{removeText}</span>
|
|
537
|
+
</Button>
|
|
538
|
+
</div>
|
|
539
|
+
</div>
|
|
156
540
|
</div>
|
|
157
|
-
|
|
541
|
+
{:else}
|
|
542
|
+
<button
|
|
543
|
+
type="button"
|
|
544
|
+
id={controlId}
|
|
545
|
+
class={cn(
|
|
546
|
+
"focus-ring block w-full border-2 border-dashed border-input-border rounded-container p-6 text-center enabled:hover:border-action-primary enabled:active:bg-surface-active transition-colors",
|
|
547
|
+
// Disabled form controls can swallow pointer events; let a
|
|
548
|
+
// drag land on the wrapper, which cancels it.
|
|
549
|
+
disabled ? "pointer-events-none opacity-50" : "cursor-pointer",
|
|
550
|
+
isDragOver && "border-action-primary bg-action-primary-subtle",
|
|
551
|
+
)}
|
|
552
|
+
onclick={triggerFileSelect}
|
|
553
|
+
{disabled}
|
|
554
|
+
aria-describedby={[label ? hintId : "", errorMessage ? errorId : ""]
|
|
555
|
+
.filter(Boolean)
|
|
556
|
+
.join(" ") || undefined}
|
|
557
|
+
>
|
|
558
|
+
<span class="block space-y-3">
|
|
559
|
+
<span
|
|
560
|
+
class="w-12 h-12 mx-auto bg-action-secondary rounded-control flex items-center justify-center"
|
|
561
|
+
>
|
|
562
|
+
<Image size={24} class="text-description" aria-hidden="true" />
|
|
563
|
+
</span>
|
|
564
|
+
<span id={hintId} class="block">
|
|
565
|
+
<span class="block font-medium text-headline">{placeholder}</span>
|
|
566
|
+
<span class="block text-sm text-description">
|
|
567
|
+
{browseText}
|
|
568
|
+
</span>
|
|
569
|
+
</span>
|
|
570
|
+
</span>
|
|
571
|
+
</button>
|
|
572
|
+
{/if}
|
|
158
573
|
</div>
|
|
159
574
|
{/if}
|
|
160
575
|
|
|
@@ -162,22 +577,30 @@
|
|
|
162
577
|
bind:this={fileInput}
|
|
163
578
|
type="file"
|
|
164
579
|
{accept}
|
|
580
|
+
{multiple}
|
|
165
581
|
onchange={handleFileSelect}
|
|
166
582
|
{disabled}
|
|
167
583
|
class="hidden"
|
|
168
584
|
data-testid="image-upload-input"
|
|
169
585
|
/>
|
|
170
586
|
|
|
587
|
+
<div class="sr-only" role="status" aria-live="polite" aria-atomic="true">
|
|
588
|
+
{statusMessage}
|
|
589
|
+
</div>
|
|
590
|
+
|
|
171
591
|
{#if errorMessage}
|
|
172
592
|
<!-- Same tinted fill + tinted border Alert uses, rather than a bare
|
|
173
593
|
step-600 outline on a transparent ground. -->
|
|
174
594
|
<div
|
|
595
|
+
id={errorId}
|
|
175
596
|
class="rounded-container border bg-error-subtle border-error-border text-error-text px-3 py-2 text-sm"
|
|
176
597
|
role="alert"
|
|
177
598
|
>
|
|
178
|
-
<p class="font-medium">
|
|
599
|
+
<p class="font-medium">{errorTitle}</p>
|
|
179
600
|
<p>{errorMessage}</p>
|
|
180
|
-
|
|
601
|
+
{#if errorRecovery}
|
|
602
|
+
<p class="mt-1">{errorRecovery}</p>
|
|
603
|
+
{/if}
|
|
181
604
|
</div>
|
|
182
605
|
{/if}
|
|
183
606
|
</div>
|