@passui/react 0.1.19 → 0.1.20
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 +38 -0
- package/THIRD_PARTY_NOTICES.md +25 -0
- package/dist/types/src/components/accordion/accordion.d.ts +19 -0
- package/dist/types/src/components/aspect-ratio/aspect-ratio.d.ts +7 -0
- package/dist/types/src/components/breadcrumb/breadcrumb.d.ts +16 -0
- package/dist/types/src/components/button/button.d.ts +2 -0
- package/dist/types/src/components/code-block/code-block.d.ts +10 -0
- package/dist/types/src/components/combo-box/combo-box.d.ts +9 -0
- package/dist/types/src/components/command/command.d.ts +18 -0
- package/dist/types/src/components/content-unavailable-view/content-unavailable-view.d.ts +9 -0
- package/dist/types/src/components/context-menu/context-menu-store.d.ts +2 -2
- package/dist/types/src/components/context-menu/menu-reveal.d.ts +3 -8
- package/dist/types/src/components/dialog/dialog-store.d.ts +3 -1
- package/dist/types/src/components/disclosure-group/disclosure-group.d.ts +3 -1
- package/dist/types/src/components/keyboard-shortcut/keyboard-shortcut.d.ts +5 -0
- package/dist/types/src/components/menu/menu.d.ts +1 -1
- package/dist/types/src/components/one-time-code/one-time-code.d.ts +18 -0
- package/dist/types/src/components/picker/picker.d.ts +7 -1
- package/dist/types/src/components/popover/anchored-surface.d.ts +13 -0
- package/dist/types/src/components/popover/popover.d.ts +5 -1
- package/dist/types/src/components/progress-view/progress-view.d.ts +1 -1
- package/dist/types/src/components/progress-view/use-liquid-progress.d.ts +3 -0
- package/dist/types/src/components/radio-group/radio-group.d.ts +17 -0
- package/dist/types/src/components/search-field/expanding-search-field.d.ts +5 -0
- package/dist/types/src/components/search-field/search-field.d.ts +5 -1
- package/dist/types/src/components/sidebar/sidebar.d.ts +3 -2
- package/dist/types/src/components/skeleton/skeleton.d.ts +6 -0
- package/dist/types/src/components/slosh-slider/liquid-step.d.ts +6 -0
- package/dist/types/src/components/slosh-slider/slosh-slider.d.ts +18 -0
- package/dist/types/src/components/stepper/rolling-digit.d.ts +11 -0
- package/dist/types/src/components/text-field/text-field.d.ts +3 -1
- package/dist/types/src/components/wake-slider/wake-slider.d.ts +12 -0
- package/dist/types/src/hooks/use-anchored-disclosure.d.ts +9 -0
- package/dist/types/src/hooks/use-anchored-placement.d.ts +1 -1
- package/dist/types/src/index.d.ts +13 -0
- package/dist/types/src/lib/anchored-placement.d.ts +2 -2
- package/package.json +60 -4
- package/src/components/accordion/accordion.tsx +48 -0
- package/src/components/aspect-ratio/aspect-ratio.tsx +22 -0
- package/src/components/breadcrumb/breadcrumb.tsx +53 -0
- package/src/components/button/button.tsx +5 -1
- package/src/components/code-block/code-block.tsx +41 -0
- package/src/components/combo-box/combo-box.tsx +13 -0
- package/src/components/command/command.tsx +119 -0
- package/src/components/compact-date-picker/compact-date-picker.tsx +40 -84
- package/src/components/compact-time-picker/compact-time-picker.tsx +44 -81
- package/src/components/content-unavailable-view/content-unavailable-view.tsx +37 -0
- package/src/components/context-menu/context-menu-host.tsx +28 -24
- package/src/components/context-menu/context-menu-store.ts +3 -4
- package/src/components/context-menu/menu-reveal.ts +5 -2
- package/src/components/dialog/dialog-host.tsx +13 -7
- package/src/components/dialog/dialog-store.ts +15 -4
- package/src/components/disclosure-group/disclosure-group.tsx +19 -10
- package/src/components/keyboard-shortcut/keyboard-shortcut.tsx +17 -0
- package/src/components/menu/menu.tsx +1 -1
- package/src/components/one-time-code/one-time-code.tsx +168 -0
- package/src/components/picker/picker.tsx +121 -108
- package/src/components/popover/anchored-surface.tsx +223 -0
- package/src/components/popover/popover.tsx +149 -20
- package/src/components/progress-view/progress-view.tsx +6 -1
- package/src/components/progress-view/use-liquid-progress.ts +43 -0
- package/src/components/radio-group/radio-group.tsx +63 -0
- package/src/components/search-field/expanding-search-field.tsx +183 -0
- package/src/components/search-field/search-field.tsx +41 -25
- package/src/components/search-list/search-list.tsx +16 -7
- package/src/components/sidebar/sidebar.tsx +21 -12
- package/src/components/skeleton/skeleton.tsx +19 -0
- package/src/components/slosh-slider/liquid-step.ts +42 -0
- package/src/components/slosh-slider/slosh-slider.tsx +251 -0
- package/src/components/stepper/rolling-digit.tsx +77 -0
- package/src/components/stepper/stepper.tsx +50 -29
- package/src/components/text-field/text-field.tsx +45 -7
- package/src/components/toggle/toggle.tsx +147 -30
- package/src/components/wake-slider/wake-slider.tsx +237 -0
- package/src/hooks/use-anchored-disclosure.ts +23 -0
- package/src/hooks/use-anchored-placement.ts +7 -4
- package/src/icons/icons.tsx +150 -150
- package/src/index.ts +18 -0
- package/src/lib/anchored-placement.ts +7 -3
- package/src/styles/code-block.css +33 -0
- package/src/styles/motion-controls.css +189 -0
- package/src/styles/styles.css +3 -0
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { useAnchoredDisclosure } from "../../hooks/use-anchored-disclosure"
|
|
2
|
+
import { AnchoredSurface } from "../popover/anchored-surface"
|
|
3
|
+
import { type KeyboardEvent, useEffect, useId, useRef } from "react"
|
|
3
4
|
import { createPortal } from "react-dom"
|
|
4
|
-
import { FLOATING_LAYER } from "../../hooks/use-dismiss"
|
|
5
|
-
import { useFloatingDismiss } from "../../hooks/use-floating-dismiss"
|
|
6
5
|
import { ClockIcon } from "../../icons/icons"
|
|
7
|
-
import { useAnchoredPlacement } from "../../hooks/use-anchored-placement"
|
|
8
6
|
import { cn } from "../../lib/class-names"
|
|
9
|
-
import { registerFloatingLayer } from "../../lib/floating-layers"
|
|
10
7
|
import { haptic } from "../../lib/haptics"
|
|
11
|
-
import { floatingPanelMotion } from "../../motion/transitions"
|
|
12
8
|
import { pickerTriggerStyle } from "../picker/picker-trigger"
|
|
13
9
|
import {
|
|
14
10
|
formatTime,
|
|
@@ -53,7 +49,8 @@ export function CompactTimePicker({
|
|
|
53
49
|
className,
|
|
54
50
|
disabled = false,
|
|
55
51
|
}: CompactTimePickerProps) {
|
|
56
|
-
const
|
|
52
|
+
const panel = useAnchoredDisclosure(disabled)
|
|
53
|
+
const { open } = panel
|
|
57
54
|
const trigger = useRef<HTMLButtonElement>(null)
|
|
58
55
|
const panelId = useId()
|
|
59
56
|
const chosen = parseTime(value)
|
|
@@ -75,7 +72,7 @@ export function CompactTimePicker({
|
|
|
75
72
|
aria-expanded={open && !disabled}
|
|
76
73
|
aria-controls={open && !disabled ? panelId : undefined}
|
|
77
74
|
disabled={disabled}
|
|
78
|
-
onClick={() =>
|
|
75
|
+
onClick={() => panel.toggle()}
|
|
79
76
|
className={pickerTriggerStyle(className)}
|
|
80
77
|
>
|
|
81
78
|
<span
|
|
@@ -90,33 +87,32 @@ export function CompactTimePicker({
|
|
|
90
87
|
</button>
|
|
91
88
|
{typeof document !== "undefined" &&
|
|
92
89
|
createPortal(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
90
|
+
panel.mounted && trigger.current ? (
|
|
91
|
+
<TimeColumns
|
|
92
|
+
open={open}
|
|
93
|
+
onExited={panel.onExited}
|
|
94
|
+
id={panelId}
|
|
95
|
+
anchor={trigger.current}
|
|
96
|
+
label={label}
|
|
97
|
+
locale={locale}
|
|
98
|
+
time={
|
|
99
|
+
chosen ?? {
|
|
100
|
+
hour: 9,
|
|
101
|
+
minute: 0,
|
|
105
102
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
</AnimatePresence>,
|
|
103
|
+
}
|
|
104
|
+
minuteStep={minuteStep}
|
|
105
|
+
labels={labels}
|
|
106
|
+
onChange={(next) => {
|
|
107
|
+
haptic("selection")
|
|
108
|
+
onChange(formatTime(next))
|
|
109
|
+
}}
|
|
110
|
+
onClose={(restoreFocus) => {
|
|
111
|
+
panel.close()
|
|
112
|
+
if (restoreFocus) trigger.current?.focus({ preventScroll: true })
|
|
113
|
+
}}
|
|
114
|
+
/>
|
|
115
|
+
) : null,
|
|
120
116
|
document.body
|
|
121
117
|
)}
|
|
122
118
|
</>
|
|
@@ -129,6 +125,8 @@ interface ColumnItem {
|
|
|
129
125
|
}
|
|
130
126
|
|
|
131
127
|
function TimeColumns({
|
|
128
|
+
open,
|
|
129
|
+
onExited,
|
|
132
130
|
id,
|
|
133
131
|
anchor,
|
|
134
132
|
label,
|
|
@@ -139,6 +137,8 @@ function TimeColumns({
|
|
|
139
137
|
onChange,
|
|
140
138
|
onClose,
|
|
141
139
|
}: {
|
|
140
|
+
open: boolean
|
|
141
|
+
onExited: () => void
|
|
142
142
|
id: string
|
|
143
143
|
anchor: HTMLElement
|
|
144
144
|
label: string
|
|
@@ -150,9 +150,7 @@ function TimeColumns({
|
|
|
150
150
|
onClose: (restoreFocus: boolean) => void
|
|
151
151
|
}) {
|
|
152
152
|
const root = useRef<HTMLDivElement>(null)
|
|
153
|
-
const reducedMotion = useReducedMotion()
|
|
154
153
|
const twelve = usesTwelveHours(locale)
|
|
155
|
-
const placement = useAnchoredPlacement(anchor, twelve ? 240 : 180, 280)
|
|
156
154
|
const number = new Intl.NumberFormat(locale, {
|
|
157
155
|
minimumIntegerDigits: 2,
|
|
158
156
|
})
|
|
@@ -186,54 +184,19 @@ function TimeColumns({
|
|
|
186
184
|
{ key: 1, text: periodName(true) },
|
|
187
185
|
]
|
|
188
186
|
|
|
189
|
-
useLayoutEffect(() => {
|
|
190
|
-
if (!root.current) return
|
|
191
|
-
return registerFloatingLayer(root.current, anchor)
|
|
192
|
-
}, [anchor])
|
|
193
|
-
|
|
194
|
-
useFloatingDismiss(root, anchor, onClose)
|
|
195
|
-
|
|
196
187
|
return (
|
|
197
|
-
<
|
|
188
|
+
<AnchoredSurface
|
|
189
|
+
open={open}
|
|
190
|
+
onExited={onExited}
|
|
198
191
|
ref={root}
|
|
192
|
+
anchor={anchor}
|
|
193
|
+
width={twelve ? 240 : 180}
|
|
194
|
+
needed={280}
|
|
195
|
+
onClose={onClose}
|
|
199
196
|
id={id}
|
|
200
197
|
role="dialog"
|
|
201
198
|
aria-label={label}
|
|
202
|
-
|
|
203
|
-
reducedMotion
|
|
204
|
-
? { opacity: 0 }
|
|
205
|
-
: {
|
|
206
|
-
opacity: 0,
|
|
207
|
-
scale: 1,
|
|
208
|
-
y: placement.above ? 6 : -6,
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
animate={{ opacity: 1, scale: 1, y: 0 }}
|
|
212
|
-
exit={
|
|
213
|
-
reducedMotion
|
|
214
|
-
? { opacity: 0 }
|
|
215
|
-
: {
|
|
216
|
-
opacity: 0,
|
|
217
|
-
scale: 1,
|
|
218
|
-
y: placement.above ? 4 : -4,
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
transition={
|
|
222
|
-
reducedMotion
|
|
223
|
-
? { duration: 0 }
|
|
224
|
-
: floatingPanelMotion(placement.above ? "above" : "below").transition
|
|
225
|
-
}
|
|
226
|
-
style={{
|
|
227
|
-
left: placement.left,
|
|
228
|
-
width: placement.width,
|
|
229
|
-
top: placement.top,
|
|
230
|
-
bottom: placement.bottom,
|
|
231
|
-
maxHeight: placement.maxHeight,
|
|
232
|
-
overflowY: placement.maxHeight ? "auto" : undefined,
|
|
233
|
-
transformOrigin: placement.above ? "50% 100%" : "50% 0%",
|
|
234
|
-
}}
|
|
235
|
-
className="glass-menu fixed z-[80] flex gap-1 rounded-3xl p-2"
|
|
236
|
-
{...{ [FLOATING_LAYER]: "" }}
|
|
199
|
+
contentClassName="flex-row gap-1 overflow-hidden p-2"
|
|
237
200
|
onKeyDown={(event) => {
|
|
238
201
|
if (event.key !== "Escape") return
|
|
239
202
|
event.preventDefault()
|
|
@@ -272,7 +235,7 @@ function TimeColumns({
|
|
|
272
235
|
}
|
|
273
236
|
/>
|
|
274
237
|
)}
|
|
275
|
-
</
|
|
238
|
+
</AnchoredSurface>
|
|
276
239
|
)
|
|
277
240
|
}
|
|
278
241
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ReactNode, ComponentProps } from "react"
|
|
2
|
+
import { cn } from "../../lib/class-names"
|
|
3
|
+
|
|
4
|
+
export interface ContentUnavailableViewProps extends Omit<ComponentProps<"div">, "title"> {
|
|
5
|
+
title: string
|
|
6
|
+
description?: string
|
|
7
|
+
icon?: ReactNode
|
|
8
|
+
actions?: ReactNode
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** A focused empty or unavailable state with an optional next action. */
|
|
12
|
+
export function ContentUnavailableView({
|
|
13
|
+
title,
|
|
14
|
+
description,
|
|
15
|
+
icon,
|
|
16
|
+
actions,
|
|
17
|
+
className,
|
|
18
|
+
...props
|
|
19
|
+
}: ContentUnavailableViewProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
{...props}
|
|
23
|
+
className={cn("flex flex-col items-center gap-3 px-6 py-10 text-center", className)}
|
|
24
|
+
>
|
|
25
|
+
{icon && (
|
|
26
|
+
<span aria-hidden="true" className="text-label-tertiary">
|
|
27
|
+
{icon}
|
|
28
|
+
</span>
|
|
29
|
+
)}
|
|
30
|
+
<p className="text-headline font-semibold text-label">{title}</p>
|
|
31
|
+
{description && (
|
|
32
|
+
<p className="max-w-sm text-footnote text-label-secondary">{description}</p>
|
|
33
|
+
)}
|
|
34
|
+
{actions && <div className="mt-1 flex flex-wrap justify-center gap-2">{actions}</div>}
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -36,6 +36,7 @@ function Menu({ menu }: { menu: OpenMenu }) {
|
|
|
36
36
|
maxHeight: undefined as number | undefined,
|
|
37
37
|
width: 0,
|
|
38
38
|
height: 0,
|
|
39
|
+
radius: 0,
|
|
39
40
|
ready: false,
|
|
40
41
|
})
|
|
41
42
|
const [focused, setFocused] = useState(-1)
|
|
@@ -102,6 +103,7 @@ function Menu({ menu }: { menu: OpenMenu }) {
|
|
|
102
103
|
bottom: top + (viewport?.height ?? window.innerHeight) - edges.bottom,
|
|
103
104
|
}
|
|
104
105
|
),
|
|
106
|
+
radius: Number.parseFloat(getComputedStyle(element).borderTopLeftRadius) || 0,
|
|
105
107
|
ready: true,
|
|
106
108
|
}
|
|
107
109
|
setPosition((current) =>
|
|
@@ -112,6 +114,7 @@ function Menu({ menu }: { menu: OpenMenu }) {
|
|
|
112
114
|
current.maxHeight === next.maxHeight &&
|
|
113
115
|
current.width === next.width &&
|
|
114
116
|
current.height === next.height &&
|
|
117
|
+
current.radius === next.radius &&
|
|
115
118
|
current.ready === next.ready
|
|
116
119
|
? current
|
|
117
120
|
: next
|
|
@@ -190,8 +193,7 @@ function Menu({ menu }: { menu: OpenMenu }) {
|
|
|
190
193
|
)
|
|
191
194
|
let actionIndex = -1
|
|
192
195
|
return (
|
|
193
|
-
<
|
|
194
|
-
key={position.ready ? "surface" : "measure"}
|
|
196
|
+
<div
|
|
195
197
|
ref={ref}
|
|
196
198
|
role="menu"
|
|
197
199
|
tabIndex={-1}
|
|
@@ -248,7 +250,7 @@ function Menu({ menu }: { menu: OpenMenu }) {
|
|
|
248
250
|
}}
|
|
249
251
|
{...{ [FLOATING_LAYER]: "" }}
|
|
250
252
|
className={cn(
|
|
251
|
-
"fixed z-75 isolate flex min-w-50 max-w-[min(280px,calc(100vw-24px))] flex-col rounded-3xl p-1.5",
|
|
253
|
+
"fixed z-75 isolate flex min-w-50 max-w-[min(280px,calc(100vw-24px))] flex-col rounded-3xl p-1.5 outline-none",
|
|
252
254
|
menu.touch && "min-w-60",
|
|
253
255
|
!present && "pointer-events-none"
|
|
254
256
|
)}
|
|
@@ -263,26 +265,28 @@ function Menu({ menu }: { menu: OpenMenu }) {
|
|
|
263
265
|
}}
|
|
264
266
|
onContextMenu={(event) => event.preventDefault()}
|
|
265
267
|
>
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
268
|
+
{position.ready && (
|
|
269
|
+
<motion.div
|
|
270
|
+
aria-hidden="true"
|
|
271
|
+
className="glass-menu pointer-events-none absolute top-0 left-0 -z-10 rounded-3xl"
|
|
272
|
+
style={{ width: "100%", height: "100%" }}
|
|
273
|
+
initial={reducedMotion || !position.ready ? false : source}
|
|
274
|
+
animate={
|
|
275
|
+
position.ready
|
|
276
|
+
? {
|
|
277
|
+
x: 0,
|
|
278
|
+
y: 0,
|
|
279
|
+
width: position.width,
|
|
280
|
+
height: position.height,
|
|
281
|
+
opacity: 1,
|
|
282
|
+
borderRadius: position.radius,
|
|
283
|
+
}
|
|
284
|
+
: undefined
|
|
285
|
+
}
|
|
286
|
+
exit={reducedMotion ? { opacity: 0 } : source}
|
|
287
|
+
transition={reducedMotion ? { duration: 0 } : spring.smooth}
|
|
288
|
+
/>
|
|
289
|
+
)}
|
|
286
290
|
<motion.div
|
|
287
291
|
ref={content}
|
|
288
292
|
className="flex flex-col overflow-y-auto overscroll-contain"
|
|
@@ -370,6 +374,6 @@ function Menu({ menu }: { menu: OpenMenu }) {
|
|
|
370
374
|
)
|
|
371
375
|
})}
|
|
372
376
|
</motion.div>
|
|
373
|
-
</
|
|
377
|
+
</div>
|
|
374
378
|
)
|
|
375
379
|
}
|
|
@@ -81,8 +81,8 @@ export function openFollowUpMenu(items: ContextMenuItem[]) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
84
|
+
* Expands a control into a menu, lined up with its leading or trailing
|
|
85
|
+
* edge and growing down (or up) from the control's bounds. Leading and
|
|
86
86
|
* trailing follow the text direction, so right-to-left languages mirror.
|
|
87
87
|
* The menu's real size decides where it goes; nothing is guessed.
|
|
88
88
|
*/
|
|
@@ -105,12 +105,11 @@ export function openMenu(
|
|
|
105
105
|
const rtl = getComputedStyle(anchor).direction === "rtl"
|
|
106
106
|
// Trailing is the right edge in left-to-right text, the left edge in right-to-left.
|
|
107
107
|
const right = (edge === "trailing") !== rtl
|
|
108
|
-
const gap = 6
|
|
109
108
|
menu = {
|
|
110
109
|
id: nextId++,
|
|
111
110
|
anchor,
|
|
112
111
|
x: right ? (toolbarRect ?? rect).right : (toolbarRect ?? rect).left,
|
|
113
|
-
y: toolbar ? rect.top : above ? rect.
|
|
112
|
+
y: toolbar ? rect.top : above ? rect.bottom : rect.top,
|
|
114
113
|
items,
|
|
115
114
|
touch: false,
|
|
116
115
|
alignX: right ? "end" : "start",
|
|
@@ -6,7 +6,10 @@ export type MenuRect = {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
/** Animate the material's bounds independently so menu text never stretches. */
|
|
9
|
-
export function menuSurfaceReveal(
|
|
9
|
+
export function menuSurfaceReveal(
|
|
10
|
+
source: MenuRect | undefined,
|
|
11
|
+
target: MenuRect & { radius?: number }
|
|
12
|
+
) {
|
|
10
13
|
if (
|
|
11
14
|
!source ||
|
|
12
15
|
source.width <= 0 ||
|
|
@@ -20,7 +23,7 @@ export function menuSurfaceReveal(source: MenuRect | undefined, target: MenuRect
|
|
|
20
23
|
width: target.width,
|
|
21
24
|
height: target.height,
|
|
22
25
|
opacity: 0,
|
|
23
|
-
borderRadius:
|
|
26
|
+
borderRadius: target.radius ?? 0,
|
|
24
27
|
}
|
|
25
28
|
}
|
|
26
29
|
return {
|
|
@@ -2,7 +2,13 @@ import { AnimatePresence, motion, useReducedMotion } from "motion/react"
|
|
|
2
2
|
import { useEffect, useRef, useSyncExternalStore } from "react"
|
|
3
3
|
import { Button } from "../button/button"
|
|
4
4
|
import { spring } from "../../motion/transitions"
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
advanceDialogQueue,
|
|
7
|
+
getDialog,
|
|
8
|
+
type PendingDialog,
|
|
9
|
+
settleDialog,
|
|
10
|
+
subscribeDialogs,
|
|
11
|
+
} from "./dialog-store"
|
|
6
12
|
|
|
7
13
|
/**
|
|
8
14
|
* Renders confirmation dialogs: a glass card that springs up over blurred
|
|
@@ -12,7 +18,7 @@ import { getDialog, type PendingDialog, settleDialog, subscribeDialogs } from ".
|
|
|
12
18
|
export function DialogHost() {
|
|
13
19
|
const dialog = useSyncExternalStore(subscribeDialogs, getDialog, () => null)
|
|
14
20
|
return (
|
|
15
|
-
<AnimatePresence>
|
|
21
|
+
<AnimatePresence mode="wait" onExitComplete={advanceDialogQueue}>
|
|
16
22
|
{dialog && <ConfirmCard key={dialog.id} dialog={dialog} />}
|
|
17
23
|
</AnimatePresence>
|
|
18
24
|
)
|
|
@@ -31,7 +37,7 @@ function ConfirmCard({ dialog }: { dialog: PendingDialog }) {
|
|
|
31
37
|
const onKey = (event: KeyboardEvent) => {
|
|
32
38
|
if (event.key === "Escape") {
|
|
33
39
|
event.preventDefault()
|
|
34
|
-
settleDialog(false)
|
|
40
|
+
settleDialog(false, dialog.id)
|
|
35
41
|
}
|
|
36
42
|
// Keep focus inside the dialog.
|
|
37
43
|
if (event.key === "Tab") {
|
|
@@ -48,7 +54,7 @@ function ConfirmCard({ dialog }: { dialog: PendingDialog }) {
|
|
|
48
54
|
window.removeEventListener("keydown", onKey)
|
|
49
55
|
previous?.focus?.()
|
|
50
56
|
}
|
|
51
|
-
}, [dialog.destructive])
|
|
57
|
+
}, [dialog.destructive, dialog.id])
|
|
52
58
|
|
|
53
59
|
return (
|
|
54
60
|
<div className="fixed inset-0 z-[70] flex items-center justify-center p-5">
|
|
@@ -59,7 +65,7 @@ function ConfirmCard({ dialog }: { dialog: PendingDialog }) {
|
|
|
59
65
|
animate={{ opacity: 1 }}
|
|
60
66
|
exit={{ opacity: 0 }}
|
|
61
67
|
transition={reducedMotion ? { duration: 0 } : spring.smooth}
|
|
62
|
-
onClick={() => settleDialog(false)}
|
|
68
|
+
onClick={() => settleDialog(false, dialog.id)}
|
|
63
69
|
/>
|
|
64
70
|
<motion.div
|
|
65
71
|
role="alertdialog"
|
|
@@ -90,7 +96,7 @@ function ConfirmCard({ dialog }: { dialog: PendingDialog }) {
|
|
|
90
96
|
ref={cancelRef}
|
|
91
97
|
type="button"
|
|
92
98
|
variant="secondary"
|
|
93
|
-
onClick={() => settleDialog(false)}
|
|
99
|
+
onClick={() => settleDialog(false, dialog.id)}
|
|
94
100
|
className="min-w-0 grow rounded-2xl"
|
|
95
101
|
>
|
|
96
102
|
{dialog.cancelLabel ?? "Cancel"}
|
|
@@ -99,7 +105,7 @@ function ConfirmCard({ dialog }: { dialog: PendingDialog }) {
|
|
|
99
105
|
ref={confirmRef}
|
|
100
106
|
type="button"
|
|
101
107
|
variant={dialog.destructive ? "destructive" : "default"}
|
|
102
|
-
onClick={() => settleDialog(true)}
|
|
108
|
+
onClick={() => settleDialog(true, dialog.id)}
|
|
103
109
|
className="min-w-0 grow rounded-2xl"
|
|
104
110
|
>
|
|
105
111
|
{dialog.confirmLabel ?? "OK"}
|
|
@@ -20,6 +20,7 @@ export interface PendingDialog extends ConfirmOptions {
|
|
|
20
20
|
let current: PendingDialog | null = null
|
|
21
21
|
let nextId = 1
|
|
22
22
|
const queue: PendingDialog[] = []
|
|
23
|
+
let exitingId: number | null = null
|
|
23
24
|
const listeners = new Set<() => void>()
|
|
24
25
|
|
|
25
26
|
const emit = () => {
|
|
@@ -29,15 +30,25 @@ const emit = () => {
|
|
|
29
30
|
export function confirmDialog(options: ConfirmOptions): Promise<boolean> {
|
|
30
31
|
return new Promise((resolve) => {
|
|
31
32
|
const dialog = { ...options, id: nextId++, resolve }
|
|
32
|
-
if (current) queue.push(dialog)
|
|
33
|
+
if (current || exitingId !== null) queue.push(dialog)
|
|
33
34
|
else current = dialog
|
|
34
35
|
emit()
|
|
35
36
|
})
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
export function settleDialog(confirmed: boolean) {
|
|
39
|
-
if (!current) return
|
|
40
|
-
current
|
|
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()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Advance queued confirmations only after the dismissed card has exited. */
|
|
49
|
+
export function advanceDialogQueue() {
|
|
50
|
+
if (exitingId === null) return
|
|
51
|
+
exitingId = null
|
|
41
52
|
current = queue.shift() ?? null
|
|
42
53
|
emit()
|
|
43
54
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Button } from "../button/button"
|
|
1
2
|
import { type ReactNode, useId } from "react"
|
|
2
3
|
import { ChevronDownIcon } from "../../icons/icons"
|
|
3
4
|
import { cn } from "../../lib/class-names"
|
|
@@ -10,6 +11,8 @@ export interface DisclosureGroupProps {
|
|
|
10
11
|
children: ReactNode
|
|
11
12
|
disabled?: boolean
|
|
12
13
|
className?: string
|
|
14
|
+
/** Trigger layout when composed inside a shared surface such as Accordion. */
|
|
15
|
+
triggerClassName?: string
|
|
13
16
|
/** Supporting text beneath the title. */
|
|
14
17
|
description?: string
|
|
15
18
|
}
|
|
@@ -22,14 +25,17 @@ export function DisclosureGroup({
|
|
|
22
25
|
children,
|
|
23
26
|
disabled = false,
|
|
24
27
|
className,
|
|
28
|
+
triggerClassName,
|
|
25
29
|
description,
|
|
26
30
|
}: DisclosureGroupProps) {
|
|
27
31
|
const id = useId()
|
|
28
32
|
|
|
29
33
|
return (
|
|
30
|
-
<div className={cn("min-w-0", className)}>
|
|
34
|
+
<div className={cn("w-full min-w-0", className)}>
|
|
31
35
|
<h3 className="m-0">
|
|
32
|
-
<
|
|
36
|
+
<Button
|
|
37
|
+
variant="ghost"
|
|
38
|
+
contentClassName="w-full min-w-0 justify-between gap-3"
|
|
33
39
|
type="button"
|
|
34
40
|
id={`${id}-trigger`}
|
|
35
41
|
aria-expanded={open}
|
|
@@ -37,15 +43,16 @@ export function DisclosureGroup({
|
|
|
37
43
|
disabled={disabled}
|
|
38
44
|
onClick={() => onOpenChange(!open)}
|
|
39
45
|
className={cn(
|
|
40
|
-
"
|
|
41
|
-
"text-subheadline text-label hover:bg-fill/60 focus-visible:
|
|
42
|
-
"focus-visible:outline-accent-text disabled:cursor-not-allowed disabled:opacity-40"
|
|
46
|
+
"h-auto min-h-11 w-full rounded-xl px-4 py-3 text-start active:scale-100",
|
|
47
|
+
"text-subheadline font-normal text-label hover:bg-fill/60 focus-visible:ring-inset focus-visible:ring-offset-0",
|
|
48
|
+
"focus-visible:outline-accent-text disabled:cursor-not-allowed disabled:opacity-40",
|
|
49
|
+
triggerClassName
|
|
43
50
|
)}
|
|
44
51
|
>
|
|
45
52
|
<span className="flex min-w-0 flex-col gap-0.5">
|
|
46
|
-
<span className="font-
|
|
53
|
+
<span className="font-medium break-words whitespace-normal">{label}</span>
|
|
47
54
|
{description && (
|
|
48
|
-
<span className="text-footnote font-normal text-label-secondary">
|
|
55
|
+
<span className="text-footnote leading-footnote font-normal break-words whitespace-normal text-label-secondary">
|
|
49
56
|
{description}
|
|
50
57
|
</span>
|
|
51
58
|
)}
|
|
@@ -58,11 +65,13 @@ export function DisclosureGroup({
|
|
|
58
65
|
open && "rotate-180"
|
|
59
66
|
)}
|
|
60
67
|
/>
|
|
61
|
-
</
|
|
68
|
+
</Button>
|
|
62
69
|
</h3>
|
|
63
|
-
<section id={id} aria-labelledby={`${id}-trigger`}>
|
|
70
|
+
<section id={id} aria-labelledby={`${id}-trigger`} aria-hidden={!open} inert={!open}>
|
|
64
71
|
<InlineDisclosure open={open}>
|
|
65
|
-
<div className="px-
|
|
72
|
+
<div className="min-w-0 px-4 pb-4 text-subheadline leading-subheadline text-label-secondary [overflow-wrap:anywhere]">
|
|
73
|
+
{children}
|
|
74
|
+
</div>
|
|
66
75
|
</InlineDisclosure>
|
|
67
76
|
</section>
|
|
68
77
|
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ComponentProps } from "react"
|
|
2
|
+
import { cn } from "../../lib/class-names"
|
|
3
|
+
|
|
4
|
+
export interface KeyboardShortcutProps extends ComponentProps<"kbd"> {}
|
|
5
|
+
|
|
6
|
+
/** A quiet keycap for a shortcut supplied and localized by the application. */
|
|
7
|
+
export function KeyboardShortcut({ className, ...props }: KeyboardShortcutProps) {
|
|
8
|
+
return (
|
|
9
|
+
<kbd
|
|
10
|
+
{...props}
|
|
11
|
+
className={cn(
|
|
12
|
+
"inline-flex shrink-0 items-center rounded-md border border-separator/70 bg-fill/50 px-1.5 py-0.5 font-mono text-caption2 leading-none text-label-secondary",
|
|
13
|
+
className
|
|
14
|
+
)}
|
|
15
|
+
/>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -17,7 +17,7 @@ export interface MenuProps extends Omit<ButtonProps, "onClick" | "children"> {
|
|
|
17
17
|
children: ReactNode
|
|
18
18
|
/** Logical edge used to align the menu, including RTL. @default "leading" */
|
|
19
19
|
edge?: ContextMenuEdge
|
|
20
|
-
/**
|
|
20
|
+
/** Expand upward from the trigger instead of downward. @default false */
|
|
21
21
|
above?: boolean
|
|
22
22
|
}
|
|
23
23
|
|