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