@passui/react 0.1.45 → 0.1.47
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/README.md +57 -46
- package/dist/types/src/components/accordion/accordion.d.ts +1 -1
- package/dist/types/src/components/breadcrumb/breadcrumb.d.ts +1 -1
- package/dist/types/src/components/inline-disclosure/inline-disclosure.d.ts +1 -1
- package/dist/types/src/components/popover/anchored-surface.d.ts +5 -1
- package/dist/types/src/components/popover/index.d.ts +41 -0
- package/dist/types/src/components/popover/popover-content.d.ts +38 -0
- package/dist/types/src/components/popover/popover-root.d.ts +22 -0
- package/dist/types/src/components/popover/popover-trigger.d.ts +23 -0
- package/dist/types/src/components/stepper/rolling-digit.d.ts +1 -1
- package/dist/types/src/components/table/table.d.ts +1 -1
- package/dist/types/src/components/view-controller/view-controller.d.ts +2 -2
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/machines/popover.machine.d.ts +30 -0
- package/package.json +307 -307
- package/src/appearance/index.ts +2 -2
- package/src/appearance/platform.tsx +76 -60
- package/src/appearance/theme.tsx +409 -377
- package/src/components/accordion/accordion.tsx +58 -49
- package/src/components/ambient/ambient.tsx +25 -25
- package/src/components/ambient/index.ts +1 -1
- package/src/components/aspect-ratio/aspect-ratio.tsx +21 -16
- package/src/components/breadcrumb/breadcrumb.tsx +63 -57
- package/src/components/button/button-styles.ts +39 -32
- package/src/components/button/button.tsx +91 -84
- package/src/components/button/icon-button.tsx +27 -25
- package/src/components/button/pending-spinner.tsx +8 -8
- package/src/components/button/use-pending-indicator.ts +11 -11
- package/src/components/chip/chip.tsx +35 -30
- package/src/components/code-block/code-block.tsx +40 -39
- package/src/components/combo-box/combo-box.tsx +5 -5
- package/src/components/command/command.tsx +124 -94
- package/src/components/compact-date-picker/calendar-month.ts +75 -53
- package/src/components/compact-date-picker/compact-date-picker.tsx +308 -289
- package/src/components/compact-time-picker/compact-time-picker.tsx +288 -281
- package/src/components/compact-time-picker/time-value.ts +32 -29
- package/src/components/content-unavailable-view/content-unavailable-view.tsx +64 -48
- package/src/components/content-view/content-view.tsx +168 -159
- package/src/components/content-view/index.ts +1 -1
- package/src/components/context-menu/conceal-menu-source.ts +25 -25
- package/src/components/context-menu/context-menu-host.tsx +439 -402
- package/src/components/context-menu/context-menu-store.ts +125 -123
- package/src/components/context-menu/menu-navigation.ts +25 -18
- package/src/components/context-menu/menu-position.ts +33 -29
- package/src/components/context-menu/menu-reveal.ts +32 -32
- package/src/components/context-menu/use-context-menu.ts +63 -56
- package/src/components/conversation/conversation.tsx +265 -263
- package/src/components/conversation/index.ts +1 -1
- package/src/components/date-picker/date-picker.tsx +95 -94
- package/src/components/date-picker/date-value.ts +20 -10
- package/src/components/dialog/dialog-host.tsx +105 -103
- package/src/components/dialog/dialog-store.ts +37 -36
- package/src/components/dialog/dialog.tsx +192 -179
- package/src/components/dialog/index.ts +1 -1
- package/src/components/disclosure-group/disclosure-group.tsx +93 -86
- package/src/components/divider/divider.tsx +24 -22
- package/src/components/inline-disclosure/inline-disclosure.tsx +40 -36
- package/src/components/keyboard-shortcut/keyboard-shortcut.tsx +23 -19
- package/src/components/labeled-content/labeled-content.tsx +68 -68
- package/src/components/locale-switcher/index.ts +1 -1
- package/src/components/locale-switcher/locale-switcher.tsx +278 -251
- package/src/components/menu/menu.tsx +75 -58
- package/src/components/morph/index.ts +2 -2
- package/src/components/morph/morph.tsx +227 -201
- package/src/components/morph/use-morph-disclosure.ts +19 -19
- package/src/components/one-time-code/one-time-code.tsx +216 -200
- package/src/components/picker/picker-trigger.ts +16 -13
- package/src/components/picker/picker.tsx +371 -345
- package/src/components/popover/anchored-surface.tsx +253 -229
- package/src/components/popover/index.ts +50 -0
- package/src/components/popover/popover-content.tsx +374 -0
- package/src/components/popover/popover-root.tsx +97 -0
- package/src/components/popover/popover-trigger.tsx +71 -0
- package/src/components/profile-image/profile-image.tsx +80 -64
- package/src/components/progress-view/progress-view.tsx +61 -59
- package/src/components/progress-view/use-liquid-progress.ts +38 -38
- package/src/components/radio-group/radio-group.tsx +71 -68
- package/src/components/search-field/expanding-search-field.tsx +191 -177
- package/src/components/search-field/search-field.tsx +59 -50
- package/src/components/search-list/search-list.tsx +246 -236
- package/src/components/section/section.tsx +58 -58
- package/src/components/secure-field/secure-field.tsx +3 -3
- package/src/components/segmented-control/segmented-control.tsx +136 -136
- package/src/components/sheet/modal-environment.ts +71 -64
- package/src/components/sheet/sheet-portal.tsx +58 -53
- package/src/components/sheet/sheet.tsx +146 -141
- package/src/components/sheet/use-sheet-focus.ts +97 -82
- package/src/components/sidebar/sidebar.tsx +78 -78
- package/src/components/skeleton/skeleton.tsx +30 -21
- package/src/components/slider/slider.tsx +113 -104
- package/src/components/slosh-slider/liquid-step.ts +32 -31
- package/src/components/slosh-slider/slosh-slider.tsx +265 -228
- package/src/components/stat-tile/index.ts +1 -1
- package/src/components/stat-tile/stat-tile.tsx +79 -68
- package/src/components/status-indicator/status-indicator.tsx +40 -37
- package/src/components/stepper/rolling-digit.tsx +82 -68
- package/src/components/stepper/stepper.tsx +122 -113
- package/src/components/swatch-picker/swatch-picker.tsx +120 -120
- package/src/components/tab-bar/tab-bar.tsx +149 -148
- package/src/components/tab-view/tab-view.tsx +140 -137
- package/src/components/table/table.tsx +117 -98
- package/src/components/text-editor/text-editor.tsx +34 -29
- package/src/components/text-field/text-field.tsx +182 -180
- package/src/components/toast/toast-store.ts +127 -115
- package/src/components/toast/toaster.tsx +361 -347
- package/src/components/toggle/toggle.tsx +297 -282
- package/src/components/tooltip/tooltip-host.tsx +157 -150
- package/src/components/view-controller/index.ts +1 -1
- package/src/components/view-controller/view-controller.tsx +64 -53
- package/src/components/wake-slider/wake-slider.tsx +220 -205
- package/src/hooks/scroll/minimize-decision.ts +11 -7
- package/src/hooks/use-anchored-disclosure.ts +19 -19
- package/src/hooks/use-anchored-placement.ts +29 -25
- package/src/hooks/use-dismiss.ts +33 -29
- package/src/hooks/use-floating-dismiss.ts +24 -22
- package/src/hooks/use-media-query.ts +67 -60
- package/src/hooks/use-minimize-on-scroll.ts +69 -60
- package/src/hooks/use-visual-viewport.ts +63 -63
- package/src/hooks/viewport/types.ts +16 -16
- package/src/hooks/viewport/viewport-metrics.ts +16 -10
- package/src/icons/icons.tsx +440 -328
- package/src/index.ts +91 -91
- package/src/lib/anchored-placement.ts +70 -65
- package/src/lib/class-names.ts +14 -13
- package/src/lib/clipboard.ts +8 -7
- package/src/lib/date.ts +61 -60
- package/src/lib/floating-layers.ts +39 -24
- package/src/lib/haptics.ts +10 -10
- package/src/lib/language.ts +9 -9
- package/src/lib/range-value.ts +27 -21
- package/src/lib/screen-edges.ts +33 -30
- package/src/lib/shapes.ts +38 -34
- package/src/machines/popover.machine.ts +51 -0
- package/src/motion/transitions.ts +46 -43
- package/src/styles/code-block.css +69 -43
- package/src/styles/motion-controls.css +393 -191
- package/src/tokens.ts +1 -1
- package/dist/types/src/components/popover/popover-fit.d.ts +0 -21
- package/dist/types/src/components/popover/popover.d.ts +0 -21
- package/src/components/popover/popover-fit.ts +0 -69
- package/src/components/popover/popover.tsx +0 -334
|
@@ -1,105 +1,106 @@
|
|
|
1
|
-
import { useState } from "react"
|
|
2
|
-
import { TextField, type TextFieldSize } from "../text-field/text-field"
|
|
3
|
-
import { cn } from "../../lib/class-names"
|
|
4
|
-
import { dateValue } from "./date-value"
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { TextField, type TextFieldSize } from "../text-field/text-field";
|
|
3
|
+
import { cn } from "../../lib/class-names";
|
|
4
|
+
import { dateValue } from "./date-value";
|
|
5
5
|
|
|
6
6
|
const legendText: Record<TextFieldSize, string> = {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
7
|
+
xs: "mb-2 text-caption font-medium",
|
|
8
|
+
sm: "mb-2 text-footnote font-medium",
|
|
9
|
+
md: "mb-3 text-subheadline font-medium",
|
|
10
|
+
lg: "mb-3 text-body font-medium",
|
|
11
|
+
};
|
|
12
12
|
const fieldsetGap: Record<TextFieldSize, string> = {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
13
|
+
xs: "gap-2",
|
|
14
|
+
sm: "gap-2",
|
|
15
|
+
md: "gap-3",
|
|
16
|
+
lg: "gap-4",
|
|
17
|
+
};
|
|
18
18
|
const errorText: Record<TextFieldSize, string> = {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
19
|
+
xs: "text-caption2 text-danger",
|
|
20
|
+
sm: "text-caption text-danger",
|
|
21
|
+
md: "text-footnote text-danger",
|
|
22
|
+
lg: "text-subheadline text-danger",
|
|
23
|
+
};
|
|
24
24
|
|
|
25
25
|
type DatePickerProps = {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
26
|
+
value: string;
|
|
27
|
+
onChange: (value: string) => void;
|
|
28
|
+
label: string;
|
|
29
|
+
labels: {
|
|
30
|
+
day: string;
|
|
31
|
+
month: string;
|
|
32
|
+
year: string;
|
|
33
|
+
invalid: string;
|
|
34
|
+
};
|
|
35
|
+
locale: string;
|
|
36
|
+
max?: string;
|
|
37
|
+
/** Field scale passed to each TextField; matches TextField's xs|sm|md|lg. @default "md" */
|
|
38
|
+
size?: TextFieldSize;
|
|
39
|
+
};
|
|
40
40
|
|
|
41
41
|
/** Segmented date entry suited to birthdays, without a native calendar dialog. */
|
|
42
42
|
export function DatePicker({
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
value,
|
|
44
|
+
onChange,
|
|
45
|
+
label,
|
|
46
|
+
labels,
|
|
47
|
+
locale,
|
|
48
|
+
max,
|
|
49
|
+
size = "md",
|
|
50
50
|
}: DatePickerProps) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
51
|
+
const [year, month, day] = value.split("-");
|
|
52
|
+
const [parts, setParts] = useState({
|
|
53
|
+
year: year ?? "",
|
|
54
|
+
month: month ?? "",
|
|
55
|
+
day: day ?? "",
|
|
56
|
+
});
|
|
57
|
+
const complete = parts.year.length === 4 && Boolean(parts.month && parts.day);
|
|
58
|
+
const invalid =
|
|
59
|
+
complete && !dateValue(parts.year, parts.month, parts.day, max);
|
|
60
|
+
const order = new Intl.DateTimeFormat(locale, {
|
|
61
|
+
year: "numeric",
|
|
62
|
+
month: "2-digit",
|
|
63
|
+
day: "2-digit",
|
|
64
|
+
timeZone: "UTC",
|
|
65
|
+
})
|
|
66
|
+
.formatToParts(new Date("2000-01-02T00:00:00Z"))
|
|
67
|
+
.filter(
|
|
68
|
+
(
|
|
69
|
+
part,
|
|
70
|
+
): part is Intl.DateTimeFormatPart & {
|
|
71
|
+
type: "year" | "month" | "day";
|
|
72
|
+
} => part.type === "year" || part.type === "month" || part.type === "day",
|
|
73
|
+
);
|
|
74
|
+
return (
|
|
75
|
+
<fieldset className={cn("flex min-w-0 flex-col", fieldsetGap[size])}>
|
|
76
|
+
<legend className={legendText[size]}>{label}</legend>
|
|
77
|
+
<div className={cn("grid grid-cols-3", fieldsetGap[size])}>
|
|
78
|
+
{order.map(({ type }) => (
|
|
79
|
+
<TextField
|
|
80
|
+
key={type}
|
|
81
|
+
label={labels[type]}
|
|
82
|
+
inputMode="numeric"
|
|
83
|
+
autoComplete={`bday-${type}`}
|
|
84
|
+
maxLength={type === "year" ? 4 : 2}
|
|
85
|
+
value={parts[type]}
|
|
86
|
+
size={size}
|
|
87
|
+
aria-invalid={invalid || undefined}
|
|
88
|
+
onChange={(event) => {
|
|
89
|
+
const next = {
|
|
90
|
+
...parts,
|
|
91
|
+
[type]: event.target.value.replace(/\D/g, ""),
|
|
92
|
+
};
|
|
93
|
+
setParts(next);
|
|
94
|
+
onChange(dateValue(next.year, next.month, next.day, max));
|
|
95
|
+
}}
|
|
96
|
+
/>
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
{invalid && (
|
|
100
|
+
<p role="status" className={errorText[size]}>
|
|
101
|
+
{labels.invalid}
|
|
102
|
+
</p>
|
|
103
|
+
)}
|
|
104
|
+
</fieldset>
|
|
105
|
+
);
|
|
105
106
|
}
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
/** Returns a date-only value without timezone conversion or date rollover. */
|
|
2
|
-
export function dateValue(
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
export function dateValue(
|
|
3
|
+
year: string,
|
|
4
|
+
month: string,
|
|
5
|
+
day: string,
|
|
6
|
+
max?: string,
|
|
7
|
+
) {
|
|
8
|
+
if (
|
|
9
|
+
!/^\d{4}$/.test(year) ||
|
|
10
|
+
!/^\d{1,2}$/.test(month) ||
|
|
11
|
+
!/^\d{1,2}$/.test(day)
|
|
12
|
+
)
|
|
13
|
+
return "";
|
|
14
|
+
const y = Number(year);
|
|
15
|
+
const m = Number(month);
|
|
16
|
+
const d = Number(day);
|
|
17
|
+
const leap = y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0);
|
|
18
|
+
const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
19
|
+
if (y < 1 || m < 1 || m > 12 || d < 1 || d > days[m - 1]) return "";
|
|
20
|
+
const value = `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")}`;
|
|
21
|
+
return max && value > max ? "" : value;
|
|
12
22
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { AnimatePresence, motion, useReducedMotion } from "motion/react"
|
|
2
|
-
import { useEffect, useRef, useSyncExternalStore } from "react"
|
|
3
|
-
import { Button } from "../button/button"
|
|
4
|
-
import { spring } from "../../motion/transitions"
|
|
1
|
+
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
|
2
|
+
import { useEffect, useRef, useSyncExternalStore } from "react";
|
|
3
|
+
import { Button } from "../button/button";
|
|
4
|
+
import { spring } from "../../motion/transitions";
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "./dialog-store"
|
|
6
|
+
advanceDialogQueue,
|
|
7
|
+
getDialog,
|
|
8
|
+
type PendingDialog,
|
|
9
|
+
settleDialog,
|
|
10
|
+
subscribeDialogs,
|
|
11
|
+
} from "./dialog-store";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Renders confirmation dialogs: a glass card that springs up over blurred
|
|
@@ -16,102 +16,104 @@ import {
|
|
|
16
16
|
* action is destructive, where the safe choice has focus.
|
|
17
17
|
*/
|
|
18
18
|
export function DialogHost() {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
const dialog = useSyncExternalStore(subscribeDialogs, getDialog, () => null);
|
|
20
|
+
return (
|
|
21
|
+
<AnimatePresence mode="wait" onExitComplete={advanceDialogQueue}>
|
|
22
|
+
{dialog && <ConfirmCard key={dialog.id} dialog={dialog} />}
|
|
23
|
+
</AnimatePresence>
|
|
24
|
+
);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function ConfirmCard({ dialog }: { dialog: PendingDialog }) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
const reducedMotion = useReducedMotion();
|
|
29
|
+
const confirmRef = useRef<HTMLButtonElement>(null);
|
|
30
|
+
const cancelRef = useRef<HTMLButtonElement>(null);
|
|
31
|
+
const titleId = `dialog-title-${dialog.id}`;
|
|
32
|
+
const messageId = `dialog-message-${dialog.id}`;
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const previous = window.document.activeElement as HTMLElement | null;
|
|
36
|
+
(dialog.destructive ? cancelRef : confirmRef).current?.focus();
|
|
37
|
+
const onKey = (event: KeyboardEvent) => {
|
|
38
|
+
if (event.key === "Escape") {
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
settleDialog(false, dialog.id);
|
|
41
|
+
}
|
|
42
|
+
// Keep focus inside the dialog.
|
|
43
|
+
if (event.key === "Tab") {
|
|
44
|
+
const order = [cancelRef.current, confirmRef.current].filter(
|
|
45
|
+
Boolean,
|
|
46
|
+
) as HTMLElement[];
|
|
47
|
+
const at = order.indexOf(window.document.activeElement as HTMLElement);
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
order[
|
|
50
|
+
(at + (event.shiftKey ? -1 : 1) + order.length) % order.length
|
|
51
|
+
]?.focus();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
window.addEventListener("keydown", onKey);
|
|
55
|
+
return () => {
|
|
56
|
+
window.removeEventListener("keydown", onKey);
|
|
57
|
+
previous?.focus?.();
|
|
58
|
+
};
|
|
59
|
+
}, [dialog.destructive, dialog.id]);
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
61
|
+
return (
|
|
62
|
+
<div className="fixed inset-0 z-[70] flex items-center justify-center p-5">
|
|
63
|
+
<motion.div
|
|
64
|
+
aria-hidden="true"
|
|
65
|
+
className="absolute inset-0 bg-black/25 backdrop-blur-md"
|
|
66
|
+
initial={{ opacity: 0 }}
|
|
67
|
+
animate={{ opacity: 1 }}
|
|
68
|
+
exit={{ opacity: 0 }}
|
|
69
|
+
transition={reducedMotion ? { duration: 0 } : spring.smooth}
|
|
70
|
+
onClick={() => settleDialog(false, dialog.id)}
|
|
71
|
+
/>
|
|
72
|
+
<motion.div
|
|
73
|
+
role="alertdialog"
|
|
74
|
+
aria-modal="true"
|
|
75
|
+
aria-labelledby={titleId}
|
|
76
|
+
aria-describedby={dialog.message ? messageId : undefined}
|
|
77
|
+
className="glass-menu rounded-5xl relative flex w-full max-w-[340px] flex-col gap-4 p-5 text-center"
|
|
78
|
+
initial={reducedMotion ? { opacity: 0 } : { opacity: 0, y: 12 }}
|
|
79
|
+
animate={{ opacity: 1, y: 0 }}
|
|
80
|
+
exit={reducedMotion ? { opacity: 0 } : { opacity: 0, y: 6 }}
|
|
81
|
+
transition={reducedMotion ? { duration: 0 } : spring.smooth}
|
|
82
|
+
>
|
|
83
|
+
<div className="flex flex-col gap-1.5 px-1 pt-1">
|
|
84
|
+
<h2 id={titleId} className="text-body text-label font-semibold">
|
|
85
|
+
{dialog.title}
|
|
86
|
+
</h2>
|
|
87
|
+
{dialog.message && (
|
|
88
|
+
<p
|
|
89
|
+
id={messageId}
|
|
90
|
+
className="text-subheadline text-label-secondary leading-snug"
|
|
91
|
+
>
|
|
92
|
+
{dialog.message}
|
|
93
|
+
</p>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
<div className="flex gap-2.5">
|
|
97
|
+
<Button
|
|
98
|
+
ref={cancelRef}
|
|
99
|
+
type="button"
|
|
100
|
+
variant="secondary"
|
|
101
|
+
onClick={() => settleDialog(false, dialog.id)}
|
|
102
|
+
className="min-w-0 grow rounded-2xl"
|
|
103
|
+
>
|
|
104
|
+
{dialog.cancelLabel ?? "Cancel"}
|
|
105
|
+
</Button>
|
|
106
|
+
<Button
|
|
107
|
+
ref={confirmRef}
|
|
108
|
+
type="button"
|
|
109
|
+
variant={dialog.destructive ? "destructive" : "default"}
|
|
110
|
+
onClick={() => settleDialog(true, dialog.id)}
|
|
111
|
+
className="min-w-0 grow rounded-2xl"
|
|
112
|
+
>
|
|
113
|
+
{dialog.confirmLabel ?? "OK"}
|
|
114
|
+
</Button>
|
|
115
|
+
</div>
|
|
116
|
+
</motion.div>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
117
119
|
}
|
|
@@ -4,62 +4,63 @@
|
|
|
4
4
|
* `<DialogHost />`.
|
|
5
5
|
*/
|
|
6
6
|
export interface ConfirmOptions {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
title: string;
|
|
8
|
+
message?: string;
|
|
9
|
+
confirmLabel?: string;
|
|
10
|
+
cancelLabel?: string;
|
|
11
|
+
/** Destructive actions are shown in red and are not the default button. */
|
|
12
|
+
destructive?: boolean;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export interface PendingDialog extends ConfirmOptions {
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
id: number;
|
|
17
|
+
resolve: (confirmed: boolean) => void;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
let current: PendingDialog | null = null
|
|
21
|
-
let nextId = 1
|
|
22
|
-
const queue: PendingDialog[] = []
|
|
23
|
-
let exitingId: number | null = null
|
|
24
|
-
const listeners = new Set<() => void>()
|
|
20
|
+
let current: PendingDialog | null = null;
|
|
21
|
+
let nextId = 1;
|
|
22
|
+
const queue: PendingDialog[] = [];
|
|
23
|
+
let exitingId: number | null = null;
|
|
24
|
+
const listeners = new Set<() => void>();
|
|
25
25
|
|
|
26
26
|
const emit = () => {
|
|
27
|
-
|
|
28
|
-
}
|
|
27
|
+
for (const listener of listeners) listener();
|
|
28
|
+
};
|
|
29
29
|
|
|
30
30
|
export function confirmDialog(options: ConfirmOptions): Promise<boolean> {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
const dialog = { ...options, id: nextId++, resolve };
|
|
33
|
+
if (current || exitingId !== null) queue.push(dialog);
|
|
34
|
+
else current = dialog;
|
|
35
|
+
emit();
|
|
36
|
+
});
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export function settleDialog(confirmed: boolean, id?: number) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
if (!current || exitingId !== null || (id !== undefined && current.id !== id))
|
|
41
|
+
return;
|
|
42
|
+
const settled = current;
|
|
43
|
+
exitingId = settled.id;
|
|
44
|
+
current = null;
|
|
45
|
+
settled.resolve(confirmed);
|
|
46
|
+
emit();
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
/** Advance queued confirmations only after the dismissed card has exited. */
|
|
49
50
|
export function advanceDialogQueue() {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
if (exitingId === null) return;
|
|
52
|
+
exitingId = null;
|
|
53
|
+
current = queue.shift() ?? null;
|
|
54
|
+
emit();
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
export function subscribeDialogs(listener: () => void) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
listeners.add(listener);
|
|
59
|
+
return () => {
|
|
60
|
+
listeners.delete(listener);
|
|
61
|
+
};
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
export function getDialog(): PendingDialog | null {
|
|
64
|
-
|
|
65
|
+
return current;
|
|
65
66
|
}
|