@playfast/reform-better-hub-ui-primitives 0.2.2 → 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.2",
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,22 +1,22 @@
1
- import type * as React from "react";
1
+ import type * as React from 'react'
2
2
 
3
3
  interface AgentIconPropsExternalApi {
4
- className?: string;
4
+ className?: string
5
5
  }
6
6
 
7
7
  export function AgentIcon({ className }: AgentIconPropsExternalApi): React.JSX.Element {
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
- );
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
+ )
22
22
  }
@@ -6,184 +6,188 @@ import { cn } from '../foundation/cn'
6
6
  import { Input } from './input'
7
7
 
8
8
  export interface AutocompleteItemExternalApi {
9
- readonly value: string
10
- readonly label?: string
11
- readonly description?: string
9
+ readonly value: string
10
+ readonly label?: string
11
+ readonly description?: string
12
12
  }
13
13
 
14
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
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
25
  }
26
26
 
27
27
  // blur close is delayed so suggestion mousedown (which blurs the input) still lands
28
28
  const BLUR_CLOSE_DELAY_MS = 120
29
29
 
30
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
- )
31
+ [suggestion.value, suggestion.label, suggestion.description].some(
32
+ (text) => text !== undefined && Str.toLowerCase(text).includes(needle),
33
+ )
34
34
 
35
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,
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
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()
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
52
 
53
- const needle = Str.toLowerCase(query)
54
- const filtered =
55
- query === '' ? suggestions : suggestions.filter((suggestion) => matchesQuery(suggestion, needle))
56
- const activeIndex = highlighted >= 0 && highlighted < filtered.length ? highlighted : -1
57
- const optionId = (index: number): string => `${listId}-option-${index}`
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}`
58
60
 
59
- React.useEffect(() => {
60
- if (activeIndex >= 0) {
61
- document.getElementById(optionId(activeIndex))?.scrollIntoView({ block: 'nearest' })
62
- }
63
- // oxlint-disable-next-line react-hooks/exhaustive-deps -- optionId is stable per listId
64
- }, [activeIndex])
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])
65
67
 
66
- const openList = (): void => {
67
- setQuery('')
68
- setHighlighted(-1)
69
- setOpen(true)
70
- }
71
- const pick = (suggestion: AutocompleteItemExternalApi): void => {
72
- onValueChange(suggestion.value)
73
- setOpen(false)
74
- }
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
+ }
75
77
 
76
- const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
77
- if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
78
- event.preventDefault()
79
- if (!open) {
80
- openList()
81
- return
82
- }
83
- if (filtered.length > 0) {
84
- setHighlighted(
85
- event.key === 'ArrowDown'
86
- ? (activeIndex + 1) % filtered.length
87
- : (activeIndex <= 0 ? filtered.length : activeIndex) - 1,
88
- )
89
- }
90
- return
91
- }
92
- if (event.key === 'Enter') {
93
- if (open && activeIndex >= 0) {
94
- event.preventDefault()
95
- pick(filtered[activeIndex]!)
96
- } else {
97
- setOpen(false)
98
- }
99
- return
100
- }
101
- if (event.key === 'Escape' || event.key === 'Tab') {
102
- setOpen(false)
103
- }
104
- }
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
+ }
105
107
 
106
- const showEmpty = !loading && query !== '' && filtered.length === 0
107
- const showList = open && (loading || filtered.length > 0 || showEmpty)
108
+ const showEmpty = !loading && query !== '' && filtered.length === 0
109
+ const showList = open && (loading || filtered.length > 0 || showEmpty)
108
110
 
109
- return (
110
- <div className={cn('relative', className)}>
111
- <Input
112
- value={fieldValue}
113
- onChange={(event) => {
114
- onValueChange(event.target.value)
115
- setQuery(event.target.value)
116
- setHighlighted(-1)
117
- setOpen(true)
118
- }}
119
- onFocus={openList}
120
- // oxlint-disable-next-line reform-rules/no-set-timeout-interval -- DOM blur defer; Effect.sleep can't gate a React event handler
121
- onBlur={() => globalThis.setTimeout(() => setOpen(false), BLUR_CLOSE_DELAY_MS)}
122
- onKeyDown={onKeyDown}
123
- placeholder={placeholder}
124
- disabled={disabled}
125
- role="combobox"
126
- aria-label={ariaLabel}
127
- aria-expanded={showList}
128
- aria-controls={listId}
129
- aria-activedescendant={activeIndex >= 0 ? optionId(activeIndex) : undefined}
130
- aria-autocomplete="list"
131
- autoComplete="off"
132
- spellCheck={false}
133
- className="pr-8"
134
- />
135
- <span className="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground">
136
- {loading ? (
137
- <Loader2Icon className="size-4 animate-spin" />
138
- ) : (
139
- <ChevronDownIcon className="size-4" />
140
- )}
141
- </span>
142
- {showList && (
143
- <div
144
- id={listId}
145
- role="listbox"
146
- aria-label={`${ariaLabel} suggestions`}
147
- 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"
148
- >
149
- {loading && (
150
- <span className="flex items-center gap-2 px-2 py-1.5 text-xs text-muted-foreground">
151
- <Loader2Icon className="size-3 animate-spin" />
152
- {loadingText}
153
- </span>
154
- )}
155
- {filtered.map((suggestion, index) => (
156
- <button
157
- key={suggestion.value}
158
- id={optionId(index)}
159
- type="button"
160
- role="option"
161
- aria-selected={suggestion.value === fieldValue}
162
- // mousedown fires before blur so the pick lands
163
- onMouseDown={(event) => {
164
- event.preventDefault()
165
- pick(suggestion)
166
- }}
167
- onMouseMove={() => setHighlighted(index)}
168
- className={cn(
169
- 'flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm',
170
- index === activeIndex ? 'bg-accent text-accent-foreground' : 'hover:bg-accent/50',
171
- )}
172
- >
173
- <span className="flex min-w-0 flex-1 flex-col">
174
- <span className="truncate">{suggestion.label ?? suggestion.value}</span>
175
- {suggestion.description ? (
176
- <span className="truncate text-xs text-muted-foreground">{suggestion.description}</span>
177
- ) : null}
178
- </span>
179
- {suggestion.value === fieldValue ? <CheckIcon className="size-4 shrink-0" /> : null}
180
- </button>
181
- ))}
182
- {showEmpty && (
183
- <span className="px-2 py-1.5 text-xs text-muted-foreground">{emptyText}</span>
184
- )}
185
- </div>
186
- )}
187
- </div>
188
- )
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
+ )
189
193
  }
@@ -1,57 +1,58 @@
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";
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'
5
5
 
6
- import { cn } from "../foundation/cn";
6
+ import { cn } from '../foundation/cn'
7
7
 
8
8
  type BadgeVariantsExternalApi = (
9
- props?:
10
- | ({
11
- variant?: "default" | "secondary" | "destructive" | "outline" | "ghost" | "link" | undefined;
12
- } & ClassProp)
13
- | undefined,
14
- ) => string;
9
+ props?:
10
+ | ({
11
+ variant?: 'default' | 'secondary' | 'destructive' | 'outline' | 'ghost' | 'link' | undefined
12
+ } & ClassProp)
13
+ | undefined,
14
+ ) => string
15
15
 
16
16
  const badgeVariants: BadgeVariantsExternalApi = cva(
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: "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
26
- ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
27
- link: "text-primary underline-offset-4 [a&]:hover:underline",
28
- },
29
- },
30
- defaultVariants: {
31
- variant: "default",
32
- },
33
- },
34
- );
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
+ )
35
36
 
36
- type BadgePropsExternalApi = React.ComponentProps<"span"> &
37
- VariantProps<typeof badgeVariants> & { asChild?: boolean };
37
+ type BadgePropsExternalApi = React.ComponentProps<'span'> &
38
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }
38
39
 
39
40
  function Badge({
40
- className,
41
- variant = "default",
42
- asChild = false,
43
- ...props
41
+ className,
42
+ variant = 'default',
43
+ asChild = false,
44
+ ...props
44
45
  }: BadgePropsExternalApi): React.JSX.Element {
45
- const Comp = asChild ? Slot.Root : "span";
46
+ const Comp = asChild ? Slot.Root : 'span'
46
47
 
47
- return (
48
- <Comp
49
- data-slot="badge"
50
- data-variant={variant}
51
- className={cn(badgeVariants({ variant }), className)}
52
- {...props}
53
- />
54
- );
48
+ return (
49
+ <Comp
50
+ data-slot="badge"
51
+ data-variant={variant}
52
+ className={cn(badgeVariants({ variant }), className)}
53
+ {...props}
54
+ />
55
+ )
55
56
  }
56
57
 
57
- export { Badge, badgeVariants };
58
+ export { Badge, badgeVariants }