@spree/dashboard-ui 6.0.0 → 6.0.2
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/package.json +8 -6
- package/src/index.ts +2 -0
- package/src/shadcn.css +640 -0
- package/src/spree/browser-icon.tsx +36 -0
- package/src/spree/collapsible-card.tsx +66 -0
- package/src/spree/icons.ts +3 -0
- package/src/spree/metadata/metadata-card.tsx +140 -35
- package/src/spree/step-icon.tsx +74 -0
- package/src/styles.css +3 -3
- package/src/ui/badge.tsx +3 -0
- package/src/ui/command.tsx +134 -35
- package/src/ui/dropdown-menu.tsx +7 -3
- package/src/ui/field.tsx +1 -1
package/src/ui/command.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Autocomplete as AutocompletePrimitive } from '@base-ui/react'
|
|
2
2
|
import i18n from 'i18next'
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { cn } from '../lib/utils'
|
|
@@ -12,16 +12,67 @@ import {
|
|
|
12
12
|
} from '../spree/icons'
|
|
13
13
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from './dialog'
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
const CommandContext = React.createContext<{
|
|
16
|
+
query: string
|
|
17
|
+
setQuery: (query: string) => void
|
|
18
|
+
shouldFilter: boolean
|
|
19
|
+
onInputChange: React.RefObject<((value: string) => void) | undefined>
|
|
20
|
+
}>({ query: '', setQuery: () => {}, shouldFilter: false, onInputChange: { current: undefined } })
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A search field over a list of actions, on Base UI's Autocomplete rendered
|
|
24
|
+
* inline. The query lives on `CommandInput`. Rows whose `value` or text does not
|
|
25
|
+
* contain the query are hidden unless `shouldFilter` is false.
|
|
26
|
+
*/
|
|
27
|
+
function Command({
|
|
28
|
+
className,
|
|
29
|
+
children,
|
|
30
|
+
shouldFilter = true,
|
|
31
|
+
...props
|
|
32
|
+
}: React.ComponentProps<'div'> & { shouldFilter?: boolean }) {
|
|
33
|
+
const [query, setQuery] = React.useState('')
|
|
34
|
+
const onInputChange = React.useRef<((value: string) => void) | undefined>(undefined)
|
|
35
|
+
|
|
36
|
+
const context = React.useMemo(
|
|
37
|
+
() => ({ query, setQuery, shouldFilter, onInputChange }),
|
|
38
|
+
[query, shouldFilter],
|
|
39
|
+
)
|
|
40
|
+
|
|
16
41
|
return (
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
{
|
|
24
|
-
|
|
42
|
+
<AutocompletePrimitive.Root
|
|
43
|
+
inline
|
|
44
|
+
open
|
|
45
|
+
autoHighlight="always"
|
|
46
|
+
keepHighlight
|
|
47
|
+
filter={null}
|
|
48
|
+
value={query}
|
|
49
|
+
onValueChange={(next, eventDetails) => {
|
|
50
|
+
// Choosing a row would otherwise write that row's value into the field.
|
|
51
|
+
if (eventDetails.reason === 'item-press') return
|
|
52
|
+
setQuery(next)
|
|
53
|
+
onInputChange.current?.(next)
|
|
54
|
+
}}
|
|
55
|
+
onOpenChange={(_open, eventDetails) => {
|
|
56
|
+
// The list is always shown, so Escape belongs to the dialog around it.
|
|
57
|
+
if (eventDetails.reason === 'escape-key') {
|
|
58
|
+
eventDetails.cancel()
|
|
59
|
+
eventDetails.allowPropagation()
|
|
60
|
+
}
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
<CommandContext.Provider value={context}>
|
|
64
|
+
<div
|
|
65
|
+
data-slot="command"
|
|
66
|
+
className={cn(
|
|
67
|
+
'flex size-full flex-col overflow-hidden rounded-2xl! bg-popover text-popover-foreground',
|
|
68
|
+
className,
|
|
69
|
+
)}
|
|
70
|
+
{...props}
|
|
71
|
+
>
|
|
72
|
+
{children}
|
|
73
|
+
</div>
|
|
74
|
+
</CommandContext.Provider>
|
|
75
|
+
</AutocompletePrimitive.Root>
|
|
25
76
|
)
|
|
26
77
|
}
|
|
27
78
|
|
|
@@ -72,18 +123,32 @@ function CommandDialog({
|
|
|
72
123
|
function CommandInput({
|
|
73
124
|
className,
|
|
74
125
|
loading = false,
|
|
126
|
+
value,
|
|
127
|
+
onValueChange,
|
|
75
128
|
...props
|
|
76
|
-
}:
|
|
129
|
+
}: Omit<AutocompletePrimitive.Input.Props, 'value'> & {
|
|
130
|
+
value?: string
|
|
131
|
+
onValueChange?: (value: string) => void
|
|
77
132
|
/** Shows a spinner at the end of the field while results are in flight. */
|
|
78
133
|
loading?: boolean
|
|
79
134
|
}) {
|
|
135
|
+
const { query, setQuery, onInputChange } = React.useContext(CommandContext)
|
|
136
|
+
React.useLayoutEffect(() => {
|
|
137
|
+
onInputChange.current = onValueChange
|
|
138
|
+
}, [onInputChange, onValueChange])
|
|
139
|
+
// A controlled field shows only what its owner accepts: a reset from outside,
|
|
140
|
+
// or the previous text when an edit is rejected.
|
|
141
|
+
React.useLayoutEffect(() => {
|
|
142
|
+
if (value !== undefined && value !== query) setQuery(value)
|
|
143
|
+
}, [value, query, setQuery])
|
|
144
|
+
|
|
80
145
|
return (
|
|
81
146
|
<div
|
|
82
147
|
data-slot="command-input-wrapper"
|
|
83
148
|
className="flex h-14 shrink-0 items-center gap-3 border-b border-border px-5"
|
|
84
149
|
>
|
|
85
150
|
<SearchIcon className="size-5 shrink-0 text-muted-foreground" />
|
|
86
|
-
<
|
|
151
|
+
<AutocompletePrimitive.Input
|
|
87
152
|
data-slot="command-input"
|
|
88
153
|
aria-busy={loading || undefined}
|
|
89
154
|
className={cn(
|
|
@@ -115,12 +180,12 @@ function CommandInput({
|
|
|
115
180
|
* every keystroke. Pass `className="h-…"` to change it, or `h-auto max-h-…`
|
|
116
181
|
* for the shrink-to-fit behaviour a short static menu wants.
|
|
117
182
|
*/
|
|
118
|
-
function CommandList({ className, ...props }:
|
|
183
|
+
function CommandList({ className, ...props }: AutocompletePrimitive.List.Props) {
|
|
119
184
|
return (
|
|
120
|
-
<
|
|
185
|
+
<AutocompletePrimitive.List
|
|
121
186
|
data-slot="command-list"
|
|
122
187
|
className={cn(
|
|
123
|
-
'h-80 min-h-0 scroll-py-2 overflow-x-hidden overflow-y-auto p-2 outline-none',
|
|
188
|
+
'group/command-list h-80 min-h-0 scroll-py-2 overflow-x-hidden overflow-y-auto p-2 outline-none',
|
|
124
189
|
className,
|
|
125
190
|
)}
|
|
126
191
|
{...props}
|
|
@@ -128,14 +193,15 @@ function CommandList({ className, ...props }: React.ComponentProps<typeof Comman
|
|
|
128
193
|
)
|
|
129
194
|
}
|
|
130
195
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
...props
|
|
134
|
-
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
|
196
|
+
/** The message for an empty list. Hidden while the list has any rows. */
|
|
197
|
+
function CommandEmpty({ className, ...props }: React.ComponentProps<'div'>) {
|
|
135
198
|
return (
|
|
136
|
-
<
|
|
199
|
+
<div
|
|
137
200
|
data-slot="command-empty"
|
|
138
|
-
className={cn(
|
|
201
|
+
className={cn(
|
|
202
|
+
'py-10 text-center text-sm text-muted-foreground group-has-[[data-slot=command-item]]/command-list:hidden',
|
|
203
|
+
className,
|
|
204
|
+
)}
|
|
139
205
|
{...props}
|
|
140
206
|
/>
|
|
141
207
|
)
|
|
@@ -143,26 +209,35 @@ function CommandEmpty({
|
|
|
143
209
|
|
|
144
210
|
function CommandGroup({
|
|
145
211
|
className,
|
|
212
|
+
heading,
|
|
213
|
+
children,
|
|
146
214
|
...props
|
|
147
|
-
}: React.
|
|
215
|
+
}: AutocompletePrimitive.Group.Props & { heading?: React.ReactNode }) {
|
|
148
216
|
return (
|
|
149
|
-
<
|
|
217
|
+
<AutocompletePrimitive.Group
|
|
150
218
|
data-slot="command-group"
|
|
151
219
|
className={cn(
|
|
152
|
-
'overflow-hidden text-foreground not-last:pb-2
|
|
220
|
+
'overflow-hidden text-foreground not-last:pb-2 not-has-[[data-slot=command-item]]:hidden',
|
|
153
221
|
className,
|
|
154
222
|
)}
|
|
155
223
|
{...props}
|
|
156
|
-
|
|
224
|
+
>
|
|
225
|
+
{heading && (
|
|
226
|
+
<AutocompletePrimitive.GroupLabel
|
|
227
|
+
data-slot="command-group-heading"
|
|
228
|
+
className="px-3 pt-2 pb-1.5 text-xs font-medium text-muted-foreground"
|
|
229
|
+
>
|
|
230
|
+
{heading}
|
|
231
|
+
</AutocompletePrimitive.GroupLabel>
|
|
232
|
+
)}
|
|
233
|
+
{children}
|
|
234
|
+
</AutocompletePrimitive.Group>
|
|
157
235
|
)
|
|
158
236
|
}
|
|
159
237
|
|
|
160
|
-
function CommandSeparator({
|
|
161
|
-
className,
|
|
162
|
-
...props
|
|
163
|
-
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
|
238
|
+
function CommandSeparator({ className, ...props }: AutocompletePrimitive.Separator.Props) {
|
|
164
239
|
return (
|
|
165
|
-
<
|
|
240
|
+
<AutocompletePrimitive.Separator
|
|
166
241
|
data-slot="command-separator"
|
|
167
242
|
className={cn('-mx-2 my-1 h-px bg-border-subtle', className)}
|
|
168
243
|
{...props}
|
|
@@ -170,23 +245,47 @@ function CommandSeparator({
|
|
|
170
245
|
)
|
|
171
246
|
}
|
|
172
247
|
|
|
248
|
+
/**
|
|
249
|
+
* One row. `onSelect` runs on click and on Enter while the row is highlighted,
|
|
250
|
+
* and receives the row's `value`.
|
|
251
|
+
*/
|
|
173
252
|
function CommandItem({
|
|
174
253
|
className,
|
|
175
254
|
children,
|
|
255
|
+
value,
|
|
256
|
+
keywords,
|
|
257
|
+
onSelect,
|
|
176
258
|
...props
|
|
177
|
-
}:
|
|
259
|
+
}: Omit<AutocompletePrimitive.Item.Props, 'onClick' | 'onSelect' | 'value'> & {
|
|
260
|
+
value?: string
|
|
261
|
+
keywords?: string[]
|
|
262
|
+
onSelect?: (value: string) => void
|
|
263
|
+
}) {
|
|
264
|
+
const { query, shouldFilter } = React.useContext(CommandContext)
|
|
265
|
+
const text = value ?? (typeof children === 'string' ? children : '')
|
|
266
|
+
const needle = query.trim().toLowerCase()
|
|
267
|
+
if (
|
|
268
|
+
shouldFilter &&
|
|
269
|
+
needle &&
|
|
270
|
+
![text, ...(keywords ?? [])].some((term) => term.toLowerCase().includes(needle))
|
|
271
|
+
) {
|
|
272
|
+
return null
|
|
273
|
+
}
|
|
274
|
+
|
|
178
275
|
return (
|
|
179
|
-
<
|
|
276
|
+
<AutocompletePrimitive.Item
|
|
180
277
|
data-slot="command-item"
|
|
278
|
+
value={value}
|
|
279
|
+
onClick={() => onSelect?.(text)}
|
|
181
280
|
className={cn(
|
|
182
|
-
"group/command-item relative flex cursor-pointer items-center gap-3 rounded-md px-3 py-2.5 text-sm outline-hidden select-none data-
|
|
281
|
+
"group/command-item relative flex cursor-pointer items-center gap-3 rounded-md px-3 py-2.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:bg-accent-strong data-highlighted:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-highlighted:*:[svg]:text-foreground",
|
|
183
282
|
className,
|
|
184
283
|
)}
|
|
185
284
|
{...props}
|
|
186
285
|
>
|
|
187
286
|
{children}
|
|
188
287
|
<CheckIcon className="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100" />
|
|
189
|
-
</
|
|
288
|
+
</AutocompletePrimitive.Item>
|
|
190
289
|
)
|
|
191
290
|
}
|
|
192
291
|
|
|
@@ -195,7 +294,7 @@ function CommandShortcut({ className, ...props }: React.ComponentProps<'span'>)
|
|
|
195
294
|
<span
|
|
196
295
|
data-slot="command-shortcut"
|
|
197
296
|
className={cn(
|
|
198
|
-
'ml-auto text-xs tracking-widest text-muted-foreground group-data-
|
|
297
|
+
'ml-auto text-xs tracking-widest text-muted-foreground group-data-highlighted/command-item:text-foreground',
|
|
199
298
|
className,
|
|
200
299
|
)}
|
|
201
300
|
{...props}
|
package/src/ui/dropdown-menu.tsx
CHANGED
|
@@ -160,19 +160,23 @@ function DropdownMenuCheckboxItem({
|
|
|
160
160
|
data-slot="dropdown-menu-checkbox-item"
|
|
161
161
|
data-inset={inset}
|
|
162
162
|
className={cn(
|
|
163
|
-
"relative flex cursor-pointer items-center gap-
|
|
163
|
+
"group/checkbox-item relative flex cursor-pointer items-center gap-2.5 rounded-lg py-1.5 pr-2.5 pl-2.5 text-sm outline-hidden select-none transition-colors duration-75 focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
164
164
|
className,
|
|
165
165
|
)}
|
|
166
166
|
checked={checked}
|
|
167
167
|
onCheckedChange={onCheckedChange ? (checked) => onCheckedChange(checked) : undefined}
|
|
168
168
|
{...props}
|
|
169
169
|
>
|
|
170
|
+
{/* Drawn rather than a <Checkbox>: the item is already the checkbox to
|
|
171
|
+
assistive technology, and a second control inside it would be read
|
|
172
|
+
twice. Styled to match <Checkbox> so a menu and a table agree. */}
|
|
170
173
|
<span
|
|
171
|
-
|
|
174
|
+
aria-hidden
|
|
172
175
|
data-slot="dropdown-menu-checkbox-item-indicator"
|
|
176
|
+
className="pointer-events-none flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-border bg-card text-white shadow-xs transition-colors duration-100 group-data-[checked]/checkbox-item:border-blue-500 group-data-[checked]/checkbox-item:bg-blue-500"
|
|
173
177
|
>
|
|
174
178
|
<MenuPrimitive.CheckboxItemIndicator keepMounted={false}>
|
|
175
|
-
<CheckIcon />
|
|
179
|
+
<CheckIcon className="size-3.5" />
|
|
176
180
|
</MenuPrimitive.CheckboxItemIndicator>
|
|
177
181
|
</span>
|
|
178
182
|
{children}
|
package/src/ui/field.tsx
CHANGED
|
@@ -94,7 +94,7 @@ function FieldLabel({ className, ...props }: React.ComponentProps<typeof Label>)
|
|
|
94
94
|
<Label
|
|
95
95
|
data-slot="field-label"
|
|
96
96
|
className={cn(
|
|
97
|
-
'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-checked:border-blue-
|
|
97
|
+
'group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-checked:border-blue-100 has-data-checked:bg-blue-50 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border *:data-[slot=field]:p-2.5 dark:has-data-checked:border-blue-500/20 dark:has-data-checked:bg-blue-500/10',
|
|
98
98
|
// Card-shaped labels (the ones wrapping a whole `Field`) are clickable
|
|
99
99
|
// targets, so they need the affordance a bare text label doesn't:
|
|
100
100
|
// a pointer and a hover tint. Scoped by `has-[>[data-slot=field]]` so
|