@boyernick/standard-ui-react 0.1.1-canary.9 → 0.2.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/package.json +5 -3
- package/src/accordion.tsx +5 -2
- package/src/alert-dialog.tsx +1 -1
- package/src/attachment.tsx +5 -2
- package/src/autocomplete.tsx +4 -3
- package/src/block-editor.tsx +1747 -0
- package/src/brand.tsx +2 -2
- package/src/breadcrumb.tsx +14 -7
- package/src/button.tsx +22 -11
- package/src/calendar.tsx +6 -3
- package/src/carousel.tsx +220 -53
- package/src/checkbox.tsx +3 -2
- package/src/code-block.tsx +66 -5
- package/src/collapsible.tsx +4 -2
- package/src/combobox.tsx +4 -3
- package/src/command.tsx +27 -12
- package/src/dialog.tsx +33 -10
- package/src/empty.tsx +4 -4
- package/src/field.tsx +4 -3
- package/src/filter-group.tsx +143 -0
- package/src/icons.tsx +28 -1
- package/src/illustrations.tsx +219 -141
- package/src/index.ts +238 -18
- package/src/input.tsx +8 -5
- package/src/kbd.tsx +10 -4
- package/src/lib/focus.ts +41 -0
- package/src/lib/popup.ts +1 -1
- package/src/lifeline/company-icon.tsx +71 -0
- package/src/lifeline/index.ts +42 -0
- package/src/lifeline/lifeline-data.ts +97 -0
- package/src/lifeline/lifeline-desktop.tsx +204 -0
- package/src/lifeline/lifeline-event.tsx +108 -0
- package/src/lifeline/lifeline-fireworks.tsx +302 -0
- package/src/lifeline/lifeline-hover-image.tsx +269 -0
- package/src/lifeline/lifeline-icons.tsx +39 -0
- package/src/lifeline/lifeline-intro-timing.ts +105 -0
- package/src/lifeline/lifeline-labels.tsx +24 -0
- package/src/lifeline/lifeline-layout.ts +1 -0
- package/src/lifeline/lifeline-legend.tsx +31 -0
- package/src/lifeline/lifeline-lightbox.tsx +309 -0
- package/src/lifeline/lifeline-marker.tsx +183 -0
- package/src/lifeline/lifeline-people.tsx +99 -0
- package/src/lifeline/lifeline-photos.tsx +366 -0
- package/src/lifeline/lifeline-shell.tsx +135 -0
- package/src/lifeline/lifeline-utils.ts +83 -0
- package/src/lifeline/lifeline-vertical.tsx +482 -0
- package/src/lifeline/lifeline.tsx +69 -0
- package/src/lifeline/types.ts +112 -0
- package/src/lifeline/use-lifeline-intro.ts +130 -0
- package/src/lifeline/use-lifeline-scroll.ts +1011 -0
- package/src/lifeline/use-lifeline-vertical-scroll.ts +271 -0
- package/src/menubar.tsx +9 -2
- package/src/minimap.tsx +296 -0
- package/src/modal.tsx +608 -0
- package/src/navigation-menu.tsx +338 -67
- package/src/number-field.tsx +336 -61
- package/src/otp-field.tsx +4 -3
- package/src/pagination.tsx +262 -42
- package/src/password-protection.tsx +345 -0
- package/src/popover.tsx +1 -1
- package/src/progress.tsx +5 -0
- package/src/questionnaire.tsx +284 -0
- package/src/radio.tsx +4 -2
- package/src/scroll-area.tsx +4 -1
- package/src/select.tsx +5 -2
- package/src/sidebar.tsx +113 -28
- package/src/slider.tsx +66 -6
- package/src/sounds.tsx +6 -10
- package/src/spinner.tsx +105 -32
- package/src/switch.tsx +4 -1
- package/src/table.tsx +82 -15
- package/src/tabs.tsx +189 -38
- package/src/text-animate.tsx +71 -28
- package/src/textarea.tsx +6 -1
- package/src/timeline.tsx +378 -0
- package/src/toast.tsx +214 -35
- package/src/toggle.tsx +4 -2
- package/src/toolbar.tsx +12 -7
- package/src/tooltip.tsx +43 -3
- package/src/video-player.tsx +396 -127
- package/src/image-modal.tsx +0 -110
- package/src/markdown-editor.tsx +0 -302
package/src/pagination.tsx
CHANGED
|
@@ -1,17 +1,93 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
import type { ComponentProps } from "react"
|
|
5
|
+
import { IconChevronRightSmall } from "./icons"
|
|
6
|
+
import { cn } from "./lib/cn"
|
|
7
|
+
import { focusRing } from "./lib/focus"
|
|
8
|
+
import { motion } from "./lib/motion"
|
|
9
|
+
|
|
10
|
+
const paginationLinkVariants = cva(
|
|
11
|
+
cn(
|
|
12
|
+
"inline-flex cursor-pointer items-center justify-center rounded-md border border-transparent text-fg-secondary hover:bg-background-tertiary hover:text-fg-primary aria-disabled:pointer-events-none aria-disabled:opacity-50 disabled:cursor-not-allowed disabled:opacity-50 data-[active]:border-border-primary data-[active]:bg-surface data-[active]:text-fg-primary [&_svg]:size-4 [&_svg]:shrink-0",
|
|
13
|
+
focusRing,
|
|
14
|
+
),
|
|
15
|
+
{
|
|
16
|
+
variants: {
|
|
17
|
+
size: {
|
|
18
|
+
sm: "size-8 text-xs",
|
|
19
|
+
md: "size-9 text-sm",
|
|
20
|
+
lg: "size-10 text-sm",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
defaultVariants: {
|
|
24
|
+
size: "md",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const paginationDirectionVariants = cva("", {
|
|
30
|
+
variants: {
|
|
31
|
+
size: {
|
|
32
|
+
sm: "",
|
|
33
|
+
md: "",
|
|
34
|
+
lg: "",
|
|
35
|
+
},
|
|
36
|
+
iconOnly: {
|
|
37
|
+
true: "",
|
|
38
|
+
false: "w-auto gap-1",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
compoundVariants: [
|
|
42
|
+
{ size: "sm", iconOnly: false, class: "px-2.5" },
|
|
43
|
+
{ size: "md", iconOnly: false, class: "px-3" },
|
|
44
|
+
{ size: "lg", iconOnly: false, class: "px-3.5" },
|
|
45
|
+
],
|
|
46
|
+
defaultVariants: {
|
|
47
|
+
size: "md",
|
|
48
|
+
iconOnly: false,
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
export type PaginationSize = NonNullable<
|
|
53
|
+
VariantProps<typeof paginationLinkVariants>["size"]
|
|
54
|
+
>
|
|
55
|
+
|
|
56
|
+
type PaginationLinkCommonProps = VariantProps<typeof paginationLinkVariants> & {
|
|
57
|
+
active?: boolean
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type PaginationButtonProps = Omit<ComponentProps<"button">, "size"> &
|
|
61
|
+
PaginationLinkCommonProps & {
|
|
62
|
+
href?: never
|
|
63
|
+
}
|
|
64
|
+
export type PaginationAnchorProps = Omit<ComponentProps<"a">, "size"> &
|
|
65
|
+
PaginationLinkCommonProps & {
|
|
66
|
+
href: string
|
|
67
|
+
disabled?: boolean
|
|
68
|
+
}
|
|
69
|
+
export type PaginationLinkProps =
|
|
70
|
+
| PaginationButtonProps
|
|
71
|
+
| PaginationAnchorProps
|
|
72
|
+
export type PaginationProps = ComponentProps<"nav">
|
|
73
|
+
export type PaginationContentProps = ComponentProps<"ul">
|
|
74
|
+
export type PaginationItemProps = ComponentProps<"li">
|
|
75
|
+
export type PaginationPreviousProps = PaginationLinkProps & {
|
|
76
|
+
iconOnly?: boolean
|
|
77
|
+
}
|
|
78
|
+
export type PaginationNextProps = PaginationPreviousProps
|
|
79
|
+
export type PaginationFirstProps = PaginationPreviousProps
|
|
80
|
+
export type PaginationLastProps = PaginationPreviousProps
|
|
81
|
+
export type PaginationEllipsisProps = ComponentProps<"span">
|
|
82
|
+
export type PaginationStatusProps = ComponentProps<"span"> & {
|
|
83
|
+
page: number
|
|
84
|
+
totalPages: number
|
|
85
|
+
}
|
|
86
|
+
export type PaginationRangeProps = ComponentProps<"span"> & {
|
|
87
|
+
start: number
|
|
88
|
+
end: number
|
|
89
|
+
total: number
|
|
90
|
+
}
|
|
15
91
|
|
|
16
92
|
export const Pagination = ({ className, ...props }: PaginationProps) => (
|
|
17
93
|
<nav
|
|
@@ -19,60 +95,169 @@ export const Pagination = ({ className, ...props }: PaginationProps) => (
|
|
|
19
95
|
className={cn("flex w-full justify-center", className)}
|
|
20
96
|
{...props}
|
|
21
97
|
/>
|
|
22
|
-
)
|
|
98
|
+
)
|
|
23
99
|
|
|
24
100
|
export const PaginationContent = ({
|
|
25
101
|
className,
|
|
26
102
|
...props
|
|
27
103
|
}: PaginationContentProps) => (
|
|
28
104
|
<ul className={cn("flex items-center gap-1", className)} {...props} />
|
|
29
|
-
)
|
|
105
|
+
)
|
|
30
106
|
|
|
31
107
|
export const PaginationItem = ({
|
|
32
108
|
className,
|
|
33
109
|
...props
|
|
34
|
-
}: PaginationItemProps) =>
|
|
110
|
+
}: PaginationItemProps) => (
|
|
111
|
+
<li className={cn("flex items-center", className)} {...props} />
|
|
112
|
+
)
|
|
35
113
|
|
|
36
|
-
export const PaginationLink = ({
|
|
37
|
-
className,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
114
|
+
export const PaginationLink = (props: PaginationLinkProps) => {
|
|
115
|
+
const { className, active = false, size, ...linkProps } = props
|
|
116
|
+
const classes = cn(
|
|
117
|
+
paginationLinkVariants({ size }),
|
|
118
|
+
motion.colors,
|
|
119
|
+
className,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if ("href" in linkProps && typeof linkProps.href === "string") {
|
|
123
|
+
const {
|
|
124
|
+
disabled = false,
|
|
125
|
+
onClick,
|
|
126
|
+
tabIndex,
|
|
127
|
+
...anchorProps
|
|
128
|
+
} = linkProps as PaginationAnchorProps
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<a
|
|
132
|
+
{...anchorProps}
|
|
133
|
+
aria-current={active ? "page" : undefined}
|
|
134
|
+
aria-disabled={disabled || undefined}
|
|
135
|
+
data-active={active || undefined}
|
|
136
|
+
tabIndex={disabled ? -1 : tabIndex}
|
|
137
|
+
className={classes}
|
|
138
|
+
onClick={(event) => {
|
|
139
|
+
if (disabled) {
|
|
140
|
+
event.preventDefault()
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
onClick?.(event)
|
|
144
|
+
}}
|
|
145
|
+
/>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const { type = "button", ...buttonProps } =
|
|
150
|
+
linkProps as PaginationButtonProps
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<button
|
|
154
|
+
type={type}
|
|
155
|
+
aria-current={active ? "page" : undefined}
|
|
156
|
+
data-active={active || undefined}
|
|
157
|
+
className={classes}
|
|
158
|
+
{...buttonProps}
|
|
159
|
+
/>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const DoubleChevron = ({ direction }: { direction: "first" | "last" }) => (
|
|
164
|
+
<span className="flex -space-x-2" aria-hidden>
|
|
165
|
+
<IconChevronRightSmall
|
|
166
|
+
className={direction === "first" ? "rotate-180" : undefined}
|
|
167
|
+
/>
|
|
168
|
+
<IconChevronRightSmall
|
|
169
|
+
className={direction === "first" ? "rotate-180" : undefined}
|
|
170
|
+
/>
|
|
171
|
+
</span>
|
|
172
|
+
)
|
|
54
173
|
|
|
55
174
|
export const PaginationPrevious = ({
|
|
56
175
|
children = "Previous",
|
|
57
176
|
className,
|
|
177
|
+
iconOnly = false,
|
|
178
|
+
size = "md",
|
|
179
|
+
"aria-label": ariaLabel,
|
|
58
180
|
...props
|
|
59
181
|
}: PaginationPreviousProps) => (
|
|
60
|
-
<PaginationLink
|
|
182
|
+
<PaginationLink
|
|
183
|
+
aria-label={ariaLabel ?? (iconOnly ? "Previous page" : undefined)}
|
|
184
|
+
className={cn(
|
|
185
|
+
paginationDirectionVariants({ size, iconOnly }),
|
|
186
|
+
className,
|
|
187
|
+
)}
|
|
188
|
+
size={size}
|
|
189
|
+
{...props}
|
|
190
|
+
>
|
|
61
191
|
<IconChevronRightSmall className="rotate-180" aria-hidden />
|
|
62
|
-
{children}
|
|
192
|
+
{iconOnly ? null : children}
|
|
63
193
|
</PaginationLink>
|
|
64
|
-
)
|
|
194
|
+
)
|
|
65
195
|
|
|
66
196
|
export const PaginationNext = ({
|
|
67
197
|
children = "Next",
|
|
68
198
|
className,
|
|
199
|
+
iconOnly = false,
|
|
200
|
+
size = "md",
|
|
201
|
+
"aria-label": ariaLabel,
|
|
69
202
|
...props
|
|
70
203
|
}: PaginationNextProps) => (
|
|
71
|
-
<PaginationLink
|
|
72
|
-
{
|
|
204
|
+
<PaginationLink
|
|
205
|
+
aria-label={ariaLabel ?? (iconOnly ? "Next page" : undefined)}
|
|
206
|
+
className={cn(
|
|
207
|
+
paginationDirectionVariants({ size, iconOnly }),
|
|
208
|
+
className,
|
|
209
|
+
)}
|
|
210
|
+
size={size}
|
|
211
|
+
{...props}
|
|
212
|
+
>
|
|
213
|
+
{iconOnly ? null : children}
|
|
73
214
|
<IconChevronRightSmall aria-hidden />
|
|
74
215
|
</PaginationLink>
|
|
75
|
-
)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
export const PaginationFirst = ({
|
|
219
|
+
children = "First",
|
|
220
|
+
className,
|
|
221
|
+
iconOnly = false,
|
|
222
|
+
size = "md",
|
|
223
|
+
"aria-label": ariaLabel,
|
|
224
|
+
...props
|
|
225
|
+
}: PaginationFirstProps) => (
|
|
226
|
+
<PaginationLink
|
|
227
|
+
aria-label={ariaLabel ?? (iconOnly ? "First page" : undefined)}
|
|
228
|
+
className={cn(
|
|
229
|
+
paginationDirectionVariants({ size, iconOnly }),
|
|
230
|
+
className,
|
|
231
|
+
)}
|
|
232
|
+
size={size}
|
|
233
|
+
{...props}
|
|
234
|
+
>
|
|
235
|
+
<DoubleChevron direction="first" />
|
|
236
|
+
{iconOnly ? null : children}
|
|
237
|
+
</PaginationLink>
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
export const PaginationLast = ({
|
|
241
|
+
children = "Last",
|
|
242
|
+
className,
|
|
243
|
+
iconOnly = false,
|
|
244
|
+
size = "md",
|
|
245
|
+
"aria-label": ariaLabel,
|
|
246
|
+
...props
|
|
247
|
+
}: PaginationLastProps) => (
|
|
248
|
+
<PaginationLink
|
|
249
|
+
aria-label={ariaLabel ?? (iconOnly ? "Last page" : undefined)}
|
|
250
|
+
className={cn(
|
|
251
|
+
paginationDirectionVariants({ size, iconOnly }),
|
|
252
|
+
className,
|
|
253
|
+
)}
|
|
254
|
+
size={size}
|
|
255
|
+
{...props}
|
|
256
|
+
>
|
|
257
|
+
{iconOnly ? null : children}
|
|
258
|
+
<DoubleChevron direction="last" />
|
|
259
|
+
</PaginationLink>
|
|
260
|
+
)
|
|
76
261
|
|
|
77
262
|
export const PaginationEllipsis = ({
|
|
78
263
|
className,
|
|
@@ -88,4 +273,39 @@ export const PaginationEllipsis = ({
|
|
|
88
273
|
>
|
|
89
274
|
…
|
|
90
275
|
</span>
|
|
91
|
-
)
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
export const PaginationStatus = ({
|
|
279
|
+
page,
|
|
280
|
+
totalPages,
|
|
281
|
+
children,
|
|
282
|
+
className,
|
|
283
|
+
...props
|
|
284
|
+
}: PaginationStatusProps) => (
|
|
285
|
+
<span
|
|
286
|
+
aria-live="polite"
|
|
287
|
+
className={cn("whitespace-nowrap text-sm text-fg-secondary tabular-nums", className)}
|
|
288
|
+
{...props}
|
|
289
|
+
>
|
|
290
|
+
{children ?? `Page ${page} of ${totalPages}`}
|
|
291
|
+
</span>
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
export const PaginationRange = ({
|
|
295
|
+
start,
|
|
296
|
+
end,
|
|
297
|
+
total,
|
|
298
|
+
children,
|
|
299
|
+
className,
|
|
300
|
+
...props
|
|
301
|
+
}: PaginationRangeProps) => (
|
|
302
|
+
<span
|
|
303
|
+
aria-live="polite"
|
|
304
|
+
className={cn("whitespace-nowrap text-sm text-fg-secondary tabular-nums", className)}
|
|
305
|
+
{...props}
|
|
306
|
+
>
|
|
307
|
+
{children ?? `${start}–${end} of ${total}`}
|
|
308
|
+
</span>
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
export { paginationDirectionVariants, paginationLinkVariants }
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
type ReactElement,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from "react"
|
|
9
|
+
import { Button } from "./button"
|
|
10
|
+
import {
|
|
11
|
+
Dialog,
|
|
12
|
+
DialogBackdrop,
|
|
13
|
+
DialogClose,
|
|
14
|
+
DialogDescription,
|
|
15
|
+
DialogHeader,
|
|
16
|
+
DialogPopup,
|
|
17
|
+
DialogPortal,
|
|
18
|
+
DialogTitle,
|
|
19
|
+
DialogTrigger,
|
|
20
|
+
} from "./dialog"
|
|
21
|
+
import {
|
|
22
|
+
Field,
|
|
23
|
+
FieldControl,
|
|
24
|
+
FieldError,
|
|
25
|
+
FieldLabel,
|
|
26
|
+
} from "./field"
|
|
27
|
+
import { Form } from "./form"
|
|
28
|
+
import { cn } from "./lib/cn"
|
|
29
|
+
|
|
30
|
+
export type PasswordProtectionVerify = (
|
|
31
|
+
password: string,
|
|
32
|
+
) => boolean | Promise<boolean>
|
|
33
|
+
|
|
34
|
+
type PasswordProtectionSharedProps = {
|
|
35
|
+
/** Returns true when the entered password unlocks access. */
|
|
36
|
+
verify: PasswordProtectionVerify
|
|
37
|
+
title?: ReactNode
|
|
38
|
+
description?: ReactNode
|
|
39
|
+
submitLabel?: string
|
|
40
|
+
cancelLabel?: string
|
|
41
|
+
passwordLabel?: string
|
|
42
|
+
passwordPlaceholder?: string
|
|
43
|
+
/** Shown when `verify` returns false. */
|
|
44
|
+
errorMessage?: string
|
|
45
|
+
onUnlock?: () => void
|
|
46
|
+
className?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type PasswordProtectionProps = PasswordProtectionSharedProps & {
|
|
50
|
+
children: ReactNode
|
|
51
|
+
/** Controlled unlocked state. */
|
|
52
|
+
unlocked?: boolean
|
|
53
|
+
defaultUnlocked?: boolean
|
|
54
|
+
onUnlockedChange?: (unlocked: boolean) => void
|
|
55
|
+
/** Show a cancel action that dismisses without unlocking. @default true */
|
|
56
|
+
cancelable?: boolean
|
|
57
|
+
/** Called when the visitor dismisses without unlocking. */
|
|
58
|
+
onCancel?: () => void
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type PasswordProtectionDialogProps = PasswordProtectionSharedProps & {
|
|
62
|
+
/** Element rendered as the dialog trigger (`render` target). */
|
|
63
|
+
trigger: ReactElement
|
|
64
|
+
/** Label rendered inside the trigger. */
|
|
65
|
+
children: ReactNode
|
|
66
|
+
open?: boolean
|
|
67
|
+
defaultOpen?: boolean
|
|
68
|
+
onOpenChange?: (open: boolean) => void
|
|
69
|
+
/** Show a cancel action that closes without unlocking. @default true */
|
|
70
|
+
cancelable?: boolean
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const DEFAULT_TITLE = "This content is protected"
|
|
74
|
+
const DEFAULT_DESCRIPTION = "Enter password to continue"
|
|
75
|
+
const DEFAULT_SUBMIT = "Continue"
|
|
76
|
+
const DEFAULT_CANCEL = "Cancel"
|
|
77
|
+
const DEFAULT_PASSWORD_LABEL = "Password"
|
|
78
|
+
const DEFAULT_ERROR = "Incorrect password"
|
|
79
|
+
const DEFAULT_EMPTY = "Enter a password."
|
|
80
|
+
|
|
81
|
+
const passwordBackdropClassName = "bg-black/55 dark:bg-black/75"
|
|
82
|
+
const passwordPopupClassName = "gap-4"
|
|
83
|
+
|
|
84
|
+
type PasswordFormProps = Omit<PasswordProtectionSharedProps, "onUnlock"> & {
|
|
85
|
+
onSuccess: () => void
|
|
86
|
+
cancelable?: boolean
|
|
87
|
+
/** Prefer this over DialogClose when the dialog is force-open (gate). */
|
|
88
|
+
onCancel?: () => void
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const PasswordForm = ({
|
|
92
|
+
verify,
|
|
93
|
+
title = DEFAULT_TITLE,
|
|
94
|
+
description = DEFAULT_DESCRIPTION,
|
|
95
|
+
submitLabel = DEFAULT_SUBMIT,
|
|
96
|
+
cancelLabel = DEFAULT_CANCEL,
|
|
97
|
+
passwordLabel = DEFAULT_PASSWORD_LABEL,
|
|
98
|
+
passwordPlaceholder,
|
|
99
|
+
errorMessage = DEFAULT_ERROR,
|
|
100
|
+
onSuccess,
|
|
101
|
+
cancelable = false,
|
|
102
|
+
onCancel,
|
|
103
|
+
className,
|
|
104
|
+
}: PasswordFormProps) => {
|
|
105
|
+
const [errors, setErrors] = useState<Record<string, string | string[]>>({})
|
|
106
|
+
const [pending, setPending] = useState(false)
|
|
107
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
108
|
+
|
|
109
|
+
const handleSubmit = (values: Record<string, unknown>) => {
|
|
110
|
+
const password = String(values.password ?? "")
|
|
111
|
+
setErrors({})
|
|
112
|
+
|
|
113
|
+
if (!password.trim()) {
|
|
114
|
+
setErrors({ password: DEFAULT_EMPTY })
|
|
115
|
+
queueMicrotask(() => inputRef.current?.focus())
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
setPending(true)
|
|
120
|
+
|
|
121
|
+
const finish = (ok: boolean) => {
|
|
122
|
+
if (!ok) {
|
|
123
|
+
setErrors({ password: errorMessage })
|
|
124
|
+
setPending(false)
|
|
125
|
+
queueMicrotask(() => inputRef.current?.select())
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
setPending(false)
|
|
129
|
+
onSuccess()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const result = verify(password)
|
|
134
|
+
if (result instanceof Promise) {
|
|
135
|
+
void result.then(finish).catch(() => finish(false))
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
finish(result)
|
|
139
|
+
} catch {
|
|
140
|
+
finish(false)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<>
|
|
146
|
+
<DialogHeader>
|
|
147
|
+
<DialogTitle>{title}</DialogTitle>
|
|
148
|
+
{description ? (
|
|
149
|
+
<DialogDescription>{description}</DialogDescription>
|
|
150
|
+
) : null}
|
|
151
|
+
</DialogHeader>
|
|
152
|
+
<Form
|
|
153
|
+
errors={errors}
|
|
154
|
+
onFormSubmit={handleSubmit}
|
|
155
|
+
className={cn("gap-4", className)}
|
|
156
|
+
>
|
|
157
|
+
<Field name="password">
|
|
158
|
+
<FieldLabel>{passwordLabel}</FieldLabel>
|
|
159
|
+
<FieldControl
|
|
160
|
+
ref={inputRef}
|
|
161
|
+
type="password"
|
|
162
|
+
name="password"
|
|
163
|
+
autoComplete="current-password"
|
|
164
|
+
autoFocus
|
|
165
|
+
placeholder={passwordPlaceholder}
|
|
166
|
+
disabled={pending}
|
|
167
|
+
aria-invalid={errors.password ? true : undefined}
|
|
168
|
+
/>
|
|
169
|
+
<FieldError />
|
|
170
|
+
</Field>
|
|
171
|
+
<div className="flex justify-end gap-2">
|
|
172
|
+
{cancelable ? (
|
|
173
|
+
onCancel ? (
|
|
174
|
+
<Button
|
|
175
|
+
type="button"
|
|
176
|
+
variant="outline"
|
|
177
|
+
disabled={pending}
|
|
178
|
+
onClick={onCancel}
|
|
179
|
+
>
|
|
180
|
+
{cancelLabel}
|
|
181
|
+
</Button>
|
|
182
|
+
) : (
|
|
183
|
+
<DialogClose
|
|
184
|
+
render={
|
|
185
|
+
<Button type="button" variant="outline" disabled={pending} />
|
|
186
|
+
}
|
|
187
|
+
>
|
|
188
|
+
{cancelLabel}
|
|
189
|
+
</DialogClose>
|
|
190
|
+
)
|
|
191
|
+
) : null}
|
|
192
|
+
<Button type="submit" disabled={pending}>
|
|
193
|
+
{pending ? "Checking…" : submitLabel}
|
|
194
|
+
</Button>
|
|
195
|
+
</div>
|
|
196
|
+
</Form>
|
|
197
|
+
</>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Gates children behind a password dialog. The dialog stays open until
|
|
203
|
+
* `verify` succeeds — soft client-side protection for private links and pages.
|
|
204
|
+
*/
|
|
205
|
+
export const PasswordProtection = ({
|
|
206
|
+
verify,
|
|
207
|
+
children,
|
|
208
|
+
unlocked: unlockedProp,
|
|
209
|
+
defaultUnlocked = false,
|
|
210
|
+
onUnlockedChange,
|
|
211
|
+
onUnlock,
|
|
212
|
+
onCancel,
|
|
213
|
+
cancelable = true,
|
|
214
|
+
title,
|
|
215
|
+
description,
|
|
216
|
+
submitLabel,
|
|
217
|
+
cancelLabel,
|
|
218
|
+
passwordLabel,
|
|
219
|
+
passwordPlaceholder,
|
|
220
|
+
errorMessage,
|
|
221
|
+
className,
|
|
222
|
+
}: PasswordProtectionProps) => {
|
|
223
|
+
const [unlockedState, setUnlockedState] = useState(defaultUnlocked)
|
|
224
|
+
const [dismissed, setDismissed] = useState(false)
|
|
225
|
+
const unlocked = unlockedProp ?? unlockedState
|
|
226
|
+
|
|
227
|
+
const setUnlocked = (next: boolean) => {
|
|
228
|
+
if (unlockedProp === undefined) setUnlockedState(next)
|
|
229
|
+
onUnlockedChange?.(next)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const handleSuccess = () => {
|
|
233
|
+
setDismissed(false)
|
|
234
|
+
setUnlocked(true)
|
|
235
|
+
onUnlock?.()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const handleCancel = () => {
|
|
239
|
+
setDismissed(true)
|
|
240
|
+
onCancel?.()
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (unlocked) return <>{children}</>
|
|
244
|
+
if (dismissed) return null
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<Dialog
|
|
248
|
+
open
|
|
249
|
+
disablePointerDismissal={!cancelable}
|
|
250
|
+
onOpenChange={(open) => {
|
|
251
|
+
if (open) return
|
|
252
|
+
if (!cancelable) return
|
|
253
|
+
handleCancel()
|
|
254
|
+
}}
|
|
255
|
+
>
|
|
256
|
+
<DialogPortal>
|
|
257
|
+
<DialogBackdrop className={passwordBackdropClassName} />
|
|
258
|
+
<DialogPopup
|
|
259
|
+
data-slot="password-protection"
|
|
260
|
+
className={cn(passwordPopupClassName, className)}
|
|
261
|
+
>
|
|
262
|
+
<PasswordForm
|
|
263
|
+
verify={verify}
|
|
264
|
+
title={title}
|
|
265
|
+
description={description}
|
|
266
|
+
submitLabel={submitLabel}
|
|
267
|
+
cancelLabel={cancelLabel}
|
|
268
|
+
passwordLabel={passwordLabel}
|
|
269
|
+
passwordPlaceholder={passwordPlaceholder}
|
|
270
|
+
errorMessage={errorMessage}
|
|
271
|
+
cancelable={cancelable}
|
|
272
|
+
onCancel={cancelable ? handleCancel : undefined}
|
|
273
|
+
onSuccess={handleSuccess}
|
|
274
|
+
/>
|
|
275
|
+
</DialogPopup>
|
|
276
|
+
</DialogPortal>
|
|
277
|
+
</Dialog>
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Opens a password dialog from a trigger — for gated links that unlock a
|
|
283
|
+
* navigation or reveal after a successful check.
|
|
284
|
+
*/
|
|
285
|
+
export const PasswordProtectionDialog = ({
|
|
286
|
+
verify,
|
|
287
|
+
trigger,
|
|
288
|
+
children,
|
|
289
|
+
open: openProp,
|
|
290
|
+
defaultOpen = false,
|
|
291
|
+
onOpenChange,
|
|
292
|
+
onUnlock,
|
|
293
|
+
title,
|
|
294
|
+
description,
|
|
295
|
+
submitLabel,
|
|
296
|
+
cancelLabel,
|
|
297
|
+
passwordLabel,
|
|
298
|
+
passwordPlaceholder,
|
|
299
|
+
errorMessage,
|
|
300
|
+
cancelable = true,
|
|
301
|
+
className,
|
|
302
|
+
}: PasswordProtectionDialogProps) => {
|
|
303
|
+
const [openState, setOpenState] = useState(defaultOpen)
|
|
304
|
+
const open = openProp ?? openState
|
|
305
|
+
|
|
306
|
+
const setOpen = (next: boolean) => {
|
|
307
|
+
if (openProp === undefined) setOpenState(next)
|
|
308
|
+
onOpenChange?.(next)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const handleSuccess = () => {
|
|
312
|
+
setOpen(false)
|
|
313
|
+
onUnlock?.()
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return (
|
|
317
|
+
<Dialog
|
|
318
|
+
open={open}
|
|
319
|
+
onOpenChange={setOpen}
|
|
320
|
+
disablePointerDismissal={!cancelable}
|
|
321
|
+
>
|
|
322
|
+
<DialogTrigger render={trigger}>{children}</DialogTrigger>
|
|
323
|
+
<DialogPortal>
|
|
324
|
+
<DialogBackdrop className={passwordBackdropClassName} />
|
|
325
|
+
<DialogPopup
|
|
326
|
+
data-slot="password-protection-dialog"
|
|
327
|
+
className={cn(passwordPopupClassName, className)}
|
|
328
|
+
>
|
|
329
|
+
<PasswordForm
|
|
330
|
+
verify={verify}
|
|
331
|
+
title={title}
|
|
332
|
+
description={description}
|
|
333
|
+
submitLabel={submitLabel}
|
|
334
|
+
cancelLabel={cancelLabel}
|
|
335
|
+
passwordLabel={passwordLabel}
|
|
336
|
+
passwordPlaceholder={passwordPlaceholder}
|
|
337
|
+
errorMessage={errorMessage}
|
|
338
|
+
cancelable={cancelable}
|
|
339
|
+
onSuccess={handleSuccess}
|
|
340
|
+
/>
|
|
341
|
+
</DialogPopup>
|
|
342
|
+
</DialogPortal>
|
|
343
|
+
</Dialog>
|
|
344
|
+
)
|
|
345
|
+
}
|
package/src/popover.tsx
CHANGED
|
@@ -48,7 +48,7 @@ export const PopoverPositioner = ({
|
|
|
48
48
|
export const PopoverPopup = ({ className, ...props }: PopoverPopupProps) => (
|
|
49
49
|
<BasePopover.Popup
|
|
50
50
|
className={cn(
|
|
51
|
-
"z-50 flex w-72 flex-col gap-
|
|
51
|
+
"z-50 flex w-72 flex-col gap-0.5 px-4 py-3",
|
|
52
52
|
popupSurface,
|
|
53
53
|
motion.popupAnchor,
|
|
54
54
|
className,
|
package/src/progress.tsx
CHANGED
|
@@ -53,6 +53,11 @@ export const ProgressIndicator = ({
|
|
|
53
53
|
<BaseProgress.Indicator
|
|
54
54
|
className={cn(
|
|
55
55
|
"h-full rounded-full bg-brand-primary transition-[width] duration-[var(--duration-lg)] ease-enter motion-reduce:transition-none",
|
|
56
|
+
// Base UI flags a null value with data-indeterminate and leaves the
|
|
57
|
+
// indicator full width, so without this the bar reads as complete.
|
|
58
|
+
// A quarter-width stripe crossing the track on the passive curve —
|
|
59
|
+
// ambient, system-initiated movement.
|
|
60
|
+
"data-indeterminate:w-1/4 data-indeterminate:animate-[progress-indeterminate_1.4s_var(--ease-passive)_infinite] motion-reduce:animate-none",
|
|
56
61
|
className,
|
|
57
62
|
)}
|
|
58
63
|
{...props}
|