@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.
Files changed (82) hide show
  1. package/package.json +5 -3
  2. package/src/accordion.tsx +5 -2
  3. package/src/alert-dialog.tsx +1 -1
  4. package/src/attachment.tsx +5 -2
  5. package/src/autocomplete.tsx +4 -3
  6. package/src/block-editor.tsx +1747 -0
  7. package/src/brand.tsx +2 -2
  8. package/src/breadcrumb.tsx +14 -7
  9. package/src/button.tsx +22 -11
  10. package/src/calendar.tsx +6 -3
  11. package/src/carousel.tsx +220 -53
  12. package/src/checkbox.tsx +3 -2
  13. package/src/code-block.tsx +66 -5
  14. package/src/collapsible.tsx +4 -2
  15. package/src/combobox.tsx +4 -3
  16. package/src/command.tsx +27 -12
  17. package/src/dialog.tsx +33 -10
  18. package/src/empty.tsx +4 -4
  19. package/src/field.tsx +4 -3
  20. package/src/filter-group.tsx +143 -0
  21. package/src/icons.tsx +28 -1
  22. package/src/illustrations.tsx +219 -141
  23. package/src/index.ts +238 -18
  24. package/src/input.tsx +8 -5
  25. package/src/kbd.tsx +10 -4
  26. package/src/lib/focus.ts +41 -0
  27. package/src/lib/popup.ts +1 -1
  28. package/src/lifeline/company-icon.tsx +71 -0
  29. package/src/lifeline/index.ts +42 -0
  30. package/src/lifeline/lifeline-data.ts +97 -0
  31. package/src/lifeline/lifeline-desktop.tsx +204 -0
  32. package/src/lifeline/lifeline-event.tsx +108 -0
  33. package/src/lifeline/lifeline-fireworks.tsx +302 -0
  34. package/src/lifeline/lifeline-hover-image.tsx +269 -0
  35. package/src/lifeline/lifeline-icons.tsx +39 -0
  36. package/src/lifeline/lifeline-intro-timing.ts +105 -0
  37. package/src/lifeline/lifeline-labels.tsx +24 -0
  38. package/src/lifeline/lifeline-layout.ts +1 -0
  39. package/src/lifeline/lifeline-legend.tsx +31 -0
  40. package/src/lifeline/lifeline-lightbox.tsx +309 -0
  41. package/src/lifeline/lifeline-marker.tsx +183 -0
  42. package/src/lifeline/lifeline-people.tsx +99 -0
  43. package/src/lifeline/lifeline-photos.tsx +366 -0
  44. package/src/lifeline/lifeline-shell.tsx +135 -0
  45. package/src/lifeline/lifeline-utils.ts +83 -0
  46. package/src/lifeline/lifeline-vertical.tsx +482 -0
  47. package/src/lifeline/lifeline.tsx +69 -0
  48. package/src/lifeline/types.ts +112 -0
  49. package/src/lifeline/use-lifeline-intro.ts +130 -0
  50. package/src/lifeline/use-lifeline-scroll.ts +1011 -0
  51. package/src/lifeline/use-lifeline-vertical-scroll.ts +271 -0
  52. package/src/menubar.tsx +9 -2
  53. package/src/minimap.tsx +296 -0
  54. package/src/modal.tsx +608 -0
  55. package/src/navigation-menu.tsx +338 -67
  56. package/src/number-field.tsx +336 -61
  57. package/src/otp-field.tsx +4 -3
  58. package/src/pagination.tsx +262 -42
  59. package/src/password-protection.tsx +345 -0
  60. package/src/popover.tsx +1 -1
  61. package/src/progress.tsx +5 -0
  62. package/src/questionnaire.tsx +284 -0
  63. package/src/radio.tsx +4 -2
  64. package/src/scroll-area.tsx +4 -1
  65. package/src/select.tsx +5 -2
  66. package/src/sidebar.tsx +113 -28
  67. package/src/slider.tsx +66 -6
  68. package/src/sounds.tsx +6 -10
  69. package/src/spinner.tsx +105 -32
  70. package/src/switch.tsx +4 -1
  71. package/src/table.tsx +82 -15
  72. package/src/tabs.tsx +189 -38
  73. package/src/text-animate.tsx +71 -28
  74. package/src/textarea.tsx +6 -1
  75. package/src/timeline.tsx +378 -0
  76. package/src/toast.tsx +214 -35
  77. package/src/toggle.tsx +4 -2
  78. package/src/toolbar.tsx +12 -7
  79. package/src/tooltip.tsx +43 -3
  80. package/src/video-player.tsx +396 -127
  81. package/src/image-modal.tsx +0 -110
  82. package/src/markdown-editor.tsx +0 -302
@@ -1,17 +1,93 @@
1
- import type { ComponentProps } from "react";
2
- import { IconChevronRightSmall } from "./icons";
3
- import { cn } from "./lib/cn";
4
- import { motion } from "./lib/motion";
5
-
6
- export type PaginationProps = ComponentProps<"nav">;
7
- export type PaginationContentProps = ComponentProps<"ul">;
8
- export type PaginationItemProps = ComponentProps<"li">;
9
- export type PaginationLinkProps = ComponentProps<"button"> & {
10
- active?: boolean;
11
- };
12
- export type PaginationPreviousProps = PaginationLinkProps;
13
- export type PaginationNextProps = PaginationLinkProps;
14
- export type PaginationEllipsisProps = ComponentProps<"span">;
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) => <li className={cn("flex", className)} {...props} />;
110
+ }: PaginationItemProps) => (
111
+ <li className={cn("flex items-center", className)} {...props} />
112
+ )
35
113
 
36
- export const PaginationLink = ({
37
- className,
38
- active = false,
39
- type = "button",
40
- ...props
41
- }: PaginationLinkProps) => (
42
- <button
43
- type={type}
44
- aria-current={active ? "page" : undefined}
45
- data-active={active || undefined}
46
- className={cn(
47
- "inline-flex size-9 cursor-pointer items-center justify-center rounded-md border border-transparent text-sm text-fg-secondary outline-none hover:bg-background-tertiary hover:text-fg-primary focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-offset-1 focus-visible:ring-offset-background-primary focus-visible:ring-ring/20 disabled:cursor-not-allowed disabled:opacity-50 data-[active]:border-border-primary data-[active]:bg-surface data-[active]:text-fg-primary",
48
- motion.colors,
49
- className,
50
- )}
51
- {...props}
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 className={cn("w-auto gap-1 px-3", className)} {...props}>
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 className={cn("w-auto gap-1 px-3", className)} {...props}>
72
- {children}
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-1 p-4",
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}