zabi-components 8.1.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 +1011 -0
- package/README.md +1 -1
- package/THEME.md +18 -5
- package/THEMING.md +308 -74
- package/dist/atoms/ActionPanel.svelte +1 -1
- package/dist/atoms/Avatar.svelte +19 -3
- package/dist/atoms/Avatar.svelte.d.ts +12 -0
- package/dist/atoms/Badge.svelte +37 -8
- package/dist/atoms/Badge.svelte.d.ts +6 -0
- package/dist/atoms/Block.svelte +74 -0
- package/dist/atoms/Block.svelte.d.ts +29 -0
- package/dist/atoms/Button.svelte +52 -15
- package/dist/atoms/Button.svelte.d.ts +3 -2
- package/dist/atoms/Card.svelte +181 -25
- package/dist/atoms/Card.svelte.d.ts +55 -3
- package/dist/atoms/Checkbox.svelte +33 -0
- package/dist/atoms/Checkbox.svelte.d.ts +27 -0
- package/dist/atoms/Chip.svelte +351 -0
- package/dist/atoms/Chip.svelte.d.ts +57 -0
- package/dist/atoms/CodeField.svelte +385 -0
- package/dist/atoms/CodeField.svelte.d.ts +61 -0
- package/dist/atoms/ColorPicker.svelte +1 -0
- package/dist/atoms/DateField.svelte +41 -3
- package/dist/atoms/DateField.svelte.d.ts +31 -3
- package/dist/atoms/FloatingActionButton.svelte +5 -3
- package/dist/atoms/FloatingActionButton.svelte.d.ts +1 -1
- package/dist/atoms/Heading.svelte +27 -1
- package/dist/atoms/Heading.svelte.d.ts +7 -0
- package/dist/atoms/IconButton.svelte +81 -9
- package/dist/atoms/IconButton.svelte.d.ts +13 -0
- package/dist/atoms/Input.svelte +10 -5
- package/dist/atoms/Input.svelte.d.ts +3 -2
- package/dist/atoms/List.svelte +3 -2
- package/dist/atoms/ListItem.svelte +31 -7
- package/dist/atoms/ListItem.svelte.d.ts +6 -0
- package/dist/atoms/PickerField.svelte +239 -0
- package/dist/atoms/PickerField.svelte.d.ts +67 -0
- package/dist/atoms/Progress.svelte +78 -17
- package/dist/atoms/Progress.svelte.d.ts +11 -1
- package/dist/atoms/Radio.svelte +32 -0
- package/dist/atoms/Radio.svelte.d.ts +26 -0
- package/dist/atoms/Rating.svelte +132 -6
- package/dist/atoms/Rating.svelte.d.ts +24 -3
- package/dist/atoms/Select.svelte +24 -26
- package/dist/atoms/Select.svelte.d.ts +12 -0
- package/dist/atoms/SelectionControl.svelte +99 -6
- package/dist/atoms/SelectionControl.svelte.d.ts +25 -0
- 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 +35 -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/Stat.svelte +72 -0
- package/dist/atoms/Stat.svelte.d.ts +19 -0
- package/dist/atoms/TemporalField.svelte +342 -10
- package/dist/atoms/TemporalField.svelte.d.ts +20 -4
- package/dist/atoms/Textarea.svelte +100 -5
- package/dist/atoms/Textarea.svelte.d.ts +21 -0
- package/dist/atoms/TimeColumn.svelte +150 -0
- package/dist/atoms/TimeColumn.svelte.d.ts +25 -0
- package/dist/atoms/TimeField.svelte +46 -3
- package/dist/atoms/TimeField.svelte.d.ts +34 -3
- package/dist/atoms/Toast.svelte +6 -5
- package/dist/atoms/Toggle.svelte +61 -10
- package/dist/atoms/Toggle.svelte.d.ts +15 -0
- package/dist/atoms/Tooltip.svelte +20 -1
- package/dist/atoms/Tooltip.svelte.d.ts +7 -0
- package/dist/atoms/index.d.ts +21 -0
- package/dist/atoms/index.js +17 -0
- package/dist/atoms/selection-control.styles.d.ts +15 -4
- package/dist/atoms/selection-control.styles.js +16 -5
- package/dist/components/atoms/index.d.ts +21 -0
- package/dist/components/atoms/index.d.ts.map +1 -1
- package/dist/components/atoms/selection-control.styles.d.ts +15 -4
- package/dist/components/atoms/selection-control.styles.d.ts.map +1 -1
- package/dist/components/index.d.ts +27 -3
- 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 +7 -2
- package/dist/components/molecules/index.d.ts.map +1 -1
- package/dist/components/molecules/toast-store.d.ts +30 -0
- package/dist/components/molecules/toast-store.d.ts.map +1 -1
- package/dist/components/organisms/index.d.ts +6 -1
- package/dist/components/organisms/index.d.ts.map +1 -1
- package/dist/components/types/variants.d.ts +12 -3
- package/dist/components/types/variants.d.ts.map +1 -1
- package/dist/components/util/app-shell.d.ts +55 -4
- package/dist/components/util/app-shell.d.ts.map +1 -1
- package/dist/components/util/avatar.d.ts +3 -0
- package/dist/components/util/avatar.d.ts.map +1 -1
- package/dist/components/util/bottom-tab-bar.d.ts +6 -0
- package/dist/components/util/bottom-tab-bar.d.ts.map +1 -1
- 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 +6 -0
- package/dist/components/util/collapsible.d.ts.map +1 -1
- 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/field-control.d.ts +30 -0
- package/dist/components/util/field-control.d.ts.map +1 -0
- package/dist/components/util/field.d.ts +6 -0
- package/dist/components/util/field.d.ts.map +1 -1
- package/dist/components/util/focus-utils.d.ts +2 -0
- package/dist/components/util/focus-utils.d.ts.map +1 -1
- 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/photo.d.ts +19 -0
- package/dist/components/util/photo.d.ts.map +1 -1
- package/dist/components/util/progress.d.ts +19 -0
- package/dist/components/util/progress.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/sheet-drag.d.ts +25 -1
- package/dist/components/util/sheet-drag.d.ts.map +1 -1
- 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/temporal-field.d.ts +89 -0
- package/dist/components/util/temporal-field.d.ts.map +1 -0
- package/dist/components/util/toaster.d.ts +40 -0
- package/dist/components/util/toaster.d.ts.map +1 -1
- 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/top-navbar.d.ts +26 -2
- package/dist/components/util/top-navbar.d.ts.map +1 -1
- package/dist/components/util/zabi-strings.d.ts +9 -0
- package/dist/components/util/zabi-strings.d.ts.map +1 -1
- package/dist/create-theme/cli.js +6 -2
- package/dist/create-theme/index.d.ts +17 -3
- package/dist/create-theme/index.js +252 -35
- package/dist/create-theme/lib/resolve.js +97 -0
- package/dist/create-theme/theme-data.js +1618 -90
- package/dist/index.d.ts +27 -3
- package/dist/index.js +18 -0
- package/dist/map.d.ts +10 -0
- package/dist/map.js +9 -0
- package/dist/molecules/Alert.svelte +31 -12
- package/dist/molecules/Alert.svelte.d.ts +12 -1
- package/dist/molecules/AppBar.svelte +287 -28
- package/dist/molecules/AppBar.svelte.d.ts +62 -0
- package/dist/molecules/AvatarGroup.svelte +1 -1
- package/dist/molecules/BarChart.svelte +381 -0
- package/dist/molecules/BarChart.svelte.d.ts +45 -0
- package/dist/molecules/BottomSheet.svelte +88 -20
- package/dist/molecules/BottomSheet.svelte.d.ts +17 -1
- package/dist/molecules/BottomTabBar.svelte +134 -28
- package/dist/molecules/BottomTabBar.svelte.d.ts +36 -2
- package/dist/molecules/Calendar.svelte.d.ts +1 -1
- package/dist/molecules/ChipGroup.svelte +325 -0
- package/dist/molecules/ChipGroup.svelte.d.ts +41 -0
- package/dist/molecules/Collapsible.svelte +158 -54
- package/dist/molecules/Collapsible.svelte.d.ts +23 -6
- package/dist/molecules/CollapsibleGroup.svelte +16 -6
- package/dist/molecules/ConfirmDialog.svelte +5 -1
- package/dist/molecules/ConfirmDialog.svelte.d.ts +3 -0
- package/dist/molecules/Drawer.svelte +33 -12
- package/dist/molecules/Dropdown.svelte +9 -1
- package/dist/molecules/Dropdown.svelte.d.ts +2 -0
- package/dist/molecules/DropdownItem.svelte +1 -1
- package/dist/molecules/ImageUpload.svelte +230 -120
- package/dist/molecules/ImageUpload.svelte.d.ts +38 -3
- 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/Modal.svelte +11 -6
- package/dist/molecules/PhotoGrid.svelte +100 -10
- package/dist/molecules/PhotoGrid.svelte.d.ts +11 -0
- package/dist/molecules/PhotoViewer.svelte +121 -24
- package/dist/molecules/PhotoViewer.svelte.d.ts +18 -0
- package/dist/molecules/PieChart.svelte +421 -0
- package/dist/molecules/PieChart.svelte.d.ts +51 -0
- package/dist/molecules/RadioGroup.svelte +20 -4
- package/dist/molecules/RadioGroup.svelte.d.ts +5 -0
- package/dist/molecules/SegmentedControl.svelte +136 -3
- package/dist/molecules/SegmentedControl.svelte.d.ts +6 -0
- package/dist/molecules/SidebarFooter.svelte +1 -1
- package/dist/molecules/SidebarNavSection.svelte +1 -1
- package/dist/molecules/SortableList.svelte +1 -0
- package/dist/molecules/Stepper.svelte +21 -7
- package/dist/molecules/StickyActionBar.svelte +67 -8
- package/dist/molecules/StickyActionBar.svelte.d.ts +14 -1
- package/dist/molecules/Tabs.svelte +79 -2
- package/dist/molecules/Toaster.svelte +171 -7
- package/dist/molecules/Toaster.svelte.d.ts +60 -2
- package/dist/molecules/ToasterToast.svelte +292 -60
- package/dist/molecules/ToasterToast.svelte.d.ts +6 -0
- package/dist/molecules/UnsavedChangesBar.svelte +13 -2
- package/dist/molecules/index.d.ts +7 -2
- package/dist/molecules/index.js +4 -0
- package/dist/molecules/toast-store.d.ts +30 -0
- package/dist/molecules/toast-store.js +4 -1
- package/dist/organisms/AppNavigation.svelte +440 -0
- package/dist/organisms/AppNavigation.svelte.d.ts +72 -0
- package/dist/organisms/AppShell.svelte +539 -39
- package/dist/organisms/AppShell.svelte.d.ts +102 -6
- package/dist/organisms/AppShellHeaderPart.svelte +163 -0
- package/dist/organisms/AppShellHeaderPart.svelte.d.ts +57 -0
- package/dist/organisms/SidebarNavigation.svelte +23 -36
- package/dist/organisms/SidebarPanel.svelte +10 -14
- package/dist/organisms/SidebarPanel.svelte.d.ts +1 -0
- package/dist/organisms/SidebarShell.svelte +2 -3
- package/dist/organisms/TopNavbar.svelte +117 -28
- package/dist/organisms/TopNavbar.svelte.d.ts +22 -1
- package/dist/organisms/TopNavbarSheetContent.svelte +145 -0
- package/dist/organisms/TopNavbarSheetContent.svelte.d.ts +24 -0
- package/dist/organisms/index.d.ts +6 -1
- package/dist/organisms/index.js +4 -0
- package/dist/types/components.d.ts +17 -1
- package/dist/types/components.d.ts.map +1 -1
- package/dist/types/index.d.ts +497 -10
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/variants.ts +14 -3
- package/dist/util/app-shell.d.ts +55 -4
- package/dist/util/app-shell.js +32 -7
- package/dist/util/avatar.d.ts +3 -0
- package/dist/util/bottom-tab-bar.d.ts +6 -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 +6 -0
- package/dist/util/display-voice.d.ts +9 -0
- package/dist/util/display-voice.js +9 -0
- package/dist/util/field-control.d.ts +29 -0
- package/dist/util/field-control.js +32 -0
- package/dist/util/field.d.ts +6 -0
- package/dist/util/focus-utils.d.ts +2 -0
- package/dist/util/focus-utils.js +4 -0
- package/dist/util/map-view.d.ts +143 -0
- package/dist/util/map-view.js +121 -0
- package/dist/util/photo.d.ts +19 -0
- package/dist/util/photo.js +3 -0
- package/dist/util/progress.d.ts +18 -0
- package/dist/util/progress.js +17 -0
- package/dist/util/scroll-edge.d.ts +32 -0
- package/dist/util/scroll-edge.js +87 -0
- package/dist/util/sheet-drag.d.ts +25 -1
- package/dist/util/sheet-drag.js +71 -1
- 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/temporal-field.d.ts +88 -0
- package/dist/util/temporal-field.js +136 -0
- package/dist/util/toaster.d.ts +40 -0
- package/dist/util/toaster.js +55 -0
- package/dist/util/tone-surface.d.ts +33 -0
- package/dist/util/tone-surface.js +35 -0
- package/dist/util/top-navbar.d.ts +26 -2
- package/dist/util/top-navbar.js +23 -0
- package/dist/util/zabi-strings.d.ts +9 -0
- package/dist/zabi-components-colors.css +9 -3
- package/dist/zabi-components-theme-dark-only.css +474 -164
- package/dist/zabi-components-theme-dark.css +474 -164
- package/dist/zabi-components-theme-only.css +933 -99
- package/dist/zabi-components-theme.css +933 -99
- package/dist/zabi-components.css +2275 -321
- package/package.json +11 -1
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { cn } from "../util/cn.js";
|
|
8
8
|
import { isVideoPath, isVideoUrl } from "../util/media.js";
|
|
9
9
|
import { generateId } from "../util/ssr-safe.js";
|
|
10
|
+
import type { ButtonSize, ButtonVariant } from "../types/variants.js";
|
|
10
11
|
|
|
11
12
|
export type ImageUploadFileDetail = {
|
|
12
13
|
/** Selected file, or `null` when the image is removed. */
|
|
@@ -19,6 +20,14 @@
|
|
|
19
20
|
|
|
20
21
|
export type ImageUploadActionsPlacement = "overlay" | "strip";
|
|
21
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
|
+
|
|
22
31
|
export type ImageUploadPreviewDetail = {
|
|
23
32
|
/** The current `value`. */
|
|
24
33
|
url: string;
|
|
@@ -30,9 +39,35 @@
|
|
|
30
39
|
type Props = Omit<HTMLAttributes<HTMLDivElement>, "onchange" | "onclick" | "id"> & {
|
|
31
40
|
/** Extra classes for the host element. */
|
|
32
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;
|
|
33
68
|
/** Id of the control the label points at; generated when omitted. */
|
|
34
69
|
id?: string;
|
|
35
|
-
/** Visible label, associated with the dropzone. */
|
|
70
|
+
/** Visible label, associated with the dropzone. With `variant="button"` it is the button's text. */
|
|
36
71
|
label?: string;
|
|
37
72
|
/** Preview URL; `bind:value` to read it. */
|
|
38
73
|
value?: string | null;
|
|
@@ -75,12 +110,20 @@
|
|
|
75
110
|
onbrowse?: (event: Event) => void;
|
|
76
111
|
/** Selected file and preview URL; `{ file: null, url: null }` on remove. */
|
|
77
112
|
onfileselect?: (detail: ImageUploadFileDetail) => void;
|
|
78
|
-
/** A dropped file that `accept` does not allow
|
|
79
|
-
onfilereject?: (file: File) => 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;
|
|
80
117
|
};
|
|
81
118
|
|
|
82
119
|
let {
|
|
83
120
|
class: className = "",
|
|
121
|
+
variant = "dropzone",
|
|
122
|
+
buttonVariant = "secondary",
|
|
123
|
+
buttonSize = "md",
|
|
124
|
+
maxSize,
|
|
125
|
+
multiple = false,
|
|
126
|
+
maxFiles,
|
|
84
127
|
id: idProp,
|
|
85
128
|
label = "",
|
|
86
129
|
value = $bindable<Exclude<Props["value"], undefined>>(),
|
|
@@ -106,7 +149,8 @@
|
|
|
106
149
|
onbrowse,
|
|
107
150
|
onfileselect,
|
|
108
151
|
onfilereject,
|
|
109
|
-
|
|
152
|
+
onfiles,
|
|
153
|
+
children,
|
|
110
154
|
...restProps
|
|
111
155
|
}: Props = $props();
|
|
112
156
|
|
|
@@ -244,13 +288,45 @@
|
|
|
244
288
|
const input = event.target as HTMLInputElement;
|
|
245
289
|
if (!input.files || input.files.length === 0) return;
|
|
246
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
|
+
|
|
247
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;
|
|
306
|
+
}
|
|
248
307
|
const url = selectFile(file);
|
|
249
308
|
|
|
250
309
|
onchange?.(event);
|
|
251
310
|
onfileselect?.({ file, url });
|
|
252
311
|
}
|
|
253
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
|
+
|
|
254
330
|
function removeImage() {
|
|
255
331
|
if (disabled) return;
|
|
256
332
|
|
|
@@ -273,6 +349,10 @@
|
|
|
273
349
|
fileInput?.click();
|
|
274
350
|
}
|
|
275
351
|
|
|
352
|
+
function tooLarge(file: File) {
|
|
353
|
+
return maxSize !== undefined && file.size > maxSize;
|
|
354
|
+
}
|
|
355
|
+
|
|
276
356
|
/** Same rules the browser applies to the `accept` attribute. */
|
|
277
357
|
function matchesAccept(file: File) {
|
|
278
358
|
const tokens = accept
|
|
@@ -319,10 +399,19 @@
|
|
|
319
399
|
event.preventDefault();
|
|
320
400
|
if (disabled) return;
|
|
321
401
|
|
|
402
|
+
if (multiple) {
|
|
403
|
+
takeFiles(Array.from(event.dataTransfer?.files ?? []), true);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
322
407
|
const file = event.dataTransfer?.files?.[0];
|
|
323
408
|
if (!file) return;
|
|
324
409
|
if (!matchesAccept(file)) {
|
|
325
|
-
onfilereject?.(file);
|
|
410
|
+
onfilereject?.(file, "type");
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (tooLarge(file)) {
|
|
414
|
+
onfilereject?.(file, "size");
|
|
326
415
|
return;
|
|
327
416
|
}
|
|
328
417
|
|
|
@@ -337,137 +426,158 @@
|
|
|
337
426
|
});
|
|
338
427
|
</script>
|
|
339
428
|
|
|
340
|
-
<div
|
|
341
|
-
{
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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}
|
|
346
445
|
>
|
|
347
|
-
{label}
|
|
348
|
-
</
|
|
349
|
-
{
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
ondragenter={handleDragEnter}
|
|
356
|
-
ondragover={handleDragOver}
|
|
357
|
-
ondragleave={handleDragLeave}
|
|
358
|
-
ondrop={handleDrop}
|
|
359
|
-
data-drag-over={isDragOver ? "" : undefined}
|
|
360
|
-
>
|
|
361
|
-
{#if value}
|
|
362
|
-
<div
|
|
363
|
-
class="relative group"
|
|
364
|
-
role={label ? "group" : undefined}
|
|
365
|
-
aria-labelledby={label ? labelId : undefined}
|
|
446
|
+
{#if children}{@render children()}{:else}{label || browseText}{/if}
|
|
447
|
+
</Button>
|
|
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")}
|
|
366
454
|
>
|
|
367
|
-
{
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
<img
|
|
383
|
-
src={value}
|
|
384
|
-
{alt}
|
|
385
|
-
class="w-full h-32 object-cover rounded-container border-0"
|
|
386
|
-
/>
|
|
387
|
-
{/if}
|
|
388
|
-
<!-- Hover cannot reveal the actions on a touch screen, so there
|
|
389
|
-
they stay visible as a strip that leaves the preview in
|
|
390
|
-
view. The layer itself never takes the pointer: only the
|
|
391
|
-
plate under the buttons does, so a video beneath stays
|
|
392
|
-
operable. The plate is opaque so the labels keep their
|
|
393
|
-
contrast whatever the image behind them. -->
|
|
455
|
+
{label}
|
|
456
|
+
</label>
|
|
457
|
+
{/if}
|
|
458
|
+
|
|
459
|
+
<!-- Wraps both states so a drop is handled the same over either. -->
|
|
460
|
+
<div
|
|
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}
|
|
468
|
+
>
|
|
469
|
+
{#if value}
|
|
394
470
|
<div
|
|
395
|
-
class=
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
actionsInStrip
|
|
399
|
-
? "bottom-auto"
|
|
400
|
-
: "pointer-coarse:bottom-auto [@media(hover:none)]:bottom-auto",
|
|
401
|
-
isDragOver && "opacity-100 border-action-primary",
|
|
402
|
-
)}
|
|
403
|
-
data-testid="image-upload-actions"
|
|
471
|
+
class="relative group"
|
|
472
|
+
role={label ? "group" : undefined}
|
|
473
|
+
aria-labelledby={label ? labelId : undefined}
|
|
404
474
|
>
|
|
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. -->
|
|
405
502
|
<div
|
|
406
|
-
class=
|
|
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"
|
|
407
512
|
>
|
|
408
|
-
<
|
|
409
|
-
|
|
410
|
-
variant="secondary"
|
|
411
|
-
size="sm"
|
|
412
|
-
class="min-w-0"
|
|
413
|
-
onclick={triggerFileSelect}
|
|
414
|
-
aria-label={changeName}
|
|
415
|
-
aria-describedby={errorMessage ? errorId : undefined}
|
|
416
|
-
{disabled}
|
|
513
|
+
<div
|
|
514
|
+
class="pointer-events-auto flex min-w-0 flex-wrap justify-center gap-2 rounded-control bg-surface-overlay p-1"
|
|
417
515
|
>
|
|
418
|
-
<
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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>
|
|
430
539
|
</div>
|
|
431
540
|
</div>
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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>
|
|
460
569
|
</span>
|
|
461
570
|
</span>
|
|
462
|
-
</
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
571
|
+
</button>
|
|
572
|
+
{/if}
|
|
573
|
+
</div>
|
|
574
|
+
{/if}
|
|
466
575
|
|
|
467
576
|
<input
|
|
468
577
|
bind:this={fileInput}
|
|
469
578
|
type="file"
|
|
470
579
|
{accept}
|
|
580
|
+
{multiple}
|
|
471
581
|
onchange={handleFileSelect}
|
|
472
582
|
{disabled}
|
|
473
583
|
class="hidden"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Snippet } from "svelte";
|
|
2
2
|
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
import type { ButtonSize, ButtonVariant } from "../types/variants.js";
|
|
3
4
|
export type ImageUploadFileDetail = {
|
|
4
5
|
/** Selected file, or `null` when the image is removed. */
|
|
5
6
|
file: File | null;
|
|
@@ -8,6 +9,12 @@ export type ImageUploadFileDetail = {
|
|
|
8
9
|
};
|
|
9
10
|
export type ImageUploadPreviewType = "image" | "video";
|
|
10
11
|
export type ImageUploadActionsPlacement = "overlay" | "strip";
|
|
12
|
+
export type ImageUploadVariant = "dropzone" | "button";
|
|
13
|
+
/**
|
|
14
|
+
* Why a file was turned away: `accept` does not allow it, it is over
|
|
15
|
+
* `maxSize`, or (only with `multiple` and `maxFiles`) it is one too many.
|
|
16
|
+
*/
|
|
17
|
+
export type ImageUploadRejectReason = "type" | "size" | "count";
|
|
11
18
|
export type ImageUploadPreviewDetail = {
|
|
12
19
|
/** The current `value`. */
|
|
13
20
|
url: string;
|
|
@@ -18,9 +25,35 @@ export type ImageUploadPreviewDetail = {
|
|
|
18
25
|
type Props = Omit<HTMLAttributes<HTMLDivElement>, "onchange" | "onclick" | "id"> & {
|
|
19
26
|
/** Extra classes for the host element. */
|
|
20
27
|
class?: string;
|
|
28
|
+
/**
|
|
29
|
+
* `dropzone` (default): the drop area, and the preview with Change and
|
|
30
|
+
* Remove once there is a value. `button`: only a button that opens the
|
|
31
|
+
* file chooser, for a row of choices; no drop area, no preview and no
|
|
32
|
+
* Remove. The picked file comes through `onfileselect` and `bind:value`,
|
|
33
|
+
* and the app shows it where it wants.
|
|
34
|
+
*/
|
|
35
|
+
variant?: ImageUploadVariant;
|
|
36
|
+
/** `variant="button"`: the trigger's `Button` variant. */
|
|
37
|
+
buttonVariant?: ButtonVariant;
|
|
38
|
+
/** `variant="button"`: the trigger's `Button` size. */
|
|
39
|
+
buttonSize?: ButtonSize;
|
|
40
|
+
/** The largest file taken, in bytes. A larger one goes to `onfilereject` with `"size"`. */
|
|
41
|
+
maxSize?: number;
|
|
42
|
+
/**
|
|
43
|
+
* The chooser and a drop take several files, handed over together
|
|
44
|
+
* through `onfiles`, in the order they were chosen. The component
|
|
45
|
+
* then keeps none of them: no preview, `value` is not set and
|
|
46
|
+
* `onfileselect` is not called. The page shows them where it wants
|
|
47
|
+
* (a `PhotoGrid`, say).
|
|
48
|
+
*/
|
|
49
|
+
multiple?: boolean;
|
|
50
|
+
/** With `multiple`: the most files taken in one go. The rest go to `onfilereject` with `"count"`. */
|
|
51
|
+
maxFiles?: number;
|
|
52
|
+
/** `variant="button"`: the trigger's content, in place of `label`. */
|
|
53
|
+
children?: Snippet;
|
|
21
54
|
/** Id of the control the label points at; generated when omitted. */
|
|
22
55
|
id?: string;
|
|
23
|
-
/** Visible label, associated with the dropzone. */
|
|
56
|
+
/** Visible label, associated with the dropzone. With `variant="button"` it is the button's text. */
|
|
24
57
|
label?: string;
|
|
25
58
|
/** Preview URL; `bind:value` to read it. */
|
|
26
59
|
value?: string | null;
|
|
@@ -63,8 +96,10 @@ type Props = Omit<HTMLAttributes<HTMLDivElement>, "onchange" | "onclick" | "id">
|
|
|
63
96
|
onbrowse?: (event: Event) => void;
|
|
64
97
|
/** Selected file and preview URL; `{ file: null, url: null }` on remove. */
|
|
65
98
|
onfileselect?: (detail: ImageUploadFileDetail) => void;
|
|
66
|
-
/** A dropped file that `accept` does not allow
|
|
67
|
-
onfilereject?: (file: File) => void;
|
|
99
|
+
/** A dropped file that `accept` does not allow, or a dropped or chosen file over `maxSize`. */
|
|
100
|
+
onfilereject?: (file: File, reason: ImageUploadRejectReason) => void;
|
|
101
|
+
/** With `multiple`: every file that was taken, in order. Not called with none. */
|
|
102
|
+
onfiles?: (files: File[]) => void;
|
|
68
103
|
};
|
|
69
104
|
declare const ImageUpload: import("svelte").Component<Props, {}, "value">;
|
|
70
105
|
type ImageUpload = ReturnType<typeof ImageUpload>;
|