@passui/react 0.1.45 → 0.1.46

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 (140) 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 +32 -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 +279 -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 +334 -0
  72. package/src/components/popover/popover-fit.ts +58 -53
  73. package/src/components/popover/popover-root.tsx +97 -0
  74. package/src/components/popover/popover-trigger.tsx +71 -0
  75. package/src/components/profile-image/profile-image.tsx +80 -64
  76. package/src/components/progress-view/progress-view.tsx +61 -59
  77. package/src/components/progress-view/use-liquid-progress.ts +38 -38
  78. package/src/components/radio-group/radio-group.tsx +71 -68
  79. package/src/components/search-field/expanding-search-field.tsx +191 -177
  80. package/src/components/search-field/search-field.tsx +59 -50
  81. package/src/components/search-list/search-list.tsx +246 -236
  82. package/src/components/section/section.tsx +58 -58
  83. package/src/components/secure-field/secure-field.tsx +3 -3
  84. package/src/components/segmented-control/segmented-control.tsx +136 -136
  85. package/src/components/sheet/modal-environment.ts +71 -64
  86. package/src/components/sheet/sheet-portal.tsx +58 -53
  87. package/src/components/sheet/sheet.tsx +146 -141
  88. package/src/components/sheet/use-sheet-focus.ts +97 -82
  89. package/src/components/sidebar/sidebar.tsx +78 -78
  90. package/src/components/skeleton/skeleton.tsx +30 -21
  91. package/src/components/slider/slider.tsx +113 -104
  92. package/src/components/slosh-slider/liquid-step.ts +32 -31
  93. package/src/components/slosh-slider/slosh-slider.tsx +265 -228
  94. package/src/components/stat-tile/index.ts +1 -1
  95. package/src/components/stat-tile/stat-tile.tsx +79 -68
  96. package/src/components/status-indicator/status-indicator.tsx +40 -37
  97. package/src/components/stepper/rolling-digit.tsx +82 -68
  98. package/src/components/stepper/stepper.tsx +122 -113
  99. package/src/components/swatch-picker/swatch-picker.tsx +120 -120
  100. package/src/components/tab-bar/tab-bar.tsx +149 -148
  101. package/src/components/tab-view/tab-view.tsx +140 -137
  102. package/src/components/table/table.tsx +117 -98
  103. package/src/components/text-editor/text-editor.tsx +34 -29
  104. package/src/components/text-field/text-field.tsx +182 -180
  105. package/src/components/toast/toast-store.ts +127 -115
  106. package/src/components/toast/toaster.tsx +361 -347
  107. package/src/components/toggle/toggle.tsx +297 -282
  108. package/src/components/tooltip/tooltip-host.tsx +157 -150
  109. package/src/components/view-controller/index.ts +1 -1
  110. package/src/components/view-controller/view-controller.tsx +64 -53
  111. package/src/components/wake-slider/wake-slider.tsx +220 -205
  112. package/src/hooks/scroll/minimize-decision.ts +11 -7
  113. package/src/hooks/use-anchored-disclosure.ts +19 -19
  114. package/src/hooks/use-anchored-placement.ts +29 -25
  115. package/src/hooks/use-dismiss.ts +33 -29
  116. package/src/hooks/use-floating-dismiss.ts +24 -22
  117. package/src/hooks/use-media-query.ts +67 -60
  118. package/src/hooks/use-minimize-on-scroll.ts +69 -60
  119. package/src/hooks/use-visual-viewport.ts +63 -63
  120. package/src/hooks/viewport/types.ts +16 -16
  121. package/src/hooks/viewport/viewport-metrics.ts +16 -10
  122. package/src/icons/icons.tsx +440 -328
  123. package/src/index.ts +91 -91
  124. package/src/lib/anchored-placement.ts +70 -65
  125. package/src/lib/class-names.ts +14 -13
  126. package/src/lib/clipboard.ts +8 -7
  127. package/src/lib/date.ts +61 -60
  128. package/src/lib/floating-layers.ts +39 -24
  129. package/src/lib/haptics.ts +10 -10
  130. package/src/lib/language.ts +9 -9
  131. package/src/lib/range-value.ts +27 -21
  132. package/src/lib/screen-edges.ts +33 -30
  133. package/src/lib/shapes.ts +38 -34
  134. package/src/machines/popover.machine.ts +51 -0
  135. package/src/motion/transitions.ts +46 -43
  136. package/src/styles/code-block.css +69 -43
  137. package/src/styles/motion-controls.css +393 -191
  138. package/src/tokens.ts +1 -1
  139. package/dist/types/src/components/popover/popover.d.ts +0 -21
  140. package/src/components/popover/popover.tsx +0 -334
@@ -1,61 +1,70 @@
1
- import type { ReactNode } from "react"
2
- import { DisclosureGroup, type DisclosureGroupSize } from "../disclosure-group/disclosure-group"
3
- import { cn } from "../../lib/class-names"
1
+ import type { ReactNode } from "react";
2
+ import {
3
+ DisclosureGroup,
4
+ type DisclosureGroupSize,
5
+ } from "../disclosure-group/disclosure-group";
6
+ import { cn } from "../../lib/class-names";
4
7
 
5
- export type AccordionSize = DisclosureGroupSize
8
+ export type AccordionSize = DisclosureGroupSize;
6
9
 
7
10
  const containerRadius: Record<AccordionSize, string> = {
8
- sm: "rounded-xl",
9
- md: "rounded-2xl",
10
- lg: "rounded-3xl",
11
- }
11
+ sm: "rounded-xl",
12
+ md: "rounded-2xl",
13
+ lg: "rounded-3xl",
14
+ };
12
15
 
13
16
  export interface AccordionItem {
14
- /** Stable value used to identify the expanded item. */
15
- value: string
16
- title: string
17
- content: ReactNode
18
- description?: string
19
- disabled?: boolean
17
+ /** Stable value used to identify the expanded item. */
18
+ value: string;
19
+ title: string;
20
+ content: ReactNode;
21
+ description?: string;
22
+ disabled?: boolean;
20
23
  }
21
24
 
22
25
  export interface AccordionProps {
23
- items: readonly AccordionItem[]
24
- /** The expanded item, or `null` when all items are closed. */
25
- value: string | null
26
- /** Selecting the open item reports `null`, so every item can collapse. */
27
- onValueChange: (value: string | null) => void
28
- /** Density passed through to each DisclosureGroup. @default "md" */
29
- size?: AccordionSize
30
- className?: string
26
+ items: readonly AccordionItem[];
27
+ /** The expanded item, or `null` when all items are closed. */
28
+ value: string | null;
29
+ /** Selecting the open item reports `null`, so every item can collapse. */
30
+ onValueChange: (value: string | null) => void;
31
+ /** Density passed through to each DisclosureGroup. @default "md" */
32
+ size?: AccordionSize;
33
+ className?: string;
31
34
  }
32
35
 
33
36
  /** A single-open, collapsible accordion with labelled animated content regions. */
34
- export function Accordion({ items, value, onValueChange, size = "md", className }: AccordionProps) {
35
- return (
36
- <div
37
- className={cn(
38
- "flex w-full min-w-0 flex-col overflow-hidden",
39
- containerRadius[size],
40
- "border border-separator/60 bg-elevated",
41
- className
42
- )}
43
- >
44
- {items.map((item, index) => (
45
- <DisclosureGroup
46
- key={item.value}
47
- label={item.title}
48
- description={item.description}
49
- open={value === item.value}
50
- disabled={item.disabled}
51
- size={size}
52
- triggerClassName="rounded-none"
53
- onOpenChange={(open) => onValueChange(open ? item.value : null)}
54
- className={cn(index > 0 && "border-t border-separator/60")}
55
- >
56
- {item.content}
57
- </DisclosureGroup>
58
- ))}
59
- </div>
60
- )
37
+ export function Accordion({
38
+ items,
39
+ value,
40
+ onValueChange,
41
+ size = "md",
42
+ className,
43
+ }: AccordionProps) {
44
+ return (
45
+ <div
46
+ className={cn(
47
+ "flex w-full min-w-0 flex-col overflow-hidden",
48
+ containerRadius[size],
49
+ "border-separator/60 bg-elevated border",
50
+ className,
51
+ )}
52
+ >
53
+ {items.map((item, index) => (
54
+ <DisclosureGroup
55
+ key={item.value}
56
+ label={item.title}
57
+ description={item.description}
58
+ open={value === item.value}
59
+ disabled={item.disabled}
60
+ size={size}
61
+ triggerClassName="rounded-none"
62
+ onOpenChange={(open) => onValueChange(open ? item.value : null)}
63
+ className={cn(index > 0 && "border-separator/60 border-t")}
64
+ >
65
+ {item.content}
66
+ </DisclosureGroup>
67
+ ))}
68
+ </div>
69
+ );
61
70
  }
@@ -1,10 +1,10 @@
1
- import type { ComponentPropsWithoutRef } from "react"
2
- import { cn } from "../../lib/class-names"
1
+ import type { ComponentPropsWithoutRef } from "react";
2
+ import { cn } from "../../lib/class-names";
3
3
 
4
4
  type AmbientProps = ComponentPropsWithoutRef<"section"> & {
5
- /** The image whose colors glow behind the content. */
6
- src?: string | null
7
- }
5
+ /** The image whose colors glow behind the content. */
6
+ src?: string | null;
7
+ };
8
8
 
9
9
  /**
10
10
  * Content with its image's colors glowing softly behind it, no card or
@@ -12,24 +12,24 @@ type AmbientProps = ComponentPropsWithoutRef<"section"> & {
12
12
  * fades into the page, the way Apple lets a photo light its surroundings.
13
13
  */
14
14
  export function Ambient({ src, className, children, ...props }: AmbientProps) {
15
- return (
16
- <section className={cn("relative isolate", className)} {...props}>
17
- {src && (
18
- <img
19
- aria-hidden
20
- src={src}
21
- alt=""
22
- className={cn(
23
- "pointer-events-none absolute top-[38%] left-1/2 -z-10 size-[min(32rem,100vw)] -translate-x-1/2 -translate-y-1/2",
24
- "rounded-full object-cover opacity-20 blur-3xl",
25
- // on a dark page the same light blooms far wider, so it shrinks and dims
26
- "dark:size-[min(20rem,80vw)] dark:opacity-15 dark:blur-2xl",
27
- "mask-[radial-gradient(closest-side,black_35%,transparent)]",
28
- "animate-in fade-in-0 zoom-in-90 duration-1000"
29
- )}
30
- />
31
- )}
32
- {children}
33
- </section>
34
- )
15
+ return (
16
+ <section className={cn("relative isolate", className)} {...props}>
17
+ {src && (
18
+ <img
19
+ aria-hidden
20
+ src={src}
21
+ alt=""
22
+ className={cn(
23
+ "pointer-events-none absolute top-[38%] left-1/2 -z-10 size-[min(32rem,100vw)] -translate-x-1/2 -translate-y-1/2",
24
+ "rounded-full object-cover opacity-20 blur-3xl",
25
+ // on a dark page the same light blooms far wider, so it shrinks and dims
26
+ "dark:size-[min(20rem,80vw)] dark:opacity-15 dark:blur-2xl",
27
+ "mask-[radial-gradient(closest-side,black_35%,transparent)]",
28
+ "animate-in fade-in-0 zoom-in-90 duration-1000",
29
+ )}
30
+ />
31
+ )}
32
+ {children}
33
+ </section>
34
+ );
35
35
  }
@@ -1 +1 @@
1
- export * from "./ambient"
1
+ export * from "./ambient";
@@ -1,22 +1,27 @@
1
- import type { HTMLAttributes } from "react"
2
- import { cn } from "../../lib/class-names"
1
+ import type { HTMLAttributes } from "react";
2
+ import { cn } from "../../lib/class-names";
3
3
 
4
4
  export interface AspectRatioProps extends HTMLAttributes<HTMLDivElement> {
5
- /** Width divided by height. @default 1.3333333333333333 */
6
- ratio?: number
5
+ /** Width divided by height. @default 1.3333333333333333 */
6
+ ratio?: number;
7
7
  }
8
8
 
9
9
  /** A crop frame: size its geometry, while images cover without distortion. */
10
- export function AspectRatio({ ratio = 4 / 3, className, style, ...props }: AspectRatioProps) {
11
- const aspect = Number.isFinite(ratio) && ratio > 0 ? ratio : 4 / 3
12
- return (
13
- <div
14
- {...props}
15
- className={cn(
16
- "relative w-full overflow-hidden [&>img]:size-full [&>img]:object-cover [&>video]:size-full [&>video]:object-cover",
17
- className
18
- )}
19
- style={{ aspectRatio: aspect, ...style }}
20
- />
21
- )
10
+ export function AspectRatio({
11
+ ratio = 4 / 3,
12
+ className,
13
+ style,
14
+ ...props
15
+ }: AspectRatioProps) {
16
+ const aspect = Number.isFinite(ratio) && ratio > 0 ? ratio : 4 / 3;
17
+ return (
18
+ <div
19
+ {...props}
20
+ className={cn(
21
+ "relative w-full overflow-hidden [&>img]:size-full [&>img]:object-cover [&>video]:size-full [&>video]:object-cover",
22
+ className,
23
+ )}
24
+ style={{ aspectRatio: aspect, ...style }}
25
+ />
26
+ );
22
27
  }
@@ -1,67 +1,73 @@
1
- import type { ReactNode } from "react"
2
- import { cn } from "../../lib/class-names"
1
+ import type { ReactNode } from "react";
2
+ import { cn } from "../../lib/class-names";
3
3
 
4
- export type BreadcrumbSize = "sm" | "md"
4
+ export type BreadcrumbSize = "sm" | "md";
5
5
 
6
6
  const sizes: Record<BreadcrumbSize, string> = {
7
- sm: "text-caption",
8
- md: "text-footnote",
9
- }
7
+ sm: "text-caption",
8
+ md: "text-footnote",
9
+ };
10
10
 
11
11
  export interface BreadcrumbItem {
12
- label: string
13
- href?: string
12
+ label: string;
13
+ href?: string;
14
14
  }
15
15
  export interface BreadcrumbProps {
16
- label: string
17
- items: readonly BreadcrumbItem[]
18
- className?: string
19
- /** Integrate a client router without nesting interactive elements. */
20
- renderLink?: (item: BreadcrumbItem & { href: string }) => ReactNode
21
- /** Text scale. @default "md" */
22
- size?: BreadcrumbSize
16
+ label: string;
17
+ items: readonly BreadcrumbItem[];
18
+ className?: string;
19
+ /** Integrate a client router without nesting interactive elements. */
20
+ renderLink?: (item: BreadcrumbItem & { href: string }) => ReactNode;
21
+ /** Text scale. @default "md" */
22
+ size?: BreadcrumbSize;
23
23
  }
24
24
  /** Ancestors stay links; the final item identifies the current page. */
25
- export function Breadcrumb({ label, items, className, renderLink, size = "md" }: BreadcrumbProps) {
26
- return (
27
- <nav aria-label={label} className={cn("not-typeset", className)}>
28
- <ol
29
- className={cn(
30
- "m-0 flex list-none flex-wrap items-center gap-x-2 gap-y-1 p-0 text-label-secondary",
31
- sizes[size]
32
- )}
33
- >
34
- {items.map((item, index) => (
35
- <li
36
- key={`${index}-${item.label}`}
37
- className="m-0 flex min-w-0 items-center gap-2 p-0"
38
- >
39
- {index > 0 && (
40
- <span aria-hidden="true" className="text-label-tertiary">
41
- /
42
- </span>
43
- )}
44
- {index === items.length - 1 ? (
45
- <span aria-current="page" className="font-medium text-label">
46
- {item.label}
47
- </span>
48
- ) : item.href ? (
49
- (renderLink?.({ ...item, href: item.href }) ?? (
50
- <a
51
- href={item.href}
52
- className={cn(
53
- "rounded-sm hover:text-label focus-visible:outline-2 focus-visible:outline-accent-text"
54
- )}
55
- >
56
- {item.label}
57
- </a>
58
- ))
59
- ) : (
60
- <span>{item.label}</span>
61
- )}
62
- </li>
63
- ))}
64
- </ol>
65
- </nav>
66
- )
25
+ export function Breadcrumb({
26
+ label,
27
+ items,
28
+ className,
29
+ renderLink,
30
+ size = "md",
31
+ }: BreadcrumbProps) {
32
+ return (
33
+ <nav aria-label={label} className={cn("not-typeset", className)}>
34
+ <ol
35
+ className={cn(
36
+ "text-label-secondary m-0 flex list-none flex-wrap items-center gap-x-2 gap-y-1 p-0",
37
+ sizes[size],
38
+ )}
39
+ >
40
+ {items.map((item, index) => (
41
+ <li
42
+ key={item.label}
43
+ className="m-0 flex min-w-0 items-center gap-2 p-0"
44
+ >
45
+ {index > 0 && (
46
+ <span aria-hidden="true" className="text-label-tertiary">
47
+ /
48
+ </span>
49
+ )}
50
+ {index === items.length - 1 ? (
51
+ <span aria-current="page" className="text-label font-medium">
52
+ {item.label}
53
+ </span>
54
+ ) : item.href ? (
55
+ (renderLink?.({ ...item, href: item.href }) ?? (
56
+ <a
57
+ href={item.href}
58
+ className={cn(
59
+ "hover:text-label focus-visible:outline-accent-text rounded-sm focus-visible:outline-2",
60
+ )}
61
+ >
62
+ {item.label}
63
+ </a>
64
+ ))
65
+ ) : (
66
+ <span>{item.label}</span>
67
+ )}
68
+ </li>
69
+ ))}
70
+ </ol>
71
+ </nav>
72
+ );
67
73
  }
@@ -1,42 +1,49 @@
1
1
  export type ButtonVariant =
2
- | "default"
3
- | "destructive"
4
- | "outline"
5
- | "secondary"
6
- | "ghost"
7
- | "link"
8
- | "accent"
2
+ | "default"
3
+ | "destructive"
4
+ | "outline"
5
+ | "secondary"
6
+ | "ghost"
7
+ | "link"
8
+ | "accent";
9
9
 
10
10
  /** Icon-only buttons use the same sizing contract as Button. */
11
11
  export const iconButtonSizes = {
12
- icon: "size-11 p-0 rounded-full",
13
- "icon-xs": "size-8 p-0 rounded-full",
14
- "icon-sm": "size-9 p-0 rounded-full",
15
- "icon-lg": "size-12 p-0 rounded-full",
16
- } as const
12
+ icon: "size-11 p-0 rounded-full",
13
+ "icon-xs": "size-8 p-0 rounded-full",
14
+ "icon-sm": "size-9 p-0 rounded-full",
15
+ "icon-lg": "size-12 p-0 rounded-full",
16
+ } as const;
17
17
 
18
- export type IconButtonSize = keyof typeof iconButtonSizes
18
+ export type IconButtonSize = keyof typeof iconButtonSizes;
19
19
 
20
- export type ButtonSize = "default" | "xs" | "sm" | "lg" | "icon-full" | IconButtonSize
20
+ export type ButtonSize =
21
+ | "default"
22
+ | "xs"
23
+ | "sm"
24
+ | "lg"
25
+ | "icon-full"
26
+ | IconButtonSize;
21
27
 
22
28
  export const buttonVariants: Record<ButtonVariant, string> = {
23
- default:
24
- "bg-inverse text-on-inverse shadow-[inset_0_1px_0_rgb(255_255_255/0.14),0_1px_2px_rgb(0_0_0/0.18),0_6px_16px_-6px_rgb(0_0_0/0.35)] hover:brightness-[1.12]",
25
- destructive:
26
- "bg-danger text-background shadow-[inset_0_1px_0_rgb(255_255_255/0.14),0_1px_2px_rgb(0_0_0/0.18),0_6px_16px_-6px_rgb(0_0_0/0.35)] hover:brightness-[1.08]",
27
- outline: "border border-separator bg-transparent text-label hover:bg-fill/60",
28
- secondary:
29
- "bg-elevated text-label shadow-[inset_0_0_0_0.5px_color-mix(in_srgb,var(--color-separator)_55%,transparent),0_1px_2px_rgb(40_30_0/0.06)] hover:bg-fill/60",
30
- ghost: "bg-transparent text-label hover:bg-fill/80",
31
- link: "bg-transparent text-accent-text underline-offset-4 hover:underline",
32
- accent: "bg-accent text-on-accent ring-1 ring-accent-text/25 shadow-[inset_0_1px_0_rgb(255_255_255/0.35),0_1px_2px_rgb(60_40_0/0.16),0_6px_16px_-6px_color-mix(in_srgb,var(--color-accent)_70%,transparent)] hover:brightness-[1.04]",
33
- }
29
+ default:
30
+ "bg-inverse text-on-inverse shadow-[inset_0_1px_0_rgb(255_255_255/0.14),0_1px_2px_rgb(0_0_0/0.18),0_6px_16px_-6px_rgb(0_0_0/0.35)] hover:brightness-[1.12]",
31
+ destructive:
32
+ "bg-danger text-background shadow-[inset_0_1px_0_rgb(255_255_255/0.14),0_1px_2px_rgb(0_0_0/0.18),0_6px_16px_-6px_rgb(0_0_0/0.35)] hover:brightness-[1.08]",
33
+ outline: "border border-separator bg-transparent text-label hover:bg-fill/60",
34
+ secondary:
35
+ "bg-elevated text-label shadow-[inset_0_0_0_0.5px_color-mix(in_srgb,var(--color-separator)_55%,transparent),0_1px_2px_rgb(40_30_0/0.06)] hover:bg-fill/60",
36
+ ghost: "bg-transparent text-label hover:bg-fill/80",
37
+ link: "bg-transparent text-accent-text underline-offset-4 hover:underline",
38
+ accent:
39
+ "bg-accent text-on-accent ring-1 ring-accent-text/25 shadow-[inset_0_1px_0_rgb(255_255_255/0.35),0_1px_2px_rgb(60_40_0/0.16),0_6px_16px_-6px_color-mix(in_srgb,var(--color-accent)_70%,transparent)] hover:brightness-[1.04]",
40
+ };
34
41
 
35
42
  export const buttonSizes: Record<ButtonSize, string> = {
36
- default: "h-11 px-5 text-subheadline rounded-full",
37
- xs: "h-8 px-3 text-caption rounded-full",
38
- sm: "h-9 px-4 text-subheadline rounded-full",
39
- lg: "h-12 px-6 text-body rounded-full",
40
- ...iconButtonSizes,
41
- "icon-full": "h-11 w-full px-5 text-subheadline rounded-full",
42
- }
43
+ default: "h-11 px-5 text-subheadline rounded-full",
44
+ xs: "h-8 px-3 text-caption rounded-full",
45
+ sm: "h-9 px-4 text-subheadline rounded-full",
46
+ lg: "h-12 px-6 text-body rounded-full",
47
+ ...iconButtonSizes,
48
+ "icon-full": "h-11 w-full px-5 text-subheadline rounded-full",
49
+ };
@@ -1,100 +1,107 @@
1
- import { type ButtonHTMLAttributes, forwardRef } from "react"
2
- import { cn } from "../../lib/class-names"
3
- import { PendingSpinner } from "./pending-spinner"
4
- import { usePendingIndicator } from "./use-pending-indicator"
5
- import { buttonSizes, buttonVariants, type ButtonSize, type ButtonVariant } from "./button-styles"
1
+ import { type ButtonHTMLAttributes, forwardRef } from "react";
2
+ import { cn } from "../../lib/class-names";
3
+ import { PendingSpinner } from "./pending-spinner";
4
+ import { usePendingIndicator } from "./use-pending-indicator";
5
+ import {
6
+ buttonSizes,
7
+ buttonVariants,
8
+ type ButtonSize,
9
+ type ButtonVariant,
10
+ } from "./button-styles";
6
11
 
7
- export type { ButtonSize, ButtonVariant }
12
+ export type { ButtonSize, ButtonVariant };
8
13
 
9
14
  /**
10
15
  * Each variant has a little depth: a fine highlight along the top edge and
11
16
  * a soft, layered shadow, like a well-made physical key.
12
17
  */
13
18
  export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
14
- /**
15
- * Visual emphasis: `default` (solid), `accent` (main action),
16
- * `secondary` (quiet fill), `outline`, `ghost`, `link`, or `destructive`.
17
- * @default "default"
18
- */
19
- variant?: ButtonVariant
20
- /**
21
- * Label sizes: `xs`, `sm`, `default`, `lg`.
22
- * Icon sizes: `icon-xs`, `icon-sm`, `icon`, `icon-lg`, `icon-full`.
23
- * Prefer IconButton for an icon with a built-in accessible label.
24
- * @default "default"
25
- */
26
- size?: ButtonSize
27
- /** Shows a delayed spinner, keeps the button's width, and prevents repeat presses.
28
- * @default false
29
- */
30
- isPending?: boolean
31
- /** Layout classes for the content wrapper, such as a full-width disclosure row. */
32
- contentClassName?: string
19
+ /**
20
+ * Visual emphasis: `default` (solid), `accent` (main action),
21
+ * `secondary` (quiet fill), `outline`, `ghost`, `link`, or `destructive`.
22
+ * @default "default"
23
+ */
24
+ variant?: ButtonVariant;
25
+ /**
26
+ * Label sizes: `xs`, `sm`, `default`, `lg`.
27
+ * Icon sizes: `icon-xs`, `icon-sm`, `icon`, `icon-lg`, `icon-full`.
28
+ * Prefer IconButton for an icon with a built-in accessible label.
29
+ * @default "default"
30
+ */
31
+ size?: ButtonSize;
32
+ /** Shows a delayed spinner, keeps the button's width, and prevents repeat presses.
33
+ * @default false
34
+ */
35
+ isPending?: boolean;
36
+ /** Layout classes for the content wrapper, such as a full-width disclosure row. */
37
+ contentClassName?: string;
33
38
  }
34
39
 
35
40
  /** Shares native button appearance with semantic links without nesting a button. */
36
41
  export function buttonStyle({
37
- variant = "default",
38
- size = "default",
39
- className,
42
+ variant = "default",
43
+ size = "default",
44
+ className,
40
45
  }: Pick<ButtonProps, "variant" | "size" | "className"> = {}) {
41
- return cn(
42
- "group relative inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap overflow-hidden font-semibold tracking-[-0.005em]",
43
- "transition-[background-color,filter,transform,box-shadow] duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] active:scale-[0.98]",
44
- "motion-reduce:transform-none motion-reduce:transition-none disabled:pointer-events-none disabled:opacity-40",
45
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/70 focus-visible:ring-offset-1 focus-visible:ring-offset-background",
46
- buttonSizes[size],
47
- buttonVariants[variant],
48
- className
49
- )
46
+ return cn(
47
+ "group relative inline-flex shrink-0 items-center justify-center gap-2 overflow-hidden font-semibold tracking-[-0.005em] whitespace-nowrap select-none",
48
+ "transition-[background-color,filter,transform,box-shadow] duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] active:scale-[0.98]",
49
+ "disabled:pointer-events-none disabled:opacity-40 motion-reduce:transform-none motion-reduce:transition-none",
50
+ "focus-visible:ring-accent/70 focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:outline-none",
51
+ buttonSizes[size],
52
+ buttonVariants[variant],
53
+ className,
54
+ );
50
55
  }
51
56
 
52
57
  /** A native button with Passui styling, pending feedback, and an HTMLButtonElement ref. */
53
- export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
54
- {
55
- variant = "default",
56
- size = "default",
57
- isPending = false,
58
- contentClassName,
59
- disabled,
60
- children,
61
- className,
62
- type = "button",
63
- ...props
64
- },
65
- ref
66
- ) {
67
- const showPendingIndicator = usePendingIndicator(isPending)
58
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
59
+ function Button(
60
+ {
61
+ variant = "default",
62
+ size = "default",
63
+ isPending = false,
64
+ contentClassName,
65
+ disabled,
66
+ children,
67
+ className,
68
+ type = "button",
69
+ ...props
70
+ },
71
+ ref,
72
+ ) {
73
+ const showPendingIndicator = usePendingIndicator(isPending);
68
74
 
69
- return (
70
- <button
71
- ref={ref}
72
- type={type}
73
- className={buttonStyle({
74
- variant,
75
- size,
76
- // pending is busy, not unavailable: keep full colour
77
- className: cn(
78
- "data-pending:cursor-progress data-pending:disabled:opacity-100",
79
- className
80
- ),
81
- })}
82
- {...props}
83
- data-pending={isPending || undefined}
84
- disabled={disabled || isPending}
85
- aria-busy={isPending || props["aria-busy"]}
86
- >
87
- {/* the label keeps its space, so the width never jumps */}
88
- <span
89
- className={cn(
90
- "inline-flex min-w-0 items-center justify-center gap-2 truncate transition-opacity duration-200",
91
- showPendingIndicator && "opacity-0",
92
- contentClassName
93
- )}
94
- >
95
- {children}
96
- </span>
97
- {showPendingIndicator && <PendingSpinner />}
98
- </button>
99
- )
100
- })
75
+ return (
76
+ <button
77
+ ref={ref}
78
+ type={type}
79
+ className={buttonStyle({
80
+ variant,
81
+ size,
82
+ // pending is busy, not unavailable: keep full colour
83
+ className: cn(
84
+ "data-pending:cursor-progress data-pending:disabled:opacity-100",
85
+ className,
86
+ ),
87
+ })}
88
+ {...props}
89
+ data-pending={isPending || undefined}
90
+ disabled={disabled || isPending}
91
+ aria-busy={isPending || props["aria-busy"]}
92
+ >
93
+ {/* the label keeps its space, so the width never jumps */}
94
+ <span
95
+ className={cn(
96
+ "inline-flex min-w-0 items-center justify-center gap-2 truncate transition-opacity duration-200",
97
+ showPendingIndicator && "opacity-0",
98
+ contentClassName,
99
+ )}
100
+ >
101
+ {children}
102
+ </span>
103
+ {showPendingIndicator && <PendingSpinner />}
104
+ </button>
105
+ );
106
+ },
107
+ );