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,381 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
import Table from "../atoms/Table.svelte";
|
|
4
|
+
import { cn } from "../util/cn.js";
|
|
5
|
+
import {
|
|
6
|
+
CHART_GLASS,
|
|
7
|
+
chartColor,
|
|
8
|
+
chartFormatter,
|
|
9
|
+
barPath,
|
|
10
|
+
chartScale,
|
|
11
|
+
chartTextScale,
|
|
12
|
+
labelEvery,
|
|
13
|
+
measureWidth,
|
|
14
|
+
widestText,
|
|
15
|
+
type ChartColor,
|
|
16
|
+
type ChartDatum,
|
|
17
|
+
type ChartFormat,
|
|
18
|
+
} from "../util/chart.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A bar chart of one set of values, drawn as plain SVG: upright bars from
|
|
22
|
+
* a zero line, a label under each and its value over it. A bar is drawn
|
|
23
|
+
* the way `Progress` draws its fill, and with a `yMax` each bar stands in
|
|
24
|
+
* a track up to it, as a `Progress` does: so many of so many.
|
|
25
|
+
*
|
|
26
|
+
* It is a `figure`: the `label` is its caption, the drawing is hidden
|
|
27
|
+
* from assistive technology, and the same numbers are in a real table
|
|
28
|
+
* (off screen unless `showTable`). The bars are one colour, since the
|
|
29
|
+
* labels tell them apart; a datum may carry its own.
|
|
30
|
+
*/
|
|
31
|
+
type Props = Omit<HTMLAttributes<HTMLElement>, "class"> & {
|
|
32
|
+
/** The bars, in order: a `label`, a `value`, and a `color` of its own if it needs one. */
|
|
33
|
+
data: ChartDatum[];
|
|
34
|
+
/** The colour of the bars: a theme colour by name or any CSS colour. Without it, the first series colour. */
|
|
35
|
+
color?: ChartColor;
|
|
36
|
+
/** The top of the value axis, and the ceiling each bar is measured against: with it every bar stands in a track that reaches it. Without it, the highest value rounded up to a tick, and no tracks. */
|
|
37
|
+
yMax?: number;
|
|
38
|
+
/** How a value is written: `number` (default), `percent` (72 is "72 %"), or a function. */
|
|
39
|
+
yFormat?: ChartFormat;
|
|
40
|
+
/** Height of the drawing in px. The width is the container's. */
|
|
41
|
+
height?: number;
|
|
42
|
+
/** The caption, and what the chart is called. */
|
|
43
|
+
label: string;
|
|
44
|
+
/** Keeps the caption for assistive technology and takes it off the screen. */
|
|
45
|
+
labelHidden?: boolean;
|
|
46
|
+
/** Writes each value over its bar. */
|
|
47
|
+
showValues?: boolean;
|
|
48
|
+
/** Shows the data table under the chart; otherwise it is there for assistive technology only. */
|
|
49
|
+
showTable?: boolean;
|
|
50
|
+
/** The heading of the table's first column. */
|
|
51
|
+
categoryTitle?: string;
|
|
52
|
+
/** The heading of the table's value column. */
|
|
53
|
+
valueTitle?: string;
|
|
54
|
+
/** The language of the built-in number formats. Without it, the runtime's. */
|
|
55
|
+
locale?: string;
|
|
56
|
+
/** Stands the chart on a panel of the thick material (frosted glass), for a chart that floats over a wash, a tint or a picture. On a plain card it shows nothing: leave it off there. */
|
|
57
|
+
glass?: boolean;
|
|
58
|
+
class?: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
let {
|
|
62
|
+
data,
|
|
63
|
+
color,
|
|
64
|
+
yMax,
|
|
65
|
+
yFormat = "number",
|
|
66
|
+
height = 240,
|
|
67
|
+
label,
|
|
68
|
+
labelHidden = false,
|
|
69
|
+
showValues = true,
|
|
70
|
+
showTable = false,
|
|
71
|
+
categoryTitle = "Category",
|
|
72
|
+
valueTitle = "Value",
|
|
73
|
+
locale,
|
|
74
|
+
glass = false,
|
|
75
|
+
class: className = "",
|
|
76
|
+
...restProps
|
|
77
|
+
}: Props = $props();
|
|
78
|
+
|
|
79
|
+
/** Before it is measured (on the server) the drawing is laid out this wide and scaled to fit. */
|
|
80
|
+
const FALLBACK_WIDTH = 600;
|
|
81
|
+
/** The widest a bar gets: a bar is a `Progress` stood on end, a rod and not a block. */
|
|
82
|
+
const WIDEST = 24;
|
|
83
|
+
|
|
84
|
+
const uid = $props.id();
|
|
85
|
+
const clipId = `${uid}-bars`;
|
|
86
|
+
const veilId = `${uid}-veil`;
|
|
87
|
+
const shineId = `${uid}-shine`;
|
|
88
|
+
const streakId = `${uid}-streak`;
|
|
89
|
+
const softId = `${uid}-soft`;
|
|
90
|
+
|
|
91
|
+
let measured = $state(0);
|
|
92
|
+
let tickWidth = $state(0);
|
|
93
|
+
|
|
94
|
+
const width = $derived(measured > 0 ? measured : FALLBACK_WIDTH);
|
|
95
|
+
/** Chart text is set in rem; the room for it grows with the reader's text size. */
|
|
96
|
+
const rem = $derived(measured > 0 ? chartTextScale() : 1);
|
|
97
|
+
const plotHeight = $derived(Math.max(height, 80));
|
|
98
|
+
const top = $derived(showValues ? Math.round(24 * rem) : 8);
|
|
99
|
+
const bottom = $derived(Math.round(28 * rem));
|
|
100
|
+
const format = $derived(chartFormatter(yFormat, locale));
|
|
101
|
+
const isValue = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value);
|
|
102
|
+
const scale = $derived(chartScale(data.map((datum) => datum.value), { max: yMax, zero: true }));
|
|
103
|
+
const tickLabels = $derived(scale.ticks.map((tick) => format(tick)));
|
|
104
|
+
/** The value axis takes the width of its widest label as drawn; until that is known, an estimate. */
|
|
105
|
+
const left = $derived((tickWidth || Math.max(...tickLabels.map((text) => text.length), 1) * 7 * rem) + 12);
|
|
106
|
+
const innerWidth = $derived(Math.max(width - left, 10));
|
|
107
|
+
const innerHeight = $derived(Math.max(plotHeight - top - bottom, 10));
|
|
108
|
+
const band = $derived(innerWidth / Math.max(data.length, 1));
|
|
109
|
+
const barWidth = $derived(Math.max(1, Math.min(band * 0.5, WIDEST)));
|
|
110
|
+
/** A ceiling the app gave, and nothing below zero: each bar has a track up to it. */
|
|
111
|
+
const tracks = $derived(isValue(yMax) && scale.min >= 0 && scale.max > 0);
|
|
112
|
+
const every = $derived(labelEvery(data.map((datum) => datum.label), innerWidth, 7 * rem));
|
|
113
|
+
/** A value over every bar only while each has room for it. */
|
|
114
|
+
const valuesFit = $derived(showValues && band >= Math.max(...tickLabels.map((text) => text.length), 1) * 7 * rem);
|
|
115
|
+
/** Where the bars stand: zero, or the end of the axis nearest to it. */
|
|
116
|
+
const zero = $derived(Math.min(Math.max(0, scale.min), scale.max));
|
|
117
|
+
|
|
118
|
+
const y = (value: number) => top + innerHeight - ((value - scale.min) / (scale.max - scale.min)) * innerHeight;
|
|
119
|
+
|
|
120
|
+
const bars = $derived(
|
|
121
|
+
data.map((datum, index) => {
|
|
122
|
+
const value = isValue(datum.value) ? Math.min(Math.max(datum.value, scale.min), scale.max) : 0;
|
|
123
|
+
const from = y(zero);
|
|
124
|
+
const to = y(value);
|
|
125
|
+
return {
|
|
126
|
+
label: datum.label,
|
|
127
|
+
text: isValue(datum.value) ? format(datum.value) : "",
|
|
128
|
+
color: datum.color ? chartColor(datum.color, index) : chartColor(color, 0),
|
|
129
|
+
centre: left + band * (index + 0.5),
|
|
130
|
+
top: Math.min(from, to),
|
|
131
|
+
height: Math.abs(from - to),
|
|
132
|
+
path: barPath(left + band * (index + 0.5) - barWidth / 2, barWidth, from, to, barWidth / 2),
|
|
133
|
+
track: barPath(left + band * (index + 0.5) - barWidth / 2, barWidth, from, y(scale.max), barWidth / 2),
|
|
134
|
+
below: value < zero,
|
|
135
|
+
};
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
</script>
|
|
139
|
+
|
|
140
|
+
<figure class={cn("zabi-chart relative m-0 flex flex-col gap-3", glass && CHART_GLASS, className)} {...restProps}>
|
|
141
|
+
<figcaption class={cn("text-label text-sm font-medium", labelHidden && "sr-only")}>{label}</figcaption>
|
|
142
|
+
|
|
143
|
+
<div class="w-full" use:measureWidth={(value) => (measured = value)}>
|
|
144
|
+
<svg class="plot block w-full" viewBox="0 0 {width} {plotHeight}" height={plotHeight} aria-hidden="true" focusable="false">
|
|
145
|
+
<g use:widestText={{ onwidth: (value) => (tickWidth = value), key: [tickLabels, measured] }}>
|
|
146
|
+
{#each scale.ticks as tick, index (tick)}
|
|
147
|
+
{#if tick !== zero}
|
|
148
|
+
<line class="grid" x1={left} x2={left + innerWidth} y1={y(tick)} y2={y(tick)} />
|
|
149
|
+
{/if}
|
|
150
|
+
<text class="tick" x={left - 12} y={y(tick)} text-anchor="end" dominant-baseline="central">
|
|
151
|
+
{tickLabels[index]}
|
|
152
|
+
</text>
|
|
153
|
+
{/each}
|
|
154
|
+
</g>
|
|
155
|
+
|
|
156
|
+
{#each bars as bar, index (index)}
|
|
157
|
+
{#if tracks}<path class="track" d={bar.track} />{/if}
|
|
158
|
+
<path class="bar" style:--_c={bar.color} d={bar.path}>
|
|
159
|
+
<title>{bar.label}: {bar.text}</title>
|
|
160
|
+
</path>
|
|
161
|
+
{#if valuesFit && bar.text}
|
|
162
|
+
<text
|
|
163
|
+
class="value"
|
|
164
|
+
x={bar.centre}
|
|
165
|
+
y={bar.below ? bar.top + bar.height + 16 * rem : (tracks ? top : bar.top) - 8}
|
|
166
|
+
text-anchor="middle"
|
|
167
|
+
>
|
|
168
|
+
{bar.text}
|
|
169
|
+
</text>
|
|
170
|
+
{/if}
|
|
171
|
+
{#if index % every === 0}
|
|
172
|
+
<text class="label" x={bar.centre} y={plotHeight - bottom + 20 * rem} text-anchor="middle">{bar.label}</text>
|
|
173
|
+
{/if}
|
|
174
|
+
{/each}
|
|
175
|
+
|
|
176
|
+
<!--
|
|
177
|
+
The glass, as on a PieChart: the bars keep their colour, opaque, and over them
|
|
178
|
+
lie the veil of a solid control, a hairline of light on the round end of each
|
|
179
|
+
bar and a soft sheen down its near side. Decoration only; not drawn where the materials are opaque.
|
|
180
|
+
-->
|
|
181
|
+
<defs>
|
|
182
|
+
<clipPath id={clipId}>
|
|
183
|
+
{#each bars as bar, index (index)}<path d={bar.path} />{/each}
|
|
184
|
+
</clipPath>
|
|
185
|
+
<linearGradient id={veilId} x1="0" y1="0" x2="0" y2="1">
|
|
186
|
+
<stop class="veil-top" offset="0" />
|
|
187
|
+
<stop class="veil-bottom" offset="1" />
|
|
188
|
+
</linearGradient>
|
|
189
|
+
<linearGradient id={shineId} x1="0" y1="0" x2="0" y2="1">
|
|
190
|
+
<stop class="light" offset="0" />
|
|
191
|
+
<stop class="light off" offset="0.5" />
|
|
192
|
+
</linearGradient>
|
|
193
|
+
<linearGradient id={streakId} gradientUnits="userSpaceOnUse" x1="0" y1={top} x2="0" y2={top + innerHeight}>
|
|
194
|
+
<stop class="light" offset="0" />
|
|
195
|
+
<stop class="light off" offset="1" />
|
|
196
|
+
</linearGradient>
|
|
197
|
+
<filter id={softId} filterUnits="userSpaceOnUse" x="0" y="0" {width} height={plotHeight}>
|
|
198
|
+
<feGaussianBlur stdDeviation="1.2" />
|
|
199
|
+
</filter>
|
|
200
|
+
</defs>
|
|
201
|
+
<g class="glass" clip-path="url(#{clipId})">
|
|
202
|
+
{#each bars as bar, index (index)}
|
|
203
|
+
{#if bar.height > 0}
|
|
204
|
+
<path class="veil" d={bar.path} fill="url(#{veilId})" />
|
|
205
|
+
<path class="rim" d={bar.path} stroke="url(#{shineId})" />
|
|
206
|
+
{#if barWidth >= 12 && bar.height > barWidth}
|
|
207
|
+
<line
|
|
208
|
+
class="sheen"
|
|
209
|
+
filter="url(#{softId})"
|
|
210
|
+
x1={bar.centre - barWidth / 2 + 4.5}
|
|
211
|
+
x2={bar.centre - barWidth / 2 + 4.5}
|
|
212
|
+
y1={bar.top + barWidth / 2}
|
|
213
|
+
y2={bar.top + bar.height - barWidth / 2}
|
|
214
|
+
stroke="url(#{streakId})"
|
|
215
|
+
/>
|
|
216
|
+
{/if}
|
|
217
|
+
{/if}
|
|
218
|
+
{/each}
|
|
219
|
+
</g>
|
|
220
|
+
|
|
221
|
+
<!-- The zero line is drawn last, so every bar stands on it. -->
|
|
222
|
+
<line class="grid axis" x1={left} x2={left + innerWidth} y1={y(zero)} y2={y(zero)} />
|
|
223
|
+
</svg>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<!-- The library's own Table, so the numbers look like every other table in the app. -->
|
|
227
|
+
<Table class={showTable ? "" : "sr-only"}>
|
|
228
|
+
<thead class="border-border bg-surface-elevated border-b">
|
|
229
|
+
<tr>
|
|
230
|
+
<th class="text-headline px-4 py-3 text-start font-medium" scope="col">{categoryTitle}</th>
|
|
231
|
+
<th class="text-headline px-4 py-3 text-end font-medium" scope="col">{valueTitle}</th>
|
|
232
|
+
</tr>
|
|
233
|
+
</thead>
|
|
234
|
+
<tbody class="divide-border divide-y">
|
|
235
|
+
{#each bars as bar, index (index)}
|
|
236
|
+
<tr>
|
|
237
|
+
<th class="text-body px-4 py-3 text-start font-normal" scope="row">{bar.label}</th>
|
|
238
|
+
<td class="text-body px-4 py-3 text-end tabular-nums">{bar.text}</td>
|
|
239
|
+
</tr>
|
|
240
|
+
{/each}
|
|
241
|
+
</tbody>
|
|
242
|
+
</Table>
|
|
243
|
+
</figure>
|
|
244
|
+
|
|
245
|
+
<style>
|
|
246
|
+
/*
|
|
247
|
+
* The series colours an app may set, on the chart or on anything around
|
|
248
|
+
* it; read with a fallback (in util/chart.ts) and never declared here:
|
|
249
|
+
* --zabi-chart-1 … --zabi-chart-6
|
|
250
|
+
*/
|
|
251
|
+
.plot {
|
|
252
|
+
overflow: visible;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* The library's hairline, as between the rows of a list; the line the bars
|
|
256
|
+
stand on is the edge a Progress track has. */
|
|
257
|
+
.grid {
|
|
258
|
+
stroke: var(--color-border);
|
|
259
|
+
stroke-width: 1;
|
|
260
|
+
shape-rendering: crispEdges;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.grid.axis {
|
|
264
|
+
stroke: var(--color-border-strong);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* The track of a Progress, stood on end: same fill, same edge. */
|
|
268
|
+
.track {
|
|
269
|
+
fill: var(--color-progress-track);
|
|
270
|
+
stroke: var(--color-progress-track-border);
|
|
271
|
+
stroke-width: 1;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.tick,
|
|
275
|
+
.label {
|
|
276
|
+
fill: var(--color-caption);
|
|
277
|
+
font-size: 0.75rem;
|
|
278
|
+
font-variant-numeric: tabular-nums;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* What a bar is called reads a step firmer than the scale beside it. */
|
|
282
|
+
.label {
|
|
283
|
+
fill: var(--color-description);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.value {
|
|
287
|
+
fill: var(--color-headline);
|
|
288
|
+
font-size: 0.75rem;
|
|
289
|
+
font-weight: 600;
|
|
290
|
+
font-variant-numeric: tabular-nums;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.bar {
|
|
294
|
+
fill: var(--_c);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/*
|
|
298
|
+
* The bars are glass, drawn, in the same terms as the slices of a PieChart (which has the
|
|
299
|
+
* list): the veil of a solid control, one pixel of light on the round end fading down the
|
|
300
|
+
* sides, a soft sheen down the near side, and the contact shadow.
|
|
301
|
+
*/
|
|
302
|
+
.bar {
|
|
303
|
+
/* The contact shadow only: a wider one is a smudge under a rod this thin. */
|
|
304
|
+
filter: drop-shadow(0 1px 2px rgb(var(--shadow-color) / var(--shadow-opacity-contact)));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.glass {
|
|
308
|
+
pointer-events: none;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.veil-top {
|
|
312
|
+
stop-color: var(--color-control-gradient-accent-start);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.veil-bottom {
|
|
316
|
+
stop-color: var(--color-control-gradient-accent-end);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.light {
|
|
320
|
+
stop-color: color-mix(in srgb, var(--zabi-base-50) 30%, transparent);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.light.off {
|
|
324
|
+
stop-opacity: 0;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.rim {
|
|
328
|
+
fill: none;
|
|
329
|
+
stroke-width: 2px;
|
|
330
|
+
vector-effect: non-scaling-stroke;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.sheen {
|
|
334
|
+
stroke-width: 3;
|
|
335
|
+
stroke-linecap: round;
|
|
336
|
+
opacity: 0.45;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* Flat where the library's materials are opaque: the same switches, the same reasons. */
|
|
340
|
+
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more), (forced-colors: active) {
|
|
341
|
+
.glass {
|
|
342
|
+
display: none;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.bar {
|
|
346
|
+
filter: none;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
:global([data-materials="opaque"]) .glass {
|
|
351
|
+
display: none;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
:global([data-materials="opaque"]) .bar {
|
|
355
|
+
filter: none;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/* Forced colours: bars in the system's text colour; the labels tell them apart. */
|
|
359
|
+
@media (forced-colors: active) {
|
|
360
|
+
.bar,
|
|
361
|
+
.tick,
|
|
362
|
+
.label,
|
|
363
|
+
.value {
|
|
364
|
+
fill: CanvasText;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.grid,
|
|
368
|
+
.grid.axis {
|
|
369
|
+
stroke: GrayText;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.track {
|
|
373
|
+
fill: Canvas;
|
|
374
|
+
stroke: CanvasText;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.bar {
|
|
378
|
+
fill: Highlight;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
</style>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
2
|
+
import { type ChartColor, type ChartDatum, type ChartFormat } from "../util/chart.js";
|
|
3
|
+
/**
|
|
4
|
+
* A bar chart of one set of values, drawn as plain SVG: upright bars from
|
|
5
|
+
* a zero line, a label under each and its value over it. A bar is drawn
|
|
6
|
+
* the way `Progress` draws its fill, and with a `yMax` each bar stands in
|
|
7
|
+
* a track up to it, as a `Progress` does: so many of so many.
|
|
8
|
+
*
|
|
9
|
+
* It is a `figure`: the `label` is its caption, the drawing is hidden
|
|
10
|
+
* from assistive technology, and the same numbers are in a real table
|
|
11
|
+
* (off screen unless `showTable`). The bars are one colour, since the
|
|
12
|
+
* labels tell them apart; a datum may carry its own.
|
|
13
|
+
*/
|
|
14
|
+
type Props = Omit<HTMLAttributes<HTMLElement>, "class"> & {
|
|
15
|
+
/** The bars, in order: a `label`, a `value`, and a `color` of its own if it needs one. */
|
|
16
|
+
data: ChartDatum[];
|
|
17
|
+
/** The colour of the bars: a theme colour by name or any CSS colour. Without it, the first series colour. */
|
|
18
|
+
color?: ChartColor;
|
|
19
|
+
/** The top of the value axis, and the ceiling each bar is measured against: with it every bar stands in a track that reaches it. Without it, the highest value rounded up to a tick, and no tracks. */
|
|
20
|
+
yMax?: number;
|
|
21
|
+
/** How a value is written: `number` (default), `percent` (72 is "72 %"), or a function. */
|
|
22
|
+
yFormat?: ChartFormat;
|
|
23
|
+
/** Height of the drawing in px. The width is the container's. */
|
|
24
|
+
height?: number;
|
|
25
|
+
/** The caption, and what the chart is called. */
|
|
26
|
+
label: string;
|
|
27
|
+
/** Keeps the caption for assistive technology and takes it off the screen. */
|
|
28
|
+
labelHidden?: boolean;
|
|
29
|
+
/** Writes each value over its bar. */
|
|
30
|
+
showValues?: boolean;
|
|
31
|
+
/** Shows the data table under the chart; otherwise it is there for assistive technology only. */
|
|
32
|
+
showTable?: boolean;
|
|
33
|
+
/** The heading of the table's first column. */
|
|
34
|
+
categoryTitle?: string;
|
|
35
|
+
/** The heading of the table's value column. */
|
|
36
|
+
valueTitle?: string;
|
|
37
|
+
/** The language of the built-in number formats. Without it, the runtime's. */
|
|
38
|
+
locale?: string;
|
|
39
|
+
/** Stands the chart on a panel of the thick material (frosted glass), for a chart that floats over a wash, a tint or a picture. On a plain card it shows nothing: leave it off there. */
|
|
40
|
+
glass?: boolean;
|
|
41
|
+
class?: string;
|
|
42
|
+
};
|
|
43
|
+
declare const BarChart: import("svelte").Component<Props, {}, "">;
|
|
44
|
+
type BarChart = ReturnType<typeof BarChart>;
|
|
45
|
+
export default BarChart;
|
|
@@ -59,7 +59,10 @@
|
|
|
59
59
|
* the lowest, closed. It is also a button that moves the sheet a step, so
|
|
60
60
|
* the same is there for a keyboard and a screen reader. A swipe on the
|
|
61
61
|
* content scrolls the content; it moves the sheet only when the content
|
|
62
|
-
* is at its top and the swipe goes down.
|
|
62
|
+
* is at its top and the swipe goes down. A box inside the content that
|
|
63
|
+
* scrolls by itself (a long list, a text area) keeps every swipe that
|
|
64
|
+
* starts on it, at its top too, and so does anything carrying
|
|
65
|
+
* `data-sheet-no-drag` (a map, a wheel, a swipeable row) or a range input.
|
|
63
66
|
*
|
|
64
67
|
* From the `md` breakpoint up the sheet is still attached to the bottom
|
|
65
68
|
* edge, centred and no wider than 40rem.
|
|
@@ -87,6 +90,19 @@
|
|
|
87
90
|
* Set it when you open the sheet to start somewhere else.
|
|
88
91
|
*/
|
|
89
92
|
snap?: BottomSheetSnap;
|
|
93
|
+
/**
|
|
94
|
+
* The sheet is as tall as its content, up to half the screen, instead
|
|
95
|
+
* of half the screen whatever it holds. It has one resting height, so
|
|
96
|
+
* `snapPoints` and `snap` are ignored and the grip only drags. Off by
|
|
97
|
+
* default.
|
|
98
|
+
*/
|
|
99
|
+
fit?: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* With `fit`: the tallest the sheet grows before its content scrolls.
|
|
102
|
+
* `half` (the default) is half the screen; `full` is down from the
|
|
103
|
+
* status bar, like the `full` snap point. Ignored without `fit`.
|
|
104
|
+
*/
|
|
105
|
+
fitMax?: "half" | "full";
|
|
90
106
|
/**
|
|
91
107
|
* Render the overlay in `document.body`, so an ancestor with a
|
|
92
108
|
* transform, filter or clipped overflow cannot trap it; pass `false`
|
|
@@ -133,6 +149,8 @@
|
|
|
133
149
|
description = "",
|
|
134
150
|
snapPoints,
|
|
135
151
|
snap = $bindable(),
|
|
152
|
+
fit = false,
|
|
153
|
+
fitMax = "half",
|
|
136
154
|
portal = true,
|
|
137
155
|
dismissible = true,
|
|
138
156
|
onclose,
|
|
@@ -164,7 +182,8 @@
|
|
|
164
182
|
applyDefaults();
|
|
165
183
|
$effect.pre(applyDefaults);
|
|
166
184
|
|
|
167
|
-
|
|
185
|
+
// Used only where --duration-slow cannot be read.
|
|
186
|
+
const SLIDE_MS = 300;
|
|
168
187
|
|
|
169
188
|
const titleId = generateId("bottom-sheet-title");
|
|
170
189
|
const descriptionId = generateId("bottom-sheet-description");
|
|
@@ -188,7 +207,7 @@
|
|
|
188
207
|
/** Position among the open overlays; lifts a sheet opened later above the earlier ones. */
|
|
189
208
|
let depth = $state(0);
|
|
190
209
|
|
|
191
|
-
const points = $derived(normaliseSnapPoints(snapPoints));
|
|
210
|
+
const points = $derived(fit ? normaliseSnapPoints(["half"]) : normaliseSnapPoints(snapPoints));
|
|
192
211
|
/** The snap point in effect: the one asked for if it is in use, else the lowest. */
|
|
193
212
|
const currentSnap = $derived(snap && points.includes(snap) ? snap : points[0]);
|
|
194
213
|
const stepTo = $derived(nextSnap(points, currentSnap));
|
|
@@ -199,14 +218,21 @@
|
|
|
199
218
|
* that a short screen leaves no room for content, and never above full.
|
|
200
219
|
*/
|
|
201
220
|
const FULL = "calc(100% - env(safe-area-inset-top, 0px))";
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
);
|
|
221
|
+
const HALF = `min(max(50%, 18rem), ${FULL})`;
|
|
222
|
+
const snapHeight = $derived(currentSnap === "full" ? FULL : HALF);
|
|
205
223
|
|
|
206
224
|
/** While a drag is on: the height and offset it has moved the panel to, in px. */
|
|
207
225
|
let dragging = $state(false);
|
|
208
226
|
let dragHeight = $state<number | null>(null);
|
|
209
227
|
let dragOffset = $state(0);
|
|
228
|
+
/**
|
|
229
|
+
* False from opening until the slide-in has ended. The panel's height
|
|
230
|
+
* transition is for snapping and settling a drag; while the sheet arrives
|
|
231
|
+
* a late change of height (the overlay shrinking over a keyboard, a snap
|
|
232
|
+
* set as it opens) would ride the spring and move the content after the
|
|
233
|
+
* eye has landed on it. Until then the height is applied at once.
|
|
234
|
+
*/
|
|
235
|
+
let arrived = $state(false);
|
|
210
236
|
|
|
211
237
|
function close(reason: BottomSheetCloseReason) {
|
|
212
238
|
if (!dismissible) return;
|
|
@@ -218,18 +244,42 @@
|
|
|
218
244
|
onclose?.({ reason });
|
|
219
245
|
}
|
|
220
246
|
|
|
221
|
-
/**
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
247
|
+
/**
|
|
248
|
+
* Slides the panel up from the bottom edge. Skipped where motion is unwanted or unsupported.
|
|
249
|
+
* Returns how long the slide takes in ms: 0 when there is none.
|
|
250
|
+
*/
|
|
251
|
+
function slideIn(container: HTMLElement): number {
|
|
252
|
+
if (typeof container.animate !== "function" || prefersReducedMotion()) return 0;
|
|
253
|
+
// Arriving is a smooth slide to rest on the standard curve, with no
|
|
254
|
+
// overshoot (the spring is for snapping and settling a drag). Read
|
|
255
|
+
// from the element so a theme that overrides the tokens is followed.
|
|
256
|
+
// Where the tokens cannot be read (no stylesheet) or the engine
|
|
257
|
+
// rejects the easing, plain ease-out.
|
|
258
|
+
const style = getComputedStyle(container);
|
|
259
|
+
const duration = parseDuration(style.getPropertyValue("--duration-slow"));
|
|
260
|
+
const standard = style.getPropertyValue("--ease-standard").trim();
|
|
261
|
+
const frames = [{ transform: "translateY(100%)" }, { transform: "translateY(0)" }];
|
|
262
|
+
try {
|
|
263
|
+
container.animate(frames, { duration, easing: standard || "ease-out" });
|
|
264
|
+
} catch {
|
|
265
|
+
container.animate(frames, { duration, easing: "ease-out" });
|
|
266
|
+
}
|
|
267
|
+
return duration;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** `300ms`, `0.3s` or `0s` to milliseconds; the fallback when it is not a time. */
|
|
271
|
+
function parseDuration(value: string): number {
|
|
272
|
+
const match = /^\s*(-?\d*\.?\d+)(ms|s)\s*$/.exec(value);
|
|
273
|
+
if (!match) return SLIDE_MS;
|
|
274
|
+
const ms = parseFloat(match[1]) * (match[2] === "s" ? 1000 : 1);
|
|
275
|
+
return ms >= 0 ? ms : SLIDE_MS;
|
|
228
276
|
}
|
|
229
277
|
|
|
230
278
|
/** The snap points in px, lowest first, for the screen as it is now. */
|
|
231
279
|
function measureSnapHeights(): number[] {
|
|
232
280
|
if (!root) return [];
|
|
281
|
+
// A fitted sheet rests at the height its content gives it.
|
|
282
|
+
if (fit && panel) return [panel.getBoundingClientRect().height];
|
|
233
283
|
const available = root.clientHeight - parseFloat(getComputedStyle(root).paddingTop || "0");
|
|
234
284
|
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
235
285
|
const half = Math.min(Math.max(root.clientHeight * 0.5, 18 * rem), available);
|
|
@@ -280,7 +330,8 @@
|
|
|
280
330
|
const unlockScroll = lockBodyScroll();
|
|
281
331
|
const overlay = joinOverlayStack(container);
|
|
282
332
|
depth = overlay.depth;
|
|
283
|
-
|
|
333
|
+
arrived = false;
|
|
334
|
+
const arrival = setTimeout(() => (arrived = true), slideIn(container));
|
|
284
335
|
const t = setTimeout(() => {
|
|
285
336
|
// Something inside has taken focus already (a field that
|
|
286
337
|
// focuses itself as it mounts): leave it there. `initialFocus`
|
|
@@ -297,6 +348,8 @@
|
|
|
297
348
|
});
|
|
298
349
|
return () => {
|
|
299
350
|
clearTimeout(t);
|
|
351
|
+
clearTimeout(arrival);
|
|
352
|
+
arrived = false;
|
|
300
353
|
stopRecovery();
|
|
301
354
|
overlay.leave();
|
|
302
355
|
unlockScroll();
|
|
@@ -415,9 +468,14 @@ a background is dropped there, and the grip would be gone. -->
|
|
|
415
468
|
|
|
416
469
|
{#if isOpen}
|
|
417
470
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
471
|
+
<!-- `overflow-clip`, not `hidden`: a clipped box is a scroll container
|
|
472
|
+
nobody can scroll. The panel waits below the screen while it slides in, so
|
|
473
|
+
the root has overflow to scroll, and WebKit scrolls it to bring the
|
|
474
|
+
focused grip into view: the sheet rose to the top of the screen, and sat
|
|
475
|
+
32px high (the filler under the panel) once the slide had ended. -->
|
|
418
476
|
<div
|
|
419
477
|
bind:this={root}
|
|
420
|
-
class="group/overlay fixed inset-0 z-modal overflow-
|
|
478
|
+
class="group/overlay fixed inset-0 z-modal overflow-clip pt-[env(safe-area-inset-top)] {dismissible
|
|
421
479
|
? 'cursor-pointer'
|
|
422
480
|
: 'cursor-default'} bg-overlay"
|
|
423
481
|
style:z-index={depth > 0 ? `calc(var(--z-modal) + ${depth})` : undefined}
|
|
@@ -430,20 +488,30 @@ a background is dropped there, and the grip would be gone. -->
|
|
|
430
488
|
<div
|
|
431
489
|
bind:this={panel}
|
|
432
490
|
class={cn(
|
|
433
|
-
|
|
491
|
+
// The glass is a layer behind the content (`::before`), not a filter on
|
|
492
|
+
// the panel: a backdrop-filter would make the panel the containing
|
|
493
|
+
// block of every fixed-position popover inside it.
|
|
494
|
+
"material-layer-thick absolute inset-x-0 bottom-0 mx-auto flex w-full cursor-default flex-col rounded-t-overlay",
|
|
434
495
|
"max-h-[calc(100dvh-env(safe-area-inset-top,0px))] pb-[env(safe-area-inset-bottom)]",
|
|
435
496
|
// Over the keyboard: no taller than what is left, and the home
|
|
436
497
|
// indicator it would clear is under the keyboard.
|
|
437
498
|
"group-data-keyboard-open/overlay:max-h-[calc(100%_-_env(safe-area-inset-top,0px))] group-data-keyboard-open/overlay:pb-0",
|
|
438
499
|
// Clear of the notch and the rounded corners in landscape.
|
|
439
500
|
"pl-[env(safe-area-inset-left)] pr-[env(safe-area-inset-right)]",
|
|
440
|
-
|
|
441
|
-
|
|
501
|
+
// Snapping and settling spring; the overshoot may lift the panel a
|
|
502
|
+
// few px, so the filler continues its fill under the screen edge.
|
|
503
|
+
// The panel's own background is transparent now (the fill is on its
|
|
504
|
+
// layer), so the filler paints the material token itself: it follows
|
|
505
|
+
// the same fallbacks.
|
|
506
|
+
"transition-[height,transform] duration-(--duration-slow) ease-spring motion-reduce:transition-none",
|
|
507
|
+
"after:pointer-events-none after:absolute after:inset-x-0 after:top-full after:h-8 after:content-[''] after:bg-(--color-material-thick)",
|
|
508
|
+
"md:w-[min(100%,40rem)]",
|
|
442
509
|
className,
|
|
443
510
|
)}
|
|
444
|
-
style:height={dragHeight !== null ? `${dragHeight}px` : snapHeight}
|
|
511
|
+
style:height={dragHeight !== null ? `${dragHeight}px` : fit ? "auto" : snapHeight}
|
|
512
|
+
style:max-height={fit ? (fitMax === "full" ? FULL : HALF) : undefined}
|
|
445
513
|
style:transform={dragOffset > 0 ? `translateY(${dragOffset}px)` : undefined}
|
|
446
|
-
style:transition={dragging ? "none" : undefined}
|
|
514
|
+
style:transition={dragging || !arrived ? "none" : undefined}
|
|
447
515
|
role="dialog"
|
|
448
516
|
aria-modal="true"
|
|
449
517
|
aria-labelledby={titleId}
|
|
@@ -23,7 +23,10 @@ import { type BottomSheetCloseReason, type BottomSheetSnap } from "../util/botto
|
|
|
23
23
|
* the lowest, closed. It is also a button that moves the sheet a step, so
|
|
24
24
|
* the same is there for a keyboard and a screen reader. A swipe on the
|
|
25
25
|
* content scrolls the content; it moves the sheet only when the content
|
|
26
|
-
* is at its top and the swipe goes down.
|
|
26
|
+
* is at its top and the swipe goes down. A box inside the content that
|
|
27
|
+
* scrolls by itself (a long list, a text area) keeps every swipe that
|
|
28
|
+
* starts on it, at its top too, and so does anything carrying
|
|
29
|
+
* `data-sheet-no-drag` (a map, a wheel, a swipeable row) or a range input.
|
|
27
30
|
*
|
|
28
31
|
* From the `md` breakpoint up the sheet is still attached to the bottom
|
|
29
32
|
* edge, centred and no wider than 40rem.
|
|
@@ -47,6 +50,19 @@ type Props = Omit<HTMLAttributes<HTMLDivElement>, "class" | "title" | "onclose"
|
|
|
47
50
|
* Set it when you open the sheet to start somewhere else.
|
|
48
51
|
*/
|
|
49
52
|
snap?: BottomSheetSnap;
|
|
53
|
+
/**
|
|
54
|
+
* The sheet is as tall as its content, up to half the screen, instead
|
|
55
|
+
* of half the screen whatever it holds. It has one resting height, so
|
|
56
|
+
* `snapPoints` and `snap` are ignored and the grip only drags. Off by
|
|
57
|
+
* default.
|
|
58
|
+
*/
|
|
59
|
+
fit?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* With `fit`: the tallest the sheet grows before its content scrolls.
|
|
62
|
+
* `half` (the default) is half the screen; `full` is down from the
|
|
63
|
+
* status bar, like the `full` snap point. Ignored without `fit`.
|
|
64
|
+
*/
|
|
65
|
+
fitMax?: "half" | "full";
|
|
50
66
|
/**
|
|
51
67
|
* Render the overlay in `document.body`, so an ancestor with a
|
|
52
68
|
* transform, filter or clipped overflow cannot trap it; pass `false`
|