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
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
4
|
+
import { cn } from "../util/cn.js";
|
|
5
|
+
import { displayVoice } from "../util/display-voice.js";
|
|
6
|
+
|
|
7
|
+
type StatSize = "sm" | "md" | "lg";
|
|
8
|
+
|
|
9
|
+
type Props = Omit<HTMLAttributes<HTMLDivElement>, "class"> & {
|
|
10
|
+
/** The figure, shown as given: the app formats numbers for its locale. */
|
|
11
|
+
value: string | number;
|
|
12
|
+
label?: string;
|
|
13
|
+
/** Drawn after the value, smaller, on the same baseline ("av 19"). */
|
|
14
|
+
unit?: string;
|
|
15
|
+
size?: StatSize;
|
|
16
|
+
align?: "start" | "center" | "end";
|
|
17
|
+
labelPosition?: "below" | "above";
|
|
18
|
+
class?: string;
|
|
19
|
+
/** Replaces `value` for a figure that needs markup. */
|
|
20
|
+
children?: Snippet;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let {
|
|
24
|
+
value,
|
|
25
|
+
label = "",
|
|
26
|
+
unit = "",
|
|
27
|
+
size = "md",
|
|
28
|
+
align = "start",
|
|
29
|
+
labelPosition = "below",
|
|
30
|
+
class: className = "",
|
|
31
|
+
children,
|
|
32
|
+
...restProps
|
|
33
|
+
}: Props = $props();
|
|
34
|
+
|
|
35
|
+
/** Value 24 / 36 / 48px, unit 14 / 16 / 20px, label 12 / 14 / 14px. */
|
|
36
|
+
const sizeClasses: Record<StatSize, { value: string; unit: string; label: string }> = {
|
|
37
|
+
sm: { value: "text-2xl", unit: "text-sm", label: "text-xs" },
|
|
38
|
+
md: { value: "text-4xl", unit: "text-base", label: "text-sm" },
|
|
39
|
+
lg: { value: "text-5xl", unit: "text-xl", label: "text-sm" },
|
|
40
|
+
};
|
|
41
|
+
const alignClasses = {
|
|
42
|
+
start: "items-start text-start",
|
|
43
|
+
center: "items-center text-center",
|
|
44
|
+
end: "items-end text-end",
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
47
|
+
const s = $derived(sizeClasses[size] ?? sizeClasses.md);
|
|
48
|
+
const hostClasses = $derived(
|
|
49
|
+
cn(`flex flex-col gap-1 ${alignClasses[align] ?? alignClasses.start} ${className}`),
|
|
50
|
+
);
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
{#snippet figure()}
|
|
54
|
+
<div class="inline-flex items-baseline gap-1 whitespace-nowrap">
|
|
55
|
+
<span class="font-display text-headline tabular-nums leading-none {displayVoice} {s.value}">
|
|
56
|
+
{#if children}{@render children()}{:else}{value}{/if}
|
|
57
|
+
</span>
|
|
58
|
+
{#if unit}
|
|
59
|
+
<span class="text-description font-medium {s.unit}">{unit}</span>
|
|
60
|
+
{/if}
|
|
61
|
+
</div>
|
|
62
|
+
{/snippet}
|
|
63
|
+
|
|
64
|
+
<div class={hostClasses} {...restProps}>
|
|
65
|
+
{#if labelPosition === "above"}
|
|
66
|
+
{#if label}<span class="text-description {s.label}">{label}</span>{/if}
|
|
67
|
+
{@render figure()}
|
|
68
|
+
{:else}
|
|
69
|
+
{@render figure()}
|
|
70
|
+
{#if label}<span class="text-description {s.label}">{label}</span>{/if}
|
|
71
|
+
{/if}
|
|
72
|
+
</div>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
type StatSize = "sm" | "md" | "lg";
|
|
4
|
+
type Props = Omit<HTMLAttributes<HTMLDivElement>, "class"> & {
|
|
5
|
+
/** The figure, shown as given: the app formats numbers for its locale. */
|
|
6
|
+
value: string | number;
|
|
7
|
+
label?: string;
|
|
8
|
+
/** Drawn after the value, smaller, on the same baseline ("av 19"). */
|
|
9
|
+
unit?: string;
|
|
10
|
+
size?: StatSize;
|
|
11
|
+
align?: "start" | "center" | "end";
|
|
12
|
+
labelPosition?: "below" | "above";
|
|
13
|
+
class?: string;
|
|
14
|
+
/** Replaces `value` for a figure that needs markup. */
|
|
15
|
+
children?: Snippet;
|
|
16
|
+
};
|
|
17
|
+
declare const Stat: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type Stat = ReturnType<typeof Stat>;
|
|
19
|
+
export default Stat;
|
|
@@ -1,16 +1,59 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Remembered for the page's life: the platform's own picker refused to
|
|
4
|
+
* open (or is not there), so a field that wanted it shows its native input.
|
|
5
|
+
*/
|
|
6
|
+
let nativePickerFailed = false;
|
|
7
|
+
</script>
|
|
8
|
+
|
|
1
9
|
<script lang="ts">
|
|
10
|
+
import CalendarIcon from "@lucide/svelte/icons/calendar";
|
|
11
|
+
import Clock from "@lucide/svelte/icons/clock";
|
|
12
|
+
import { onMount, tick } from "svelte";
|
|
2
13
|
import type { SizeVariant } from "../types/variants.js";
|
|
3
14
|
import { cn } from "../util/cn.js";
|
|
15
|
+
import { clampDate, formatDate, formatTime, isIsoDate, monthOf, todayIso } from "../util/date.js";
|
|
16
|
+
import { mergeStrings } from "../util/ready-made-strings.js";
|
|
17
|
+
import { generateId } from "../util/ssr-safe.js";
|
|
18
|
+
import {
|
|
19
|
+
DATE_FIELD_STRINGS,
|
|
20
|
+
TIME_FIELD_STRINGS,
|
|
21
|
+
hasLocale,
|
|
22
|
+
hourChoices,
|
|
23
|
+
minuteChoices,
|
|
24
|
+
minuteStep,
|
|
25
|
+
openingTime,
|
|
26
|
+
parseHourMinute,
|
|
27
|
+
stepFitsLibraryPicker,
|
|
28
|
+
timeInRange,
|
|
29
|
+
type DateFieldStrings,
|
|
30
|
+
type TimeFieldStrings,
|
|
31
|
+
} from "../util/temporal-field.js";
|
|
32
|
+
import { zabiStringsFor } from "../util/zabi-strings.js";
|
|
33
|
+
import BottomSheet from "../molecules/BottomSheet.svelte";
|
|
34
|
+
import Calendar from "../molecules/Calendar.svelte";
|
|
35
|
+
import Button from "./Button.svelte";
|
|
4
36
|
import Input from "./Input.svelte";
|
|
37
|
+
import PickerField from "./PickerField.svelte";
|
|
38
|
+
import TimeColumn from "./TimeColumn.svelte";
|
|
5
39
|
|
|
6
40
|
/**
|
|
7
41
|
* What DateField and TimeField share. Internal: it is not exported from
|
|
8
42
|
* the package, and its props are those of the two fields.
|
|
9
43
|
*
|
|
10
|
-
*
|
|
11
|
-
* the hint and the error message are Input's own and
|
|
12
|
-
* What is added here is only what a native date or
|
|
13
|
-
* look like one: see the classes and the style block
|
|
44
|
+
* Without `locale` it is an `Input` with a native `type`, so the box, the
|
|
45
|
+
* sizes, the label, the hint and the error message are Input's own and
|
|
46
|
+
* cannot drift from it. What is added here is only what a native date or
|
|
47
|
+
* time input needs to look like one: see the classes and the style block
|
|
48
|
+
* below.
|
|
49
|
+
*
|
|
50
|
+
* With `locale`, the same on the server and until the page has run: the
|
|
51
|
+
* native input, which works and submits without scripts. Once mounted the
|
|
52
|
+
* native input stays as the form control, out of sight, the tab order and
|
|
53
|
+
* the accessibility tree, and a field-shaped button (PickerField) takes
|
|
54
|
+
* its place: it shows the value written by `Intl.DateTimeFormat` and opens
|
|
55
|
+
* the library's picker in a BottomSheet (a Calendar; two columns for a
|
|
56
|
+
* time), or, with `picker="native"`, the platform's own.
|
|
14
57
|
*/
|
|
15
58
|
interface Props {
|
|
16
59
|
kind: "date" | "time";
|
|
@@ -29,6 +72,12 @@
|
|
|
29
72
|
readonly?: boolean;
|
|
30
73
|
size?: SizeVariant;
|
|
31
74
|
class?: string;
|
|
75
|
+
locale?: string | string[];
|
|
76
|
+
format?: Intl.DateTimeFormatOptions;
|
|
77
|
+
placeholder?: string;
|
|
78
|
+
picker?: "library" | "native";
|
|
79
|
+
strings?: Partial<DateFieldStrings> | Partial<TimeFieldStrings>;
|
|
80
|
+
openAt?: string;
|
|
32
81
|
"aria-describedby"?: string | null;
|
|
33
82
|
}
|
|
34
83
|
|
|
@@ -49,6 +98,12 @@
|
|
|
49
98
|
readonly = false,
|
|
50
99
|
size = "md",
|
|
51
100
|
class: className = "",
|
|
101
|
+
locale,
|
|
102
|
+
format,
|
|
103
|
+
placeholder = "",
|
|
104
|
+
picker = "library",
|
|
105
|
+
strings,
|
|
106
|
+
openAt,
|
|
52
107
|
"aria-describedby": describedBy,
|
|
53
108
|
...restProps
|
|
54
109
|
}: Props = $props();
|
|
@@ -64,6 +119,195 @@
|
|
|
64
119
|
applyDefaults();
|
|
65
120
|
$effect.pre(applyDefaults);
|
|
66
121
|
|
|
122
|
+
/** The words of a provider above, then this field's own. */
|
|
123
|
+
const dateProvided = zabiStringsFor("dateField");
|
|
124
|
+
const timeProvided = zabiStringsFor("timeField");
|
|
125
|
+
const dateText = $derived(mergeStrings(DATE_FIELD_STRINGS, dateProvided(), strings as Partial<DateFieldStrings>));
|
|
126
|
+
const timeText = $derived(mergeStrings(TIME_FIELD_STRINGS, timeProvided(), strings as Partial<TimeFieldStrings>));
|
|
127
|
+
|
|
128
|
+
// The id of the field. Only a field with a locale needs to know it: the
|
|
129
|
+
// native input and the button must not share one once the button is in.
|
|
130
|
+
// svelte-ignore state_referenced_locally
|
|
131
|
+
const ownId = hasLocale(locale) ? generateId("input") : undefined;
|
|
132
|
+
const baseId = $derived(idProp ?? ownId);
|
|
133
|
+
|
|
134
|
+
let root: HTMLDivElement | undefined = $state();
|
|
135
|
+
let trigger = $state<HTMLButtonElement | HTMLAnchorElement | null>(null);
|
|
136
|
+
|
|
137
|
+
/** True once the page has run. The server and the first render never are. */
|
|
138
|
+
let mounted = $state(false);
|
|
139
|
+
let supportsNativePicker = $state(false);
|
|
140
|
+
let sessionBroken = $state(false);
|
|
141
|
+
/** This field's native input was put back in sight: the platform's picker would not open. */
|
|
142
|
+
let unhidden = $state(false);
|
|
143
|
+
let isOpen = $state(false);
|
|
144
|
+
|
|
145
|
+
onMount(() => {
|
|
146
|
+
supportsNativePicker = typeof HTMLInputElement !== "undefined" && typeof HTMLInputElement.prototype.showPicker === "function";
|
|
147
|
+
sessionBroken = nativePickerFailed;
|
|
148
|
+
mounted = true;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
/** `library`, unless the app asked for the platform's, or the library's picker cannot serve this `step`. */
|
|
152
|
+
const effectivePicker = $derived(
|
|
153
|
+
picker === "native" || (kind === "time" && !stepFitsLibraryPicker(step)) ? "native" : "library",
|
|
154
|
+
);
|
|
155
|
+
const enhanced = $derived(
|
|
156
|
+
mounted &&
|
|
157
|
+
hasLocale(locale) &&
|
|
158
|
+
!unhidden &&
|
|
159
|
+
(effectivePicker === "library" || (supportsNativePicker && !sessionBroken)),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const display = $derived(
|
|
163
|
+
!value
|
|
164
|
+
? ""
|
|
165
|
+
: kind === "date"
|
|
166
|
+
? formatDate(value, locale, format)
|
|
167
|
+
: formatTime(value, locale, format),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const text = $derived(kind === "date" ? dateText : timeText);
|
|
171
|
+
|
|
172
|
+
/** What the button is named by when the label is not on the page: the label and the value. */
|
|
173
|
+
const hiddenLabelName = $derived(hideLabel && label ? [label, display || placeholder].filter(Boolean).join(", ") : undefined);
|
|
174
|
+
|
|
175
|
+
const nativeInput = () => root?.querySelector<HTMLInputElement>("input[data-temporal-native]") ?? null;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* What the picker chose goes into the native input, which is the form
|
|
179
|
+
* control: its value, and real `input` and `change` events, so a listener
|
|
180
|
+
* the app put on the field hears it as it hears a native change. The
|
|
181
|
+
* binding follows from the `input` event.
|
|
182
|
+
*/
|
|
183
|
+
function setValue(next: string) {
|
|
184
|
+
if (next === value) return;
|
|
185
|
+
const input = nativeInput();
|
|
186
|
+
if (input) {
|
|
187
|
+
input.value = next;
|
|
188
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
189
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
190
|
+
}
|
|
191
|
+
value = next;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function closeAndFocus() {
|
|
195
|
+
isOpen = false;
|
|
196
|
+
await tick();
|
|
197
|
+
trigger?.focus();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---- the sheet -------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
let calendarMonth = $state<string | undefined>();
|
|
203
|
+
let sheetSnap = $state<"half" | "full">("half");
|
|
204
|
+
let draftHour = $state("");
|
|
205
|
+
let draftMinute = $state("");
|
|
206
|
+
/** Where the columns stand while nothing is chosen. Set as the sheet opens; never written to `value`. */
|
|
207
|
+
let anchorHour = $state("");
|
|
208
|
+
let anchorMinute = $state("");
|
|
209
|
+
|
|
210
|
+
function openPicker() {
|
|
211
|
+
if (disabled || readonly) return;
|
|
212
|
+
if (effectivePicker === "native") {
|
|
213
|
+
openNative();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (kind === "date") {
|
|
217
|
+
calendarMonth = monthOf(isIsoDate(value) ? value : clampDate(todayIso(), min, max)) ?? undefined;
|
|
218
|
+
sheetSnap = typeof window !== "undefined" && window.innerHeight >= 900 ? "half" : "full";
|
|
219
|
+
} else {
|
|
220
|
+
const now = parseHourMinute(value);
|
|
221
|
+
draftHour = now ? String(now[0]).padStart(2, "0") : "";
|
|
222
|
+
draftMinute = now ? String(now[1]).padStart(2, "0") : "";
|
|
223
|
+
const clock = new Date();
|
|
224
|
+
[anchorHour, anchorMinute] = openingTime({
|
|
225
|
+
openAt,
|
|
226
|
+
now: [clock.getHours(), clock.getMinutes()],
|
|
227
|
+
stepMinutes: minuteStep(step),
|
|
228
|
+
min,
|
|
229
|
+
max,
|
|
230
|
+
});
|
|
231
|
+
sheetSnap = typeof window !== "undefined" && window.innerHeight >= 700 ? "half" : "full";
|
|
232
|
+
}
|
|
233
|
+
isOpen = true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The platform's picker, from inside the press that asked for it. Where it is refused, the native input comes back. */
|
|
237
|
+
function openNative() {
|
|
238
|
+
const input = nativeInput();
|
|
239
|
+
try {
|
|
240
|
+
if (!input) throw new Error("no input");
|
|
241
|
+
input.showPicker();
|
|
242
|
+
} catch {
|
|
243
|
+
nativePickerFailed = true;
|
|
244
|
+
unhidden = true;
|
|
245
|
+
void tick().then(() => root?.querySelector<HTMLInputElement>("input")?.focus());
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const canClear = $derived(!required && value !== "");
|
|
250
|
+
const sheetTitle = $derived(label || (kind === "date" ? dateText.chooseDate : timeText.chooseTime));
|
|
251
|
+
|
|
252
|
+
function chooseDay(date: string) {
|
|
253
|
+
setValue(date);
|
|
254
|
+
void closeAndFocus();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function clear() {
|
|
258
|
+
setValue("");
|
|
259
|
+
void closeAndFocus();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ---- the time columns -----------------------------------------------
|
|
263
|
+
|
|
264
|
+
const hours = $derived(hourChoices(locale, min, max));
|
|
265
|
+
const minutes = $derived(
|
|
266
|
+
minuteChoices({ locale, stepMinutes: minuteStep(step), hour: draftHour, current: value, min, max }),
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
function chooseHour(hour: string) {
|
|
270
|
+
draftHour = hour;
|
|
271
|
+
const options = minuteChoices({ locale, stepMinutes: minuteStep(step), hour, current: value, min, max });
|
|
272
|
+
// A minute the new hour does not allow: the first one it does. None yet: the one
|
|
273
|
+
// `openAt` asked for if the hour allows it (not the one "now" happens to stand at), else the first.
|
|
274
|
+
if (!draftMinute && parseHourMinute(openAt) && options.find((option) => option.value === anchorMinute)?.disabled === false) {
|
|
275
|
+
draftMinute = anchorMinute;
|
|
276
|
+
} else if (!draftMinute || options.find((option) => option.value === draftMinute)?.disabled !== false) {
|
|
277
|
+
draftMinute = options.find((option) => !option.disabled)?.value ?? draftMinute;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const draftValid = $derived(
|
|
282
|
+
draftHour !== "" && draftMinute !== "" && timeInRange(Number(draftHour), Number(draftMinute), min, max),
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
function done() {
|
|
286
|
+
if (!draftValid) return;
|
|
287
|
+
const next = `${draftHour}:${draftMinute}`;
|
|
288
|
+
// The seconds of a value that is kept are kept.
|
|
289
|
+
const kept = parseHourMinute(value);
|
|
290
|
+
setValue(kept && `${String(kept[0]).padStart(2, "0")}:${String(kept[1]).padStart(2, "0")}` === next ? value : next);
|
|
291
|
+
void closeAndFocus();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---- the native input, out of sight once the button is in -----------
|
|
295
|
+
|
|
296
|
+
const HIDDEN_STYLE =
|
|
297
|
+
"position:absolute;inline-size:1px;block-size:1px;margin:-1px;padding:0;border:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;opacity:0;pointer-events:none";
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* The browser's own validation (`required` and empty) fires on the hidden
|
|
301
|
+
* input, where its bubble cannot be shown: that is cancelled, and focus
|
|
302
|
+
* goes to the button. The message people read is the field's `error`.
|
|
303
|
+
*/
|
|
304
|
+
function handleInvalid(event: Event) {
|
|
305
|
+
(restProps as { oninvalid?: (event: Event) => void }).oninvalid?.(event);
|
|
306
|
+
if (!enhanced) return;
|
|
307
|
+
event.preventDefault();
|
|
308
|
+
trigger?.focus();
|
|
309
|
+
}
|
|
310
|
+
|
|
67
311
|
/** What Input does not declare, but passes on to the `<input>`. */
|
|
68
312
|
const native = $derived({
|
|
69
313
|
min,
|
|
@@ -72,6 +316,15 @@
|
|
|
72
316
|
readonly: readonly || undefined,
|
|
73
317
|
"data-empty": value ? undefined : "",
|
|
74
318
|
...restProps,
|
|
319
|
+
...(hasLocale(locale) ? { oninvalid: handleInvalid } : {}),
|
|
320
|
+
...(enhanced
|
|
321
|
+
? {
|
|
322
|
+
tabindex: -1,
|
|
323
|
+
"aria-hidden": "true" as const,
|
|
324
|
+
"data-temporal-native": "",
|
|
325
|
+
style: HIDDEN_STYLE,
|
|
326
|
+
}
|
|
327
|
+
: {}),
|
|
75
328
|
});
|
|
76
329
|
|
|
77
330
|
const fieldClasses = $derived(
|
|
@@ -89,25 +342,104 @@
|
|
|
89
342
|
);
|
|
90
343
|
</script>
|
|
91
344
|
|
|
92
|
-
<div>
|
|
345
|
+
<div bind:this={root}>
|
|
346
|
+
{#if enhanced}
|
|
347
|
+
<PickerField
|
|
348
|
+
bind:element={trigger}
|
|
349
|
+
id={baseId}
|
|
350
|
+
label={hideLabel ? "" : label}
|
|
351
|
+
value={display}
|
|
352
|
+
{placeholder}
|
|
353
|
+
{size}
|
|
354
|
+
{disabled}
|
|
355
|
+
{hint}
|
|
356
|
+
{error}
|
|
357
|
+
expanded={isOpen}
|
|
358
|
+
aria-describedby={describedBy}
|
|
359
|
+
aria-label={hiddenLabelName}
|
|
360
|
+
data-readonly={readonly ? "" : undefined}
|
|
361
|
+
onclick={openPicker}
|
|
362
|
+
>
|
|
363
|
+
{#snippet trailing()}
|
|
364
|
+
{#if kind === "date"}
|
|
365
|
+
<CalendarIcon size={20} aria-hidden="true" />
|
|
366
|
+
{:else}
|
|
367
|
+
<Clock size={20} aria-hidden="true" />
|
|
368
|
+
{/if}
|
|
369
|
+
{/snippet}
|
|
370
|
+
</PickerField>
|
|
371
|
+
{/if}
|
|
93
372
|
<Input
|
|
94
|
-
id={
|
|
373
|
+
id={enhanced && baseId ? `${baseId}-native` : baseId}
|
|
95
374
|
type={kind}
|
|
96
375
|
bind:value
|
|
97
376
|
{name}
|
|
98
377
|
{label}
|
|
99
|
-
{hideLabel}
|
|
378
|
+
hideLabel={hideLabel || enhanced}
|
|
100
379
|
{required}
|
|
101
380
|
{disabled}
|
|
102
381
|
{size}
|
|
103
|
-
{hint}
|
|
104
|
-
{error}
|
|
105
|
-
aria-describedby={describedBy}
|
|
382
|
+
hint={enhanced ? "" : hint}
|
|
383
|
+
error={enhanced ? "" : error}
|
|
384
|
+
aria-describedby={enhanced ? undefined : describedBy}
|
|
106
385
|
class={fieldClasses}
|
|
107
386
|
{...native}
|
|
108
387
|
/>
|
|
109
388
|
</div>
|
|
110
389
|
|
|
390
|
+
{#snippet clearAction()}
|
|
391
|
+
<Button variant="ghost" onclick={clear}>{text.clear}</Button>
|
|
392
|
+
{/snippet}
|
|
393
|
+
|
|
394
|
+
{#snippet doneAction()}
|
|
395
|
+
{#if canClear}
|
|
396
|
+
{@render clearAction()}
|
|
397
|
+
{/if}
|
|
398
|
+
{#if draftHour !== "" && draftMinute !== ""}
|
|
399
|
+
<span data-chosen-time class="me-auto min-w-0 self-center text-sm text-body">
|
|
400
|
+
{timeText.chosenTime}
|
|
401
|
+
<span class="font-semibold">{formatTime(`${draftHour}:${draftMinute}`, locale, format)}</span>
|
|
402
|
+
</span>
|
|
403
|
+
{/if}
|
|
404
|
+
<Button variant="primary" disabled={!draftValid} onclick={done}>{timeText.done}</Button>
|
|
405
|
+
{/snippet}
|
|
406
|
+
|
|
407
|
+
{#if enhanced && effectivePicker === "library"}
|
|
408
|
+
{#if kind === "date"}
|
|
409
|
+
<BottomSheet
|
|
410
|
+
bind:isOpen
|
|
411
|
+
title={sheetTitle}
|
|
412
|
+
snap={sheetSnap}
|
|
413
|
+
initialFocus="[data-date][tabindex='0']"
|
|
414
|
+
data-temporal-sheet="date"
|
|
415
|
+
{...canClear ? { footer: clearAction } : {}}
|
|
416
|
+
>
|
|
417
|
+
<Calendar
|
|
418
|
+
bind:month={calendarMonth}
|
|
419
|
+
selected={isIsoDate(value) ? value : null}
|
|
420
|
+
locale={Array.isArray(locale) ? locale[0] : locale}
|
|
421
|
+
{min}
|
|
422
|
+
{max}
|
|
423
|
+
onselect={chooseDay}
|
|
424
|
+
/>
|
|
425
|
+
</BottomSheet>
|
|
426
|
+
{:else}
|
|
427
|
+
<BottomSheet
|
|
428
|
+
bind:isOpen
|
|
429
|
+
title={sheetTitle}
|
|
430
|
+
snap={sheetSnap}
|
|
431
|
+
initialFocus="[role='option'][tabindex='0']"
|
|
432
|
+
data-temporal-sheet="time"
|
|
433
|
+
footer={doneAction}
|
|
434
|
+
>
|
|
435
|
+
<div class="flex gap-3">
|
|
436
|
+
<TimeColumn label={timeText.hours} options={hours} selected={draftHour} openAt={anchorHour} onselect={chooseHour} />
|
|
437
|
+
<TimeColumn label={timeText.minutes} options={minutes} selected={draftMinute} openAt={anchorMinute} onselect={(minute) => (draftMinute = minute)} />
|
|
438
|
+
</div>
|
|
439
|
+
</BottomSheet>
|
|
440
|
+
{/if}
|
|
441
|
+
{/if}
|
|
442
|
+
|
|
111
443
|
<style>
|
|
112
444
|
/*
|
|
113
445
|
* The parts of a native date or time input that utilities cannot reach.
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import type { SizeVariant } from "../types/variants.js";
|
|
2
|
+
import { type DateFieldStrings, type TimeFieldStrings } from "../util/temporal-field.js";
|
|
2
3
|
/**
|
|
3
4
|
* What DateField and TimeField share. Internal: it is not exported from
|
|
4
5
|
* the package, and its props are those of the two fields.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
* the hint and the error message are Input's own and
|
|
8
|
-
* What is added here is only what a native date or
|
|
9
|
-
* look like one: see the classes and the style block
|
|
7
|
+
* Without `locale` it is an `Input` with a native `type`, so the box, the
|
|
8
|
+
* sizes, the label, the hint and the error message are Input's own and
|
|
9
|
+
* cannot drift from it. What is added here is only what a native date or
|
|
10
|
+
* time input needs to look like one: see the classes and the style block
|
|
11
|
+
* below.
|
|
12
|
+
*
|
|
13
|
+
* With `locale`, the same on the server and until the page has run: the
|
|
14
|
+
* native input, which works and submits without scripts. Once mounted the
|
|
15
|
+
* native input stays as the form control, out of sight, the tab order and
|
|
16
|
+
* the accessibility tree, and a field-shaped button (PickerField) takes
|
|
17
|
+
* its place: it shows the value written by `Intl.DateTimeFormat` and opens
|
|
18
|
+
* the library's picker in a BottomSheet (a Calendar; two columns for a
|
|
19
|
+
* time), or, with `picker="native"`, the platform's own.
|
|
10
20
|
*/
|
|
11
21
|
interface Props {
|
|
12
22
|
kind: "date" | "time";
|
|
@@ -25,6 +35,12 @@ interface Props {
|
|
|
25
35
|
readonly?: boolean;
|
|
26
36
|
size?: SizeVariant;
|
|
27
37
|
class?: string;
|
|
38
|
+
locale?: string | string[];
|
|
39
|
+
format?: Intl.DateTimeFormatOptions;
|
|
40
|
+
placeholder?: string;
|
|
41
|
+
picker?: "library" | "native";
|
|
42
|
+
strings?: Partial<DateFieldStrings> | Partial<TimeFieldStrings>;
|
|
43
|
+
openAt?: string;
|
|
28
44
|
"aria-describedby"?: string | null;
|
|
29
45
|
}
|
|
30
46
|
declare const TemporalField: import("svelte").Component<Props, {}, "value">;
|