@vendure-io/ui 1.2.0 → 1.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vendure-io/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "React component library for Vendure, built on shadcn/ui and Tailwind v4",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"homepage": "https://github.com/vendurehq/design/tree/main/packages/ui",
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Autocomplete } from '@base-ui/react/autocomplete';
|
|
4
|
+
import { ComboboxContent, ComboboxItem, ComboboxList } from '@vendure-io/ui/components/ui/combobox';
|
|
5
|
+
import {
|
|
6
|
+
InputGroup,
|
|
7
|
+
InputGroupAddon,
|
|
8
|
+
InputGroupInput,
|
|
9
|
+
} from '@vendure-io/ui/components/ui/input-group';
|
|
10
|
+
import { Spinner } from '@vendure-io/ui/components/ui/spinner';
|
|
11
|
+
import { cn } from '@vendure-io/ui/lib/utils';
|
|
12
|
+
import * as React from 'react';
|
|
13
|
+
|
|
14
|
+
export interface ComboboxFreeTextItem {
|
|
15
|
+
/**
|
|
16
|
+
* The string committed to `value` when this row is chosen. Must be unique
|
|
17
|
+
* across `items`: it is used as the React key and to resolve a picked row back
|
|
18
|
+
* to its record for `onSelectItem`.
|
|
19
|
+
*/
|
|
20
|
+
value: string;
|
|
21
|
+
/** Primary line of the suggestion row. */
|
|
22
|
+
label: string;
|
|
23
|
+
/** Optional secondary line. */
|
|
24
|
+
description?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ComboboxFreeTextProps<T extends ComboboxFreeTextItem = ComboboxFreeTextItem> {
|
|
28
|
+
/** The input text — the source of truth. Free-form: it need not match any item. */
|
|
29
|
+
value: string;
|
|
30
|
+
/**
|
|
31
|
+
* Fires on every keystroke and when a suggestion is chosen (with the chosen
|
|
32
|
+
* item's `value`). Always a string — never the item object, because most
|
|
33
|
+
* fires (keystrokes, free text) have no item behind them.
|
|
34
|
+
*/
|
|
35
|
+
onValueChange: (value: string) => void;
|
|
36
|
+
/**
|
|
37
|
+
* Fires only when a suggestion is chosen (by click or keyboard) — never on a
|
|
38
|
+
* keystroke or free-text entry, since those have no underlying record. Hands
|
|
39
|
+
* back the full item, so attach extra fields (e.g. an `id`) to your `items`
|
|
40
|
+
* and read them here.
|
|
41
|
+
*/
|
|
42
|
+
onSelectItem?: (item: T) => void;
|
|
43
|
+
/**
|
|
44
|
+
* Suggestions for the current input. The caller fetches these — typically
|
|
45
|
+
* debounced against `value` — and they are assumed to be already filtered
|
|
46
|
+
* (the component does no client-side filtering). Items may carry extra fields
|
|
47
|
+
* beyond `value`/`label`/`description`; those flow through to `onSelectItem`.
|
|
48
|
+
*/
|
|
49
|
+
items: readonly T[];
|
|
50
|
+
/** Async state of the suggestion source. Shows a trailing spinner while true. */
|
|
51
|
+
loading?: boolean;
|
|
52
|
+
/** Render a suggestion row. Defaults to `label` over `description`. */
|
|
53
|
+
renderItem?: (item: T) => React.ReactNode;
|
|
54
|
+
/** id forwarded to the input. */
|
|
55
|
+
id?: string;
|
|
56
|
+
placeholder?: string;
|
|
57
|
+
/** Render the error state (sets `aria-invalid` on the input). */
|
|
58
|
+
invalid?: boolean;
|
|
59
|
+
disabled?: boolean;
|
|
60
|
+
/** Class applied to the input container. */
|
|
61
|
+
className?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function defaultRenderItem(item: ComboboxFreeTextItem): React.ReactNode {
|
|
65
|
+
return (
|
|
66
|
+
<div className="flex min-w-0 flex-col">
|
|
67
|
+
<span className="truncate">{item.label}</span>
|
|
68
|
+
{item.description ? (
|
|
69
|
+
<span className="text-muted-foreground truncate text-xs">{item.description}</span>
|
|
70
|
+
) : null}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A text input with debounced, server-driven suggestions where free text wins:
|
|
77
|
+
* pick a suggestion to commit its value, or keep typing and your text is the
|
|
78
|
+
* value. A choice is never forced — Enter, blur and no-match all keep the text.
|
|
79
|
+
*
|
|
80
|
+
* Built on Base UI's `Autocomplete` (the free-text primitive — `selectionMode`
|
|
81
|
+
* is fixed to `none` and the input value is the source of truth), not the
|
|
82
|
+
* pick-only `Combobox`. `mode="none"` keeps suggestions static (no client-side
|
|
83
|
+
* filtering, since the caller pre-filters server-side) and disables
|
|
84
|
+
* inline-autocompletion. Fetching, debounce and validation live with the caller.
|
|
85
|
+
*/
|
|
86
|
+
function ComboboxFreeText<T extends ComboboxFreeTextItem = ComboboxFreeTextItem>({
|
|
87
|
+
value,
|
|
88
|
+
onValueChange,
|
|
89
|
+
onSelectItem,
|
|
90
|
+
items,
|
|
91
|
+
loading = false,
|
|
92
|
+
renderItem = defaultRenderItem,
|
|
93
|
+
id,
|
|
94
|
+
placeholder,
|
|
95
|
+
invalid,
|
|
96
|
+
disabled,
|
|
97
|
+
className,
|
|
98
|
+
}: ComboboxFreeTextProps<T>) {
|
|
99
|
+
// Base UI opens the popup on every keystroke; we only want it open when there
|
|
100
|
+
// is something pickable, so a no-match closes it silently instead of flashing
|
|
101
|
+
// an empty box. `open` tracks the user's intent (typing, Escape, blur,
|
|
102
|
+
// select) and we AND it with "items present" to derive the rendered state.
|
|
103
|
+
const [open, setOpen] = React.useState(false);
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<Autocomplete.Root
|
|
107
|
+
mode="none"
|
|
108
|
+
autoHighlight={false}
|
|
109
|
+
value={value}
|
|
110
|
+
onValueChange={(next, details) => {
|
|
111
|
+
// Call onValueChange before onSelectItem so a record captured in the
|
|
112
|
+
// latter survives any reset the value handler performs.
|
|
113
|
+
onValueChange(next);
|
|
114
|
+
// `item-press` is the only reason a row selection (mouse or keyboard)
|
|
115
|
+
// drives a value change; keystrokes and free text use other reasons.
|
|
116
|
+
// The literal is checked against Base UI's public `reason` union, so a
|
|
117
|
+
// future rename surfaces as a type error rather than silent breakage.
|
|
118
|
+
if (onSelectItem && details.reason === 'item-press') {
|
|
119
|
+
const picked = items.find((i) => i.value === next);
|
|
120
|
+
if (picked) {
|
|
121
|
+
onSelectItem(picked);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}}
|
|
125
|
+
open={open && items.length > 0}
|
|
126
|
+
onOpenChange={setOpen}
|
|
127
|
+
disabled={disabled}
|
|
128
|
+
>
|
|
129
|
+
<InputGroup className={cn('w-full', className)}>
|
|
130
|
+
<Autocomplete.Input
|
|
131
|
+
render={<InputGroupInput />}
|
|
132
|
+
id={id}
|
|
133
|
+
placeholder={placeholder}
|
|
134
|
+
disabled={disabled}
|
|
135
|
+
aria-invalid={invalid || undefined}
|
|
136
|
+
/>
|
|
137
|
+
{loading ? (
|
|
138
|
+
<InputGroupAddon align="inline-end">
|
|
139
|
+
<Spinner />
|
|
140
|
+
</InputGroupAddon>
|
|
141
|
+
) : null}
|
|
142
|
+
</InputGroup>
|
|
143
|
+
<ComboboxContent>
|
|
144
|
+
<ComboboxList>
|
|
145
|
+
{items.map((item) => (
|
|
146
|
+
<ComboboxItem key={item.value} value={item.value}>
|
|
147
|
+
{renderItem(item)}
|
|
148
|
+
</ComboboxItem>
|
|
149
|
+
))}
|
|
150
|
+
</ComboboxList>
|
|
151
|
+
</ComboboxContent>
|
|
152
|
+
</Autocomplete.Root>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export { ComboboxFreeText };
|
package/src/lib/base-ui.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
export { Accordion as AccordionPrimitive } from '@base-ui/react/accordion';
|
|
26
26
|
export { AlertDialog as AlertDialogPrimitive } from '@base-ui/react/alert-dialog';
|
|
27
|
+
export { Autocomplete as AutocompletePrimitive } from '@base-ui/react/autocomplete';
|
|
27
28
|
export { Avatar as AvatarPrimitive } from '@base-ui/react/avatar';
|
|
28
29
|
export { Button as ButtonPrimitive } from '@base-ui/react/button';
|
|
29
30
|
export { Checkbox as CheckboxPrimitive } from '@base-ui/react/checkbox';
|