@playfast/reform-better-hub-ui-primitives 0.2.1 → 0.3.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/README.md CHANGED
@@ -74,15 +74,15 @@ const Example = () => (
74
74
 
75
75
  ## Key Concepts
76
76
 
77
- | Concept | What it is |
78
- | ---------------------- | --------------------------------------------------------------------------------------------- |
79
- | Form & action | `Button` (+ `buttonVariants`), `Input`, `Textarea`, `InputGroup`, `Badge` (+ `badgeVariants`) |
80
- | Overlays | `Dialog`, `Sheet`, `Popover`, `Tooltip`, `DropdownMenu` (Radix-backed, compound parts) |
81
- | Command palette | `Command` (cmdk) + `Combobox` |
82
- | Branding & background | `Logo` / `LogoMark`, `AgentIcon`, `GitHubBackground`, `HalftoneBackground` |
83
- | Live time | `LiveDuration`, `TimeAgo` (self-ticking displays) |
84
- | Layout | `ResizeHandle` |
85
- | Class utility | `cn(...)` — `clsx` + `tailwind-merge` |
77
+ | Concept | What it is |
78
+ | --------------------- | --------------------------------------------------------------------------------------------- |
79
+ | Form & action | `Button` (+ `buttonVariants`), `Input`, `Textarea`, `InputGroup`, `Badge` (+ `badgeVariants`) |
80
+ | Overlays | `Dialog`, `Sheet`, `Popover`, `Tooltip`, `DropdownMenu` (Radix-backed, compound parts) |
81
+ | Command palette | `Command` (cmdk) + `Combobox` |
82
+ | Branding & background | `Logo` / `LogoMark`, `AgentIcon`, `GitHubBackground`, `HalftoneBackground` |
83
+ | Live time | `LiveDuration`, `TimeAgo` (self-ticking displays) |
84
+ | Layout | `ResizeHandle` |
85
+ | Class utility | `cn(...)` — `clsx` + `tailwind-merge` |
86
86
 
87
87
  Each overlay follows the Radix compound pattern (`Dialog` + `DialogTrigger` +
88
88
  `DialogContent`, etc.); `variant`/`size` props are driven by
package/package.json CHANGED
@@ -1,29 +1,32 @@
1
1
  {
2
2
  "name": "@playfast/reform-better-hub-ui-primitives",
3
- "playbook": "./playbook",
4
- "version": "0.2.1",
5
- "type": "module",
3
+ "version": "0.3.0",
6
4
  "description": "React UI primitives adapted from better-hub by Better Auth (https://github.com/better-auth/better-hub). Full credit to the Better Auth team.",
7
5
  "keywords": [
8
- "reform",
9
- "effect",
10
- "react",
11
- "ui",
12
6
  "components",
13
7
  "design-system",
14
- "shadcn",
8
+ "effect",
15
9
  "radix",
16
- "tailwind"
10
+ "react",
11
+ "reform",
12
+ "shadcn",
13
+ "tailwind",
14
+ "ui"
17
15
  ],
16
+ "bugs": {
17
+ "url": "https://github.com/playfast/reform/issues"
18
+ },
18
19
  "license": "MIT",
19
20
  "repository": {
20
21
  "type": "git",
21
22
  "url": "https://github.com/playfast/reform.git",
22
23
  "directory": "packages/better-hub-ui-primitives"
23
24
  },
24
- "bugs": {
25
- "url": "https://github.com/playfast/reform/issues"
26
- },
25
+ "files": [
26
+ "src",
27
+ "README.md"
28
+ ],
29
+ "type": "module",
27
30
  "sideEffects": [
28
31
  "**/*.css"
29
32
  ],
@@ -33,10 +36,9 @@
33
36
  "./styles.css": "./src/foundation/styles.css",
34
37
  "./*": "./src/*.tsx"
35
38
  },
36
- "files": [
37
- "src",
38
- "README.md"
39
- ],
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
40
42
  "scripts": {
41
43
  "clean": "rm -rf dist .tsbuildinfo",
42
44
  "check": "tsc --noEmit",
@@ -47,10 +49,6 @@
47
49
  "lint": "oxlint src",
48
50
  "lint:fix": "oxlint --fix src"
49
51
  },
50
- "peerDependencies": {
51
- "react": "^19.0.0",
52
- "react-dom": "^19.0.0"
53
- },
54
52
  "dependencies": {
55
53
  "@base-ui/react": "^1.3.0",
56
54
  "@radix-ui/react-dialog": "^1.1.15",
@@ -66,7 +64,9 @@
66
64
  "radix-ui": "^1.4.3",
67
65
  "tailwind-merge": "^3.4.1"
68
66
  },
69
- "publishConfig": {
70
- "access": "public"
71
- }
67
+ "peerDependencies": {
68
+ "react": "^19.0.0",
69
+ "react-dom": "^19.0.0"
70
+ },
71
+ "playbook": "./playbook"
72
72
  }
@@ -1,25 +1,22 @@
1
- // Adapted from better-hub by Better Auth — https://github.com/better-auth/better-hub
2
- // (apps/web/src/components/ui/agent-icon.tsx). Full credit to the Better Auth team.
3
-
4
- import type * as React from "react";
1
+ import type * as React from 'react'
5
2
 
6
3
  interface AgentIconPropsExternalApi {
7
- className?: string;
4
+ className?: string
8
5
  }
9
6
 
10
7
  export function AgentIcon({ className }: AgentIconPropsExternalApi): React.JSX.Element {
11
- return (
12
- <svg
13
- xmlns="http://www.w3.org/2000/svg"
14
- width="1em"
15
- height="1em"
16
- viewBox="0 0 28 28"
17
- className={className}
18
- >
19
- <path
20
- fill="currentColor"
21
- d="M10.06 18.701a1.628 1.628 0 0 0 2.43-.676l.77-2.34a3.82 3.82 0 0 1 2.416-2.418l2.238-.727a1.6 1.6 0 0 0 .786-.595a1.62 1.62 0 0 0-.849-2.489l-2.215-.72a3.82 3.82 0 0 1-2.42-2.414l-.727-2.237a1.62 1.62 0 0 0-.594-.785a1.655 1.655 0 0 0-1.879 0a1.63 1.63 0 0 0-.6.8L8.68 6.365a3.82 3.82 0 0 1-2.359 2.37l-2.24.726a1.626 1.626 0 0 0 .02 3.073l2.216.72a3.86 3.86 0 0 1 1.816 1.286c.266.343.471.728.606 1.14l.728 2.234c.112.318.32.593.595.787m-3.273-6.873l-2.22-.72s-.085-.038-.085-.108a.14.14 0 0 1 .084-.115L6.8 10.16a5.34 5.34 0 0 0 3.32-3.375l.721-2.219s.043-.084.114-.084s.113.084.113.084l.723 2.223a5.32 5.32 0 0 0 3.377 3.372l2.27.735a.13.13 0 0 1 .078.11a.15.15 0 0 1-.083.113l-2.225.722a5.32 5.32 0 0 0-3.374 3.374l-.715 2.2a.11.11 0 0 1-.115.093c-.104 0-.118-.075-.118-.075l-.723-2.22a5.32 5.32 0 0 0-3.376-3.385m13.017 12.954A1.2 1.2 0 0 0 20.5 25a1.2 1.2 0 0 0 .692-.216a1.2 1.2 0 0 0 .446-.6l.372-1.143a1.61 1.61 0 0 1 1.017-1.02l1.166-.378A1.21 1.21 0 0 0 25 20.505a1.2 1.2 0 0 0-.844-1.146l-1.144-.37a1.61 1.61 0 0 1-1.02-1.018l-.38-1.163a1.2 1.2 0 0 0-2.274.016l-.374 1.146a1.61 1.61 0 0 1-.993 1.017l-1.166.378a1.21 1.21 0 0 0-.807 1.139a1.2 1.2 0 0 0 .823 1.138l1.144.372a1.6 1.6 0 0 1 1.02 1.023l.379 1.163a1.2 1.2 0 0 0 .44.582m-1.371-4.194l-.27-.088l.283-.096a3.12 3.12 0 0 0 1.943-1.974l.088-.27l.089.272a3.11 3.11 0 0 0 1.974 1.975l.293.1l-.271.087a3.12 3.12 0 0 0-1.974 1.977l-.088.27l-.088-.272a3.12 3.12 0 0 0-1.98-1.98"
22
- />
23
- </svg>
24
- );
8
+ return (
9
+ <svg
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ width="1em"
12
+ height="1em"
13
+ viewBox="0 0 28 28"
14
+ className={className}
15
+ >
16
+ <path
17
+ fill="currentColor"
18
+ d="M10.06 18.701a1.628 1.628 0 0 0 2.43-.676l.77-2.34a3.82 3.82 0 0 1 2.416-2.418l2.238-.727a1.6 1.6 0 0 0 .786-.595a1.62 1.62 0 0 0-.849-2.489l-2.215-.72a3.82 3.82 0 0 1-2.42-2.414l-.727-2.237a1.62 1.62 0 0 0-.594-.785a1.655 1.655 0 0 0-1.879 0a1.63 1.63 0 0 0-.6.8L8.68 6.365a3.82 3.82 0 0 1-2.359 2.37l-2.24.726a1.626 1.626 0 0 0 .02 3.073l2.216.72a3.86 3.86 0 0 1 1.816 1.286c.266.343.471.728.606 1.14l.728 2.234c.112.318.32.593.595.787m-3.273-6.873l-2.22-.72s-.085-.038-.085-.108a.14.14 0 0 1 .084-.115L6.8 10.16a5.34 5.34 0 0 0 3.32-3.375l.721-2.219s.043-.084.114-.084s.113.084.113.084l.723 2.223a5.32 5.32 0 0 0 3.377 3.372l2.27.735a.13.13 0 0 1 .078.11a.15.15 0 0 1-.083.113l-2.225.722a5.32 5.32 0 0 0-3.374 3.374l-.715 2.2a.11.11 0 0 1-.115.093c-.104 0-.118-.075-.118-.075l-.723-2.22a5.32 5.32 0 0 0-3.376-3.385m13.017 12.954A1.2 1.2 0 0 0 20.5 25a1.2 1.2 0 0 0 .692-.216a1.2 1.2 0 0 0 .446-.6l.372-1.143a1.61 1.61 0 0 1 1.017-1.02l1.166-.378A1.21 1.21 0 0 0 25 20.505a1.2 1.2 0 0 0-.844-1.146l-1.144-.37a1.61 1.61 0 0 1-1.02-1.018l-.38-1.163a1.2 1.2 0 0 0-2.274.016l-.374 1.146a1.61 1.61 0 0 1-.993 1.017l-1.166.378a1.21 1.21 0 0 0-.807 1.139a1.2 1.2 0 0 0 .823 1.138l1.144.372a1.6 1.6 0 0 1 1.02 1.023l.379 1.163a1.2 1.2 0 0 0 .44.582m-1.371-4.194l-.27-.088l.283-.096a3.12 3.12 0 0 0 1.943-1.974l.088-.27l.089.272a3.11 3.11 0 0 0 1.974 1.975l.293.1l-.271.087a3.12 3.12 0 0 0-1.974 1.977l-.088.27l-.088-.272a3.12 3.12 0 0 0-1.98-1.98"
19
+ />
20
+ </svg>
21
+ )
25
22
  }
@@ -0,0 +1,193 @@
1
+ import * as React from 'react'
2
+ import { String as Str } from 'effect'
3
+ import { CheckIcon, ChevronDownIcon, Loader2Icon } from 'lucide-react'
4
+
5
+ import { cn } from '../foundation/cn'
6
+ import { Input } from './input'
7
+
8
+ export interface AutocompleteItemExternalApi {
9
+ readonly value: string
10
+ readonly label?: string
11
+ readonly description?: string
12
+ }
13
+
14
+ export interface AutocompletePropsExternalApi {
15
+ readonly value: string
16
+ readonly onValueChange: (next: string) => void
17
+ readonly items: ReadonlyArray<AutocompleteItemExternalApi>
18
+ readonly placeholder?: string
19
+ readonly 'aria-label': string
20
+ readonly loading?: boolean
21
+ readonly loadingText?: string
22
+ readonly emptyText?: string
23
+ readonly disabled?: boolean
24
+ readonly className?: string
25
+ }
26
+
27
+ // blur close is delayed so suggestion mousedown (which blurs the input) still lands
28
+ const BLUR_CLOSE_DELAY_MS = 120
29
+
30
+ const matchesQuery = (suggestion: AutocompleteItemExternalApi, needle: string): boolean =>
31
+ [suggestion.value, suggestion.label, suggestion.description].some(
32
+ (text) => text !== undefined && Str.toLowerCase(text).includes(needle),
33
+ )
34
+
35
+ export function Autocomplete({
36
+ value: fieldValue,
37
+ onValueChange,
38
+ items: suggestions,
39
+ placeholder,
40
+ 'aria-label': ariaLabel,
41
+ loading = false,
42
+ loadingText = 'Loading…',
43
+ emptyText = 'No matches — the typed value is used as-is.',
44
+ disabled = false,
45
+ className,
46
+ }: AutocompletePropsExternalApi): React.JSX.Element {
47
+ const [open, setOpen] = React.useState(false)
48
+ // filter only what was typed since open (empty right after open ⇒ full list)
49
+ const [query, setQuery] = React.useState('')
50
+ const [highlighted, setHighlighted] = React.useState(-1)
51
+ const listId = React.useId()
52
+
53
+ const needle = Str.toLowerCase(query)
54
+ const filtered =
55
+ query === ''
56
+ ? suggestions
57
+ : suggestions.filter((suggestion) => matchesQuery(suggestion, needle))
58
+ const activeIndex = highlighted >= 0 && highlighted < filtered.length ? highlighted : -1
59
+ const optionId = (index: number): string => `${listId}-option-${index}`
60
+
61
+ React.useEffect(() => {
62
+ if (activeIndex >= 0) {
63
+ document.getElementById(optionId(activeIndex))?.scrollIntoView({ block: 'nearest' })
64
+ }
65
+ // oxlint-disable-next-line react-hooks/exhaustive-deps -- optionId is stable per listId
66
+ }, [activeIndex])
67
+
68
+ const openList = (): void => {
69
+ setQuery('')
70
+ setHighlighted(-1)
71
+ setOpen(true)
72
+ }
73
+ const pick = (suggestion: AutocompleteItemExternalApi): void => {
74
+ onValueChange(suggestion.value)
75
+ setOpen(false)
76
+ }
77
+
78
+ const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
79
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
80
+ event.preventDefault()
81
+ if (!open) {
82
+ openList()
83
+ return
84
+ }
85
+ if (filtered.length > 0) {
86
+ setHighlighted(
87
+ event.key === 'ArrowDown'
88
+ ? (activeIndex + 1) % filtered.length
89
+ : (activeIndex <= 0 ? filtered.length : activeIndex) - 1,
90
+ )
91
+ }
92
+ return
93
+ }
94
+ if (event.key === 'Enter') {
95
+ if (open && activeIndex >= 0) {
96
+ event.preventDefault()
97
+ pick(filtered[activeIndex]!)
98
+ } else {
99
+ setOpen(false)
100
+ }
101
+ return
102
+ }
103
+ if (event.key === 'Escape' || event.key === 'Tab') {
104
+ setOpen(false)
105
+ }
106
+ }
107
+
108
+ const showEmpty = !loading && query !== '' && filtered.length === 0
109
+ const showList = open && (loading || filtered.length > 0 || showEmpty)
110
+
111
+ return (
112
+ <div className={cn('relative', className)}>
113
+ <Input
114
+ value={fieldValue}
115
+ onChange={(event) => {
116
+ onValueChange(event.target.value)
117
+ setQuery(event.target.value)
118
+ setHighlighted(-1)
119
+ setOpen(true)
120
+ }}
121
+ onFocus={openList}
122
+ // oxlint-disable-next-line reform-rules/no-set-timeout-interval -- DOM blur defer; Effect.sleep can't gate a React event handler
123
+ onBlur={() => globalThis.setTimeout(() => setOpen(false), BLUR_CLOSE_DELAY_MS)}
124
+ onKeyDown={onKeyDown}
125
+ placeholder={placeholder}
126
+ disabled={disabled}
127
+ role="combobox"
128
+ aria-label={ariaLabel}
129
+ aria-expanded={showList}
130
+ aria-controls={listId}
131
+ aria-activedescendant={activeIndex >= 0 ? optionId(activeIndex) : undefined}
132
+ aria-autocomplete="list"
133
+ autoComplete="off"
134
+ spellCheck={false}
135
+ className="pr-8"
136
+ />
137
+ <span className="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground">
138
+ {loading ? (
139
+ <Loader2Icon className="size-4 animate-spin" />
140
+ ) : (
141
+ <ChevronDownIcon className="size-4" />
142
+ )}
143
+ </span>
144
+ {showList && (
145
+ <div
146
+ id={listId}
147
+ role="listbox"
148
+ aria-label={`${ariaLabel} suggestions`}
149
+ className="absolute left-0 right-0 top-full z-50 mt-1 flex max-h-60 flex-col overflow-y-auto rounded-md border bg-card p-1 text-card-foreground shadow-md"
150
+ >
151
+ {loading && (
152
+ <span className="flex items-center gap-2 px-2 py-1.5 text-xs text-muted-foreground">
153
+ <Loader2Icon className="size-3 animate-spin" />
154
+ {loadingText}
155
+ </span>
156
+ )}
157
+ {filtered.map((suggestion, index) => (
158
+ <button
159
+ key={suggestion.value}
160
+ id={optionId(index)}
161
+ type="button"
162
+ role="option"
163
+ aria-selected={suggestion.value === fieldValue}
164
+ // mousedown fires before blur so the pick lands
165
+ onMouseDown={(event) => {
166
+ event.preventDefault()
167
+ pick(suggestion)
168
+ }}
169
+ onMouseMove={() => setHighlighted(index)}
170
+ className={cn(
171
+ 'flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm',
172
+ index === activeIndex ? 'bg-accent text-accent-foreground' : 'hover:bg-accent/50',
173
+ )}
174
+ >
175
+ <span className="flex min-w-0 flex-1 flex-col">
176
+ <span className="truncate">{suggestion.label ?? suggestion.value}</span>
177
+ {suggestion.description && (
178
+ <span className="truncate text-xs text-muted-foreground">
179
+ {suggestion.description}
180
+ </span>
181
+ )}
182
+ </span>
183
+ {suggestion.value === fieldValue ? <CheckIcon className="size-4 shrink-0" /> : null}
184
+ </button>
185
+ ))}
186
+ {showEmpty && (
187
+ <span className="px-2 py-1.5 text-xs text-muted-foreground">{emptyText}</span>
188
+ )}
189
+ </div>
190
+ )}
191
+ </div>
192
+ )
193
+ }
@@ -1,59 +1,58 @@
1
- // Adapted from better-hub by Better Auth — https://github.com/better-auth/better-hub
2
- // (apps/web/src/components/ui/badge.tsx). Full credit to the Better Auth team.
3
- import * as React from "react";
4
- import { cva, type VariantProps } from "class-variance-authority";
5
- import type { ClassProp } from "class-variance-authority/types";
6
- import { Slot } from "radix-ui";
1
+ import * as React from 'react'
2
+ import { cva, type VariantProps } from 'class-variance-authority'
3
+ import type { ClassProp } from 'class-variance-authority/types'
4
+ import { Slot } from 'radix-ui'
7
5
 
8
- import { cn } from "../foundation/cn";
6
+ import { cn } from '../foundation/cn'
9
7
 
10
8
  type BadgeVariantsExternalApi = (
11
- props?:
12
- | ({
13
- variant?: "default" | "secondary" | "destructive" | "outline" | "ghost" | "link" | undefined;
14
- } & ClassProp)
15
- | undefined,
16
- ) => string;
9
+ props?:
10
+ | ({
11
+ variant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link' | undefined
12
+ } & ClassProp)
13
+ | undefined,
14
+ ) => string
17
15
 
18
16
  const badgeVariants: BadgeVariantsExternalApi = cva(
19
- "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
20
- {
21
- variants: {
22
- variant: {
23
- default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
24
- secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
25
- destructive:
26
- "bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
27
- outline: "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
28
- ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
29
- link: "text-primary underline-offset-4 [a&]:hover:underline",
30
- },
31
- },
32
- defaultVariants: {
33
- variant: "default",
34
- },
35
- },
36
- );
17
+ 'inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3',
18
+ {
19
+ variants: {
20
+ variant: {
21
+ default: 'bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
22
+ secondary: 'bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
23
+ destructive:
24
+ 'bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90',
25
+ outline:
26
+ 'border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
27
+ ghost: '[a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
28
+ link: 'text-primary underline-offset-4 [a&]:hover:underline',
29
+ },
30
+ },
31
+ defaultVariants: {
32
+ variant: 'default',
33
+ },
34
+ },
35
+ )
37
36
 
38
- type BadgePropsExternalApi = React.ComponentProps<"span"> &
39
- VariantProps<typeof badgeVariants> & { asChild?: boolean };
37
+ type BadgePropsExternalApi = React.ComponentProps<'span'> &
38
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }
40
39
 
41
40
  function Badge({
42
- className,
43
- variant = "default",
44
- asChild = false,
45
- ...props
41
+ className,
42
+ variant = 'default',
43
+ asChild = false,
44
+ ...props
46
45
  }: BadgePropsExternalApi): React.JSX.Element {
47
- const Comp = asChild ? Slot.Root : "span";
46
+ const Comp = asChild ? Slot.Root : 'span'
48
47
 
49
- return (
50
- <Comp
51
- data-slot="badge"
52
- data-variant={variant}
53
- className={cn(badgeVariants({ variant }), className)}
54
- {...props}
55
- />
56
- );
48
+ return (
49
+ <Comp
50
+ data-slot="badge"
51
+ data-variant={variant}
52
+ className={cn(badgeVariants({ variant }), className)}
53
+ {...props}
54
+ />
55
+ )
57
56
  }
58
57
 
59
- export { Badge, badgeVariants };
58
+ export { Badge, badgeVariants }
@@ -1,75 +1,66 @@
1
- // Adapted from better-hub by Better Auth — https://github.com/better-auth/better-hub
2
- // (apps/web/src/components/ui/button.tsx). Full credit to the Better Auth team.
3
- import { Slot } from "@radix-ui/react-slot";
4
- import type { VariantProps } from "class-variance-authority";
5
- import { cva } from "class-variance-authority";
6
- import type { ClassProp } from "class-variance-authority/types";
7
- import type * as React from "react";
8
- import { cn } from "../foundation/cn";
1
+ import { Slot } from '@radix-ui/react-slot'
2
+ import type { VariantProps } from 'class-variance-authority'
3
+ import { cva } from 'class-variance-authority'
4
+ import type { ClassProp } from 'class-variance-authority/types'
5
+ import type * as React from 'react'
6
+ import { cn } from '../foundation/cn'
9
7
 
10
8
  type ButtonVariantsExternalApi = (
11
- props?:
12
- | ({
13
- variant?:
14
- | "default"
15
- | "destructive"
16
- | "outline"
17
- | "secondary"
18
- | "ghost"
19
- | "link"
20
- | undefined;
21
- size?: "default" | "sm" | "lg" | "icon" | undefined;
22
- } & ClassProp)
23
- | undefined,
24
- ) => string;
9
+ props?:
10
+ | ({
11
+ variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link' | undefined
12
+ size?: 'default' | 'sm' | 'lg' | 'icon' | undefined
13
+ } & ClassProp)
14
+ | undefined,
15
+ ) => string
25
16
 
26
17
  const buttonVariants: ButtonVariantsExternalApi = cva(
27
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
28
- {
29
- variants: {
30
- variant: {
31
- default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
32
- destructive:
33
- "bg-destructive text-white shadow-xs hover:bg-destructive/90",
34
- outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
35
- secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
36
- ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
37
- link: "text-primary underline-offset-4 hover:underline",
38
- },
39
- size: {
40
- default: "h-9 px-4 py-2 has-[>svg]:px-3",
41
- sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
42
- lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
43
- icon: "size-9",
44
- },
45
- },
46
- defaultVariants: {
47
- variant: "default",
48
- size: "default",
49
- },
50
- },
51
- );
18
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
19
+ {
20
+ variants: {
21
+ variant: {
22
+ default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
23
+ destructive: 'bg-destructive text-white shadow-xs hover:bg-destructive/90',
24
+ outline:
25
+ 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
26
+ secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
27
+ ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
28
+ link: 'text-primary underline-offset-4 hover:underline',
29
+ },
30
+ size: {
31
+ default: 'h-9 px-4 py-2 has-[>svg]:px-3',
32
+ sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
33
+ lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
34
+ icon: 'size-9',
35
+ },
36
+ },
37
+ defaultVariants: {
38
+ variant: 'default',
39
+ size: 'default',
40
+ },
41
+ },
42
+ )
52
43
 
53
- type ButtonPropsExternalApi = React.ComponentProps<"button"> &
54
- VariantProps<typeof buttonVariants> & {
55
- asChild?: boolean;
56
- };
44
+ type ButtonPropsExternalApi = React.ComponentProps<'button'> &
45
+ VariantProps<typeof buttonVariants> & {
46
+ asChild?: boolean
47
+ }
57
48
 
58
49
  function Button({
59
- className,
60
- variant,
61
- size,
62
- asChild = false,
63
- ...props
50
+ className,
51
+ variant,
52
+ size,
53
+ asChild = false,
54
+ ...props
64
55
  }: ButtonPropsExternalApi): React.JSX.Element {
65
- const Comp = asChild ? Slot : "button";
66
- return (
67
- <Comp
68
- data-slot="button"
69
- className={cn(buttonVariants({ variant, size, className }))}
70
- {...props}
71
- />
72
- );
56
+ const Comp = asChild ? Slot : 'button'
57
+ return (
58
+ <Comp
59
+ data-slot="button"
60
+ className={cn(buttonVariants({ variant, size, className }))}
61
+ {...props}
62
+ />
63
+ )
73
64
  }
74
65
 
75
- export { Button, buttonVariants };
66
+ export { Button, buttonVariants }