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
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
import { untrack } from "svelte";
|
|
5
5
|
import type { HTMLAttributes } from "svelte/elements";
|
|
6
6
|
import Check from "@lucide/svelte/icons/check";
|
|
7
|
+
import CircleAlert from "@lucide/svelte/icons/circle-alert";
|
|
7
8
|
import ImageOff from "@lucide/svelte/icons/image-off";
|
|
8
9
|
import Maximize2 from "@lucide/svelte/icons/maximize-2";
|
|
9
10
|
import Plus from "@lucide/svelte/icons/plus";
|
|
11
|
+
import RotateCw from "@lucide/svelte/icons/rotate-cw";
|
|
10
12
|
import Skeleton from "../atoms/Skeleton.svelte";
|
|
11
13
|
import { isDevBuild } from "../util/app-shell.js";
|
|
12
14
|
import { cn } from "../util/cn.js";
|
|
@@ -77,6 +79,17 @@
|
|
|
77
79
|
* Open your own file picker or an ImageUpload from it.
|
|
78
80
|
*/
|
|
79
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;
|
|
80
93
|
/** Overrides for the built-in strings. */
|
|
81
94
|
strings?: Partial<PhotoGridStrings>;
|
|
82
95
|
/** Extra classes for the host element. */
|
|
@@ -95,6 +108,8 @@
|
|
|
95
108
|
selectedKeys = $bindable<Exclude<Props["selectedKeys"], undefined>>(),
|
|
96
109
|
onselect,
|
|
97
110
|
onadd,
|
|
111
|
+
onloaderror,
|
|
112
|
+
onretry,
|
|
98
113
|
strings,
|
|
99
114
|
class: className = "",
|
|
100
115
|
"aria-label": ariaLabel,
|
|
@@ -162,16 +177,38 @@
|
|
|
162
177
|
// again for every image each time one of the lists changes.
|
|
163
178
|
untrack(() => {
|
|
164
179
|
if (image.naturalWidth > 0) markLoaded(address);
|
|
165
|
-
else markFailed(address);
|
|
180
|
+
else if (markFailed(address)) reportFailed(address);
|
|
166
181
|
});
|
|
167
182
|
}
|
|
168
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
|
+
|
|
169
191
|
function markLoaded(address: string) {
|
|
170
192
|
if (!loaded.includes(address)) loaded = [...loaded, address];
|
|
171
193
|
}
|
|
172
194
|
|
|
173
|
-
|
|
174
|
-
|
|
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)}%`;
|
|
175
212
|
}
|
|
176
213
|
|
|
177
214
|
/**
|
|
@@ -295,20 +332,29 @@
|
|
|
295
332
|
onkeydown={handleKeydown}
|
|
296
333
|
>
|
|
297
334
|
{#if onadd}
|
|
298
|
-
<li
|
|
335
|
+
<li
|
|
336
|
+
class="@container relative aspect-square"
|
|
337
|
+
data-photo-grid-tile
|
|
338
|
+
onfocusin={() => handleTileFocus(ADD_KEY)}
|
|
339
|
+
>
|
|
299
340
|
<!-- A dashed edge the tile depends on to be seen, so in the
|
|
300
|
-
control-border colour; the label is shown, not only read out.
|
|
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). -->
|
|
301
346
|
<button
|
|
302
347
|
type="button"
|
|
303
|
-
class="focus-ring flex size-full cursor-pointer flex-col items-center justify-center gap-1 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"
|
|
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}
|
|
304
350
|
aria-describedby={describedKey === ADD_KEY ? hintId : undefined}
|
|
305
351
|
tabindex={tabbableKey === ADD_KEY ? 0 : -1}
|
|
306
352
|
data-photo-grid-item
|
|
307
353
|
data-photo-grid-add
|
|
308
354
|
onclick={() => onadd?.()}
|
|
309
355
|
>
|
|
310
|
-
<Plus size={24} class="shrink-0" aria-hidden="true" />
|
|
311
|
-
<span class="[overflow-wrap:anywhere]">{text.addPhoto}</span>
|
|
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>
|
|
312
358
|
</button>
|
|
313
359
|
</li>
|
|
314
360
|
{/if}
|
|
@@ -337,7 +383,7 @@
|
|
|
337
383
|
? "outline-2 -outline-offset-2 outline-(--color-action-primary)"
|
|
338
384
|
: "outline-1 -outline-offset-1 outline-(--color-border) hover:outline-(--color-border-medium) active:scale-[0.98] motion-reduce:active:scale-100",
|
|
339
385
|
)}
|
|
340
|
-
aria-label={more ? `${name}, ${text.morePhotos(counts.hidden)}` : name}
|
|
386
|
+
aria-label={more ? `${name}, ${text.morePhotos(counts.hidden)}` : nameWithStatus(photo, name)}
|
|
341
387
|
aria-pressed={selects ? on : undefined}
|
|
342
388
|
aria-describedby={key === describedKey ? hintId : undefined}
|
|
343
389
|
{tabindex}
|
|
@@ -370,7 +416,9 @@
|
|
|
370
416
|
loaded.includes(source) ? "opacity-100" : "opacity-0",
|
|
371
417
|
)}
|
|
372
418
|
onload={() => markLoaded(source)}
|
|
373
|
-
onerror={() =>
|
|
419
|
+
onerror={() => {
|
|
420
|
+
if (markFailed(source)) onloaderror?.(photo, index);
|
|
421
|
+
}}
|
|
374
422
|
{@attach settled}
|
|
375
423
|
/>
|
|
376
424
|
{/if}
|
|
@@ -382,6 +430,30 @@
|
|
|
382
430
|
<Check size={14} aria-hidden="true" />
|
|
383
431
|
</span>
|
|
384
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}
|
|
385
457
|
{#if more}
|
|
386
458
|
<!-- The photo is dimmed and the number sits on an opaque
|
|
387
459
|
plate, so it reads on any photo in either theme. The
|
|
@@ -399,6 +471,24 @@
|
|
|
399
471
|
</span>
|
|
400
472
|
{/if}
|
|
401
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}
|
|
402
492
|
{#if selects && onopen}
|
|
403
493
|
<!-- Beside the tile's button, not inside it. An opaque
|
|
404
494
|
plate, so it reads over any photo; on a coarse pointer an
|
|
@@ -50,6 +50,17 @@ type Props = Omit<HTMLAttributes<HTMLDivElement>, "class" | "aria-label" | "aria
|
|
|
50
50
|
* Open your own file picker or an ImageUpload from it.
|
|
51
51
|
*/
|
|
52
52
|
onadd?: () => void;
|
|
53
|
+
/**
|
|
54
|
+
* Called once when a thumbnail cannot be loaded, so the page can answer
|
|
55
|
+
* with a new address (a signed one that has run out). A new `thumbSrc`
|
|
56
|
+
* or `src` for the same photo is tried at once.
|
|
57
|
+
*/
|
|
58
|
+
onloaderror?: (photo: Photo, index: number) => void;
|
|
59
|
+
/**
|
|
60
|
+
* Puts a "Try again" button on each photo whose `status` is `error`
|
|
61
|
+
* and is called when it is pressed.
|
|
62
|
+
*/
|
|
63
|
+
onretry?: (photo: Photo, index: number) => void;
|
|
53
64
|
/** Overrides for the built-in strings. */
|
|
54
65
|
strings?: Partial<PhotoGridStrings>;
|
|
55
66
|
/** Extra classes for the host element. */
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
import { cn } from "../util/cn.js";
|
|
17
17
|
import {
|
|
18
18
|
focusFirstElement,
|
|
19
|
+
getFocusableElements,
|
|
20
|
+
isTopOverlay,
|
|
19
21
|
joinOverlayStack,
|
|
20
22
|
recoverStrayFocus,
|
|
21
23
|
returnFocus,
|
|
@@ -77,6 +79,12 @@
|
|
|
77
79
|
* on plates in the theme's own surface colour, so they read over any
|
|
78
80
|
* photo in either theme.
|
|
79
81
|
*
|
|
82
|
+
* It does not take the browser's Back. A history entry made by a
|
|
83
|
+
* component conflicts with the app's router (SvelteKit warns against
|
|
84
|
+
* `history.pushState` outside `$app/navigation`), so open the viewer from
|
|
85
|
+
* the router's own state: push a state on open, read `isOpen` from it,
|
|
86
|
+
* and go back to close (Z-131).
|
|
87
|
+
*
|
|
80
88
|
* Other attributes (`id`, `data-*`, ...) land on the `role="dialog"` panel.
|
|
81
89
|
*/
|
|
82
90
|
type Props = Omit<
|
|
@@ -103,6 +111,18 @@
|
|
|
103
111
|
portal?: boolean;
|
|
104
112
|
/** Fired when the viewer closes itself, with what the user did. */
|
|
105
113
|
onclose?: (detail: { reason: PhotoViewerCloseReason }) => void;
|
|
114
|
+
/**
|
|
115
|
+
* Called once when a full image cannot be loaded, so the page can
|
|
116
|
+
* answer with a new address (a signed one that has run out). A new
|
|
117
|
+
* `src` for the same photo is tried at once.
|
|
118
|
+
*/
|
|
119
|
+
onloaderror?: (photo: Photo, index: number) => void;
|
|
120
|
+
/**
|
|
121
|
+
* Called when "Try again" is pressed on a photo that failed, before
|
|
122
|
+
* the image is asked for again. Return a promise and the button waits
|
|
123
|
+
* for it, busy, so the page has time to fetch a fresh address.
|
|
124
|
+
*/
|
|
125
|
+
onretry?: (photo: Photo, index: number) => void | Promise<void>;
|
|
106
126
|
/** Accessible name of the dialog. */
|
|
107
127
|
label?: string;
|
|
108
128
|
/** Overrides for the built-in strings. */
|
|
@@ -118,6 +138,8 @@
|
|
|
118
138
|
actions = [],
|
|
119
139
|
portal = true,
|
|
120
140
|
onclose,
|
|
141
|
+
onloaderror,
|
|
142
|
+
onretry,
|
|
121
143
|
label = "Photo viewer",
|
|
122
144
|
strings,
|
|
123
145
|
class: className = "",
|
|
@@ -177,6 +199,8 @@
|
|
|
177
199
|
*/
|
|
178
200
|
let openerGrid: Element | null = null;
|
|
179
201
|
const statusId = generateId("photo-viewer-status");
|
|
202
|
+
const captionId = generateId("photo-viewer-caption");
|
|
203
|
+
const creditId = generateId("photo-viewer-credit");
|
|
180
204
|
let rtl = $state(false);
|
|
181
205
|
|
|
182
206
|
// --- Loading -----------------------------------------------------------
|
|
@@ -193,8 +217,13 @@
|
|
|
193
217
|
if (!loaded.includes(source)) loaded = [...loaded, source];
|
|
194
218
|
}
|
|
195
219
|
|
|
220
|
+
/** The page is told once for each address that fails. */
|
|
196
221
|
function markFailed(source: string) {
|
|
197
|
-
if (
|
|
222
|
+
if (failed.includes(source)) return;
|
|
223
|
+
failed = [...failed, source];
|
|
224
|
+
if (!onloaderror) return;
|
|
225
|
+
const at = photos.findIndex((item) => item.src === source);
|
|
226
|
+
if (at !== -1) onloaderror(photos[at], at);
|
|
198
227
|
}
|
|
199
228
|
|
|
200
229
|
/** An image that was complete before anything was listening. */
|
|
@@ -209,7 +238,22 @@
|
|
|
209
238
|
});
|
|
210
239
|
}
|
|
211
240
|
|
|
212
|
-
|
|
241
|
+
/** Addresses whose "Try again" is waiting for the page's `onretry`. */
|
|
242
|
+
let retrying = $state<string[]>([]);
|
|
243
|
+
|
|
244
|
+
async function retry(item: Photo, at: number) {
|
|
245
|
+
const source = item.src;
|
|
246
|
+
if (retrying.includes(source)) return;
|
|
247
|
+
if (onretry) {
|
|
248
|
+
retrying = [...retrying, source];
|
|
249
|
+
try {
|
|
250
|
+
await onretry(item, at);
|
|
251
|
+
} catch {
|
|
252
|
+
// The page's own failure: the image is asked for again all the same.
|
|
253
|
+
} finally {
|
|
254
|
+
retrying = retrying.filter((candidate) => candidate !== source);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
213
257
|
failed = failed.filter((candidate) => candidate !== source);
|
|
214
258
|
attempts = { ...attempts, [source]: (attempts[source] ?? 0) + 1 };
|
|
215
259
|
// The button that was pressed goes with the message it was in: the
|
|
@@ -719,6 +763,37 @@
|
|
|
719
763
|
if (!active || active === document.body) container.focus({ preventScroll: true });
|
|
720
764
|
});
|
|
721
765
|
|
|
766
|
+
/**
|
|
767
|
+
* An action's button can go while it holds the focus: "Remove" is pressed,
|
|
768
|
+
* the photo leaves `photos`, and the one that shows next has no "Remove".
|
|
769
|
+
* Nothing tells the page when a focused element is taken out, and a
|
|
770
|
+
* confirm dialog between the press and the removal hands the focus back
|
|
771
|
+
* to a button that is gone. So the page is watched while the viewer is
|
|
772
|
+
* open: when something is taken out or put in and the focus is found on
|
|
773
|
+
* `<body>` with this viewer on top, its first control (Close) takes it.
|
|
774
|
+
*/
|
|
775
|
+
$effect(() => {
|
|
776
|
+
const container = panel;
|
|
777
|
+
if (!isOpen || !container || typeof MutationObserver !== "function") return;
|
|
778
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
779
|
+
const check = () => {
|
|
780
|
+
timer = undefined;
|
|
781
|
+
if (!focusActive || !container.isConnected || !isTopOverlay(container)) return;
|
|
782
|
+
const active = document.activeElement;
|
|
783
|
+
if (active && active !== document.body) return;
|
|
784
|
+
(getFocusableElements(container)[0] ?? container).focus({ preventScroll: true });
|
|
785
|
+
};
|
|
786
|
+
// After the change has settled: a dialog that is closing gives the focus back first.
|
|
787
|
+
const observer = new MutationObserver(() => {
|
|
788
|
+
if (timer === undefined) timer = setTimeout(check, 0);
|
|
789
|
+
});
|
|
790
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
791
|
+
return () => {
|
|
792
|
+
observer.disconnect();
|
|
793
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
794
|
+
};
|
|
795
|
+
});
|
|
796
|
+
|
|
722
797
|
function showPhoto(turn: number) {
|
|
723
798
|
captionOpen = false;
|
|
724
799
|
void go(turn);
|
|
@@ -780,7 +855,12 @@
|
|
|
780
855
|
>
|
|
781
856
|
<ImageOff size={24} aria-hidden="true" />
|
|
782
857
|
<p>{text.loadError}</p>
|
|
783
|
-
<Button
|
|
858
|
+
<Button
|
|
859
|
+
variant="secondary"
|
|
860
|
+
size="lg"
|
|
861
|
+
loading={retrying.includes(source)}
|
|
862
|
+
onclick={() => retry(item, at)}
|
|
863
|
+
>
|
|
784
864
|
{text.retry}
|
|
785
865
|
</Button>
|
|
786
866
|
</div>
|
|
@@ -849,7 +929,9 @@
|
|
|
849
929
|
role="dialog"
|
|
850
930
|
aria-modal="true"
|
|
851
931
|
aria-label={label}
|
|
852
|
-
aria-describedby={statusId
|
|
932
|
+
aria-describedby={[statusId, photo.caption ? captionId : "", photo.credit ? creditId : ""]
|
|
933
|
+
.filter(Boolean)
|
|
934
|
+
.join(" ")}
|
|
853
935
|
tabindex="-1"
|
|
854
936
|
data-zoomed={zoomed ? "true" : undefined}
|
|
855
937
|
{...restProps}
|
|
@@ -950,40 +1032,55 @@
|
|
|
950
1032
|
{/if}
|
|
951
1033
|
|
|
952
1034
|
<!-- Bottom: the caption, then what can be done with the photo. -->
|
|
953
|
-
{#if photo.caption || actions.length > 0}
|
|
1035
|
+
{#if photo.caption || photo.credit || actions.length > 0}
|
|
954
1036
|
<div
|
|
955
1037
|
class={cn(
|
|
956
1038
|
"pointer-events-none absolute inset-x-0 bottom-0 flex flex-col items-center gap-[8px] pb-[max(8px,env(safe-area-inset-bottom))]",
|
|
957
1039
|
sides,
|
|
958
1040
|
)}
|
|
959
1041
|
>
|
|
960
|
-
{#if photo.caption}
|
|
1042
|
+
{#if photo.caption || photo.credit}
|
|
961
1043
|
<div
|
|
962
1044
|
class={cn(
|
|
963
1045
|
plate,
|
|
964
1046
|
"pointer-events-auto flex w-full items-end gap-[8px] rounded-container px-3 py-2 text-sm sm:w-[min(100%,40rem)]",
|
|
1047
|
+
// The credit takes a line of its own under the caption.
|
|
1048
|
+
photo.credit && "flex-wrap gap-y-1",
|
|
965
1049
|
)}
|
|
966
1050
|
data-photo-viewer-caption
|
|
967
1051
|
>
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
</p>
|
|
978
|
-
{#if captionLong || captionOpen}
|
|
979
|
-
<button
|
|
980
|
-
type="button"
|
|
981
|
-
class="focus-ring -my-1 -me-1 min-h-11 shrink-0 cursor-pointer rounded-[calc(var(--radius-container)-4px)] px-2 text-sm font-medium text-link"
|
|
982
|
-
aria-expanded={captionOpen}
|
|
983
|
-
onclick={() => (captionOpen = !captionOpen)}
|
|
1052
|
+
{#if photo.caption}
|
|
1053
|
+
<p
|
|
1054
|
+
id={captionId}
|
|
1055
|
+
bind:this={captionText}
|
|
1056
|
+
class={cn(
|
|
1057
|
+
"min-w-0 flex-1 [overflow-wrap:anywhere]",
|
|
1058
|
+
// The one thing in the viewer that scrolls under a finger.
|
|
1059
|
+
captionOpen ? "max-h-[40dvh] touch-pan-y overflow-y-auto" : "line-clamp-2",
|
|
1060
|
+
)}
|
|
984
1061
|
>
|
|
985
|
-
{
|
|
986
|
-
</
|
|
1062
|
+
{photo.caption}
|
|
1063
|
+
</p>
|
|
1064
|
+
{#if captionLong || captionOpen}
|
|
1065
|
+
<button
|
|
1066
|
+
type="button"
|
|
1067
|
+
class="focus-ring -my-1 -me-1 min-h-11 shrink-0 cursor-pointer rounded-[calc(var(--radius-container)-4px)] px-2 text-sm font-medium text-link"
|
|
1068
|
+
aria-expanded={captionOpen}
|
|
1069
|
+
onclick={() => (captionOpen = !captionOpen)}
|
|
1070
|
+
>
|
|
1071
|
+
{captionOpen ? text.showLess : text.showMore}
|
|
1072
|
+
</button>
|
|
1073
|
+
{/if}
|
|
1074
|
+
{/if}
|
|
1075
|
+
{#if photo.credit}
|
|
1076
|
+
<!-- Quieter than the caption, and outside what More and Less fold. -->
|
|
1077
|
+
<p
|
|
1078
|
+
id={creditId}
|
|
1079
|
+
class="min-w-0 basis-full text-description [overflow-wrap:anywhere]"
|
|
1080
|
+
data-photo-viewer-credit
|
|
1081
|
+
>
|
|
1082
|
+
{photo.credit}
|
|
1083
|
+
</p>
|
|
987
1084
|
{/if}
|
|
988
1085
|
</div>
|
|
989
1086
|
{/if}
|
|
@@ -20,6 +20,12 @@ import { type Photo, type PhotoViewerAction, type PhotoViewerCloseReason, type P
|
|
|
20
20
|
* on plates in the theme's own surface colour, so they read over any
|
|
21
21
|
* photo in either theme.
|
|
22
22
|
*
|
|
23
|
+
* It does not take the browser's Back. A history entry made by a
|
|
24
|
+
* component conflicts with the app's router (SvelteKit warns against
|
|
25
|
+
* `history.pushState` outside `$app/navigation`), so open the viewer from
|
|
26
|
+
* the router's own state: push a state on open, read `isOpen` from it,
|
|
27
|
+
* and go back to close (Z-131).
|
|
28
|
+
*
|
|
23
29
|
* Other attributes (`id`, `data-*`, ...) land on the `role="dialog"` panel.
|
|
24
30
|
*/
|
|
25
31
|
type Props = Omit<HTMLAttributes<HTMLDivElement>, "class" | "onclose" | "aria-label"> & {
|
|
@@ -44,6 +50,18 @@ type Props = Omit<HTMLAttributes<HTMLDivElement>, "class" | "onclose" | "aria-la
|
|
|
44
50
|
onclose?: (detail: {
|
|
45
51
|
reason: PhotoViewerCloseReason;
|
|
46
52
|
}) => void;
|
|
53
|
+
/**
|
|
54
|
+
* Called once when a full image cannot be loaded, so the page can
|
|
55
|
+
* answer with a new address (a signed one that has run out). A new
|
|
56
|
+
* `src` for the same photo is tried at once.
|
|
57
|
+
*/
|
|
58
|
+
onloaderror?: (photo: Photo, index: number) => void;
|
|
59
|
+
/**
|
|
60
|
+
* Called when "Try again" is pressed on a photo that failed, before
|
|
61
|
+
* the image is asked for again. Return a promise and the button waits
|
|
62
|
+
* for it, busy, so the page has time to fetch a fresh address.
|
|
63
|
+
*/
|
|
64
|
+
onretry?: (photo: Photo, index: number) => void | Promise<void>;
|
|
47
65
|
/** Accessible name of the dialog. */
|
|
48
66
|
label?: string;
|
|
49
67
|
/** Overrides for the built-in strings. */
|