@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 +9 -9
- package/package.json +23 -23
- package/src/components/agent-icon.tsx +16 -16
- package/src/components/autocomplete.tsx +166 -162
- package/src/components/badge.tsx +46 -45
- package/src/components/button.tsx +56 -63
- package/src/components/combobox.tsx +233 -228
- package/src/components/command.tsx +135 -139
- package/src/components/dialog.tsx +106 -98
- package/src/components/dropdown-menu.tsx +118 -115
- package/src/components/github-background.tsx +214 -216
- package/src/components/halftone-background.tsx +164 -155
- package/src/components/halftone-config.ts +8 -8
- package/src/components/input-group.tsx +149 -147
- package/src/components/input.tsx +17 -17
- package/src/components/live-duration.tsx +70 -64
- package/src/components/logo.tsx +9 -13
- package/src/components/popover.tsx +60 -62
- package/src/components/resize-handle.tsx +73 -75
- package/src/components/sheet.tsx +120 -112
- package/src/components/textarea.tsx +15 -15
- package/src/components/time-ago.tsx +64 -62
- package/src/components/tooltip.tsx +48 -42
- package/src/foundation/styles.css +6 -4
- package/src/index.ts +21 -21
package/README.md
CHANGED
|
@@ -74,15 +74,15 @@ const Example = () => (
|
|
|
74
74
|
|
|
75
75
|
## Key Concepts
|
|
76
76
|
|
|
77
|
-
| Concept
|
|
78
|
-
|
|
|
79
|
-
| Form & action
|
|
80
|
-
| Overlays
|
|
81
|
-
| Command palette
|
|
82
|
-
| Branding & background
|
|
83
|
-
| Live time
|
|
84
|
-
| Layout
|
|
85
|
-
| Class utility
|
|
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
|
-
"
|
|
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
|
-
"
|
|
8
|
+
"effect",
|
|
15
9
|
"radix",
|
|
16
|
-
"
|
|
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
|
-
"
|
|
25
|
-
"
|
|
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
|
-
"
|
|
37
|
-
"
|
|
38
|
-
|
|
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
|
-
"
|
|
70
|
-
"
|
|
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
|
|
1
|
+
import type * as React from 'react'
|
|
2
2
|
|
|
3
3
|
interface AgentIconPropsExternalApi {
|
|
4
|
-
|
|
4
|
+
className?: string
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
export function AgentIcon({ className }: AgentIconPropsExternalApi): React.JSX.Element {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
readonly value: string
|
|
10
|
+
readonly label?: string
|
|
11
|
+
readonly description?: string
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export interface AutocompletePropsExternalApi {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
+
const showEmpty = !loading && query !== '' && filtered.length === 0
|
|
109
|
+
const showList = open && (loading || filtered.length > 0 || showEmpty)
|
|
108
110
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
}
|
package/src/components/badge.tsx
CHANGED
|
@@ -1,57 +1,58 @@
|
|
|
1
|
-
import * as React from
|
|
2
|
-
import { cva, type VariantProps } from
|
|
3
|
-
import type { ClassProp } from
|
|
4
|
-
import { Slot } from
|
|
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
|
|
6
|
+
import { cn } from '../foundation/cn'
|
|
7
7
|
|
|
8
8
|
type BadgeVariantsExternalApi = (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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<
|
|
37
|
-
|
|
37
|
+
type BadgePropsExternalApi = React.ComponentProps<'span'> &
|
|
38
|
+
VariantProps<typeof badgeVariants> & { asChild?: boolean }
|
|
38
39
|
|
|
39
40
|
function Badge({
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
className,
|
|
42
|
+
variant = 'default',
|
|
43
|
+
asChild = false,
|
|
44
|
+
...props
|
|
44
45
|
}: BadgePropsExternalApi): React.JSX.Element {
|
|
45
|
-
|
|
46
|
+
const Comp = asChild ? Slot.Root : 'span'
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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 }
|