@vendure-io/ui 1.0.5 → 1.2.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 +49 -0
- package/package.json +3 -2
- package/src/components/custom/multi-select.tsx +176 -0
- package/src/components/ui/popover.tsx +5 -0
- package/src/lib/base-ui.ts +53 -0
package/README.md
CHANGED
|
@@ -29,8 +29,57 @@ import { cn } from "@vendure-io/ui/lib/utils";
|
|
|
29
29
|
| `@vendure-io/ui/components/ui/*` | shadcn/ui primitives (button, dialog, input, etc.) |
|
|
30
30
|
| `@vendure-io/ui/components/custom/*` | Vendure-specific components |
|
|
31
31
|
| `@vendure-io/ui/lib/*` | Utilities (`cn`, etc.) |
|
|
32
|
+
| `@vendure-io/ui/lib/base-ui` | `@base-ui/react` primitive namespaces (see below) |
|
|
32
33
|
| `@vendure-io/ui/hooks/*` | Shared React hooks |
|
|
33
34
|
|
|
35
|
+
## Customizing wrapper components
|
|
36
|
+
|
|
37
|
+
The components under `components/ui/*` are thin wrappers around [`@base-ui/react`](https://base-ui.com/) primitives. To override a single subcomponent (e.g. swap `DialogTitle` for one with a different font) while re-using the rest, import the underlying primitive namespace from `@vendure-io/ui/lib/base-ui` rather than from `@base-ui/react` directly:
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
// In your own dialog.tsx
|
|
41
|
+
import { DialogPrimitive } from "@vendure-io/ui/lib/base-ui";
|
|
42
|
+
import {
|
|
43
|
+
Dialog,
|
|
44
|
+
DialogClose,
|
|
45
|
+
DialogContent,
|
|
46
|
+
DialogDescription,
|
|
47
|
+
DialogFooter,
|
|
48
|
+
DialogHeader,
|
|
49
|
+
DialogOverlay,
|
|
50
|
+
DialogPortal,
|
|
51
|
+
DialogTrigger,
|
|
52
|
+
} from "@vendure-io/ui/components/ui/dialog";
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
Dialog,
|
|
56
|
+
DialogClose,
|
|
57
|
+
DialogContent,
|
|
58
|
+
DialogDescription,
|
|
59
|
+
DialogFooter,
|
|
60
|
+
DialogHeader,
|
|
61
|
+
DialogOverlay,
|
|
62
|
+
DialogPortal,
|
|
63
|
+
DialogTrigger,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
|
|
67
|
+
return (
|
|
68
|
+
<DialogPrimitive.Title
|
|
69
|
+
data-slot="dialog-title"
|
|
70
|
+
className={cn("font-display text-lg", className)}
|
|
71
|
+
{...props}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Each primitive is exposed as a TypeScript namespace, so you get both the runtime component (`<DialogPrimitive.Title />`) and its prop types (`DialogPrimitive.Title.Props`) from the same import. This means you don't need `@base-ui/react` as a direct dependency in your project — `@vendure-io/ui` is the single source of truth for both wrappers and primitives.
|
|
78
|
+
|
|
79
|
+
Available primitives include `AccordionPrimitive`, `AlertDialogPrimitive`, `AvatarPrimitive`, `ButtonPrimitive`, `CheckboxPrimitive`, `CollapsiblePrimitive`, `ComboboxPrimitive`, `ContextMenuPrimitive`, `DialogPrimitive`, `InputPrimitive`, `MenuPrimitive`, `MenubarPrimitive`, `NavigationMenuPrimitive`, `PopoverPrimitive`, `PreviewCardPrimitive`, `ProgressPrimitive`, `RadioPrimitive`, `RadioGroupPrimitive`, `ScrollAreaPrimitive`, `SelectPrimitive`, `SeparatorPrimitive`, `SliderPrimitive`, `SwitchPrimitive`, `TabsPrimitive`, `TogglePrimitive`, `ToggleGroupPrimitive`, `TooltipPrimitive`, plus the `mergeProps` and `useRender` helpers.
|
|
80
|
+
|
|
81
|
+
> Where two wrappers share the same base-ui primitive (e.g. both `dialog` and `sheet` are built on `Dialog`; both `dropdown-menu` and `menubar` on `Menu`), the primitive is exposed under its canonical base-ui name only.
|
|
82
|
+
|
|
34
83
|
## License
|
|
35
84
|
|
|
36
85
|
See [LICENSE.md](../../LICENSE.md) for details.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vendure-io/ui",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.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",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"directory": "packages/ui"
|
|
11
11
|
},
|
|
12
12
|
"type": "module",
|
|
13
|
+
"sideEffects": false,
|
|
13
14
|
"exports": {
|
|
14
15
|
"./components/ui/*": "./src/components/ui/*.tsx",
|
|
15
16
|
"./components/custom/*": "./src/components/custom/*.tsx",
|
|
@@ -44,7 +45,7 @@
|
|
|
44
45
|
},
|
|
45
46
|
"dependencies": {
|
|
46
47
|
"@base-ui/react": "^1.2.0",
|
|
47
|
-
"@vendure-io/design-tokens": "^1.
|
|
48
|
+
"@vendure-io/design-tokens": "^1.2.0",
|
|
48
49
|
"class-variance-authority": "^0.7.1",
|
|
49
50
|
"clsx": "^2.1.1",
|
|
50
51
|
"cmdk": "^1.1.1",
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Select,
|
|
5
|
+
SelectContent,
|
|
6
|
+
SelectGroup,
|
|
7
|
+
SelectItem,
|
|
8
|
+
SelectTrigger,
|
|
9
|
+
SelectValue,
|
|
10
|
+
} from '@vendure-io/ui/components/ui/select';
|
|
11
|
+
import { cn } from '@vendure-io/ui/lib/utils';
|
|
12
|
+
import * as React from 'react';
|
|
13
|
+
|
|
14
|
+
type ItemValue = string | number;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Describes how to read a piece of data off an item. Pass a property key — only
|
|
18
|
+
* keys whose value is assignable to `R` are allowed — or a function for full control.
|
|
19
|
+
*/
|
|
20
|
+
type AccessorKey<T, R> = { [K in keyof T]-?: T[K] extends R ? K : never }[keyof T];
|
|
21
|
+
type Accessor<T, R> = AccessorKey<T, R> | ((item: T) => R);
|
|
22
|
+
|
|
23
|
+
function resolveAccessor<T, R>(item: T, accessor: Accessor<T, R>): R {
|
|
24
|
+
return typeof accessor === 'function' ? accessor(item) : (item[accessor] as R);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface MultiSelectProps<T> {
|
|
28
|
+
/** The list of options to choose from. */
|
|
29
|
+
items: readonly T[];
|
|
30
|
+
/**
|
|
31
|
+
* How to render each option's label. Pass a property key for the common case
|
|
32
|
+
* (`itemToLabel="name"`), or a function for full control
|
|
33
|
+
* (`itemToLabel={(item) => <Flag code={item.code} />}`).
|
|
34
|
+
* @default String(item)
|
|
35
|
+
*/
|
|
36
|
+
itemToLabel?: Accessor<T, React.ReactNode>;
|
|
37
|
+
/**
|
|
38
|
+
* How to derive the stable value stored in state for each option. Pass a
|
|
39
|
+
* property key (`itemToValue="id"`) or a function. Required when items are
|
|
40
|
+
* objects; for string/number items it defaults to the item itself.
|
|
41
|
+
*/
|
|
42
|
+
itemToValue?: Accessor<T, ItemValue>;
|
|
43
|
+
/** Selected values. Use for controlled usage. */
|
|
44
|
+
value?: readonly ItemValue[];
|
|
45
|
+
/** Initially selected values for uncontrolled usage. */
|
|
46
|
+
defaultValue?: readonly ItemValue[];
|
|
47
|
+
/** Called with the next array of selected values whenever the selection changes. */
|
|
48
|
+
onValueChange?: (value: ItemValue[]) => void;
|
|
49
|
+
/** Text shown in the trigger when nothing is selected. */
|
|
50
|
+
placeholder?: React.ReactNode;
|
|
51
|
+
/**
|
|
52
|
+
* Override how the selected options are summarised in the trigger. Defaults
|
|
53
|
+
* to the selected labels joined with commas. Provide this when `itemToLabel`
|
|
54
|
+
* renders multi-line content, so the trigger stays a single, non-growing line.
|
|
55
|
+
*/
|
|
56
|
+
renderValue?: (selectedItems: T[]) => React.ReactNode;
|
|
57
|
+
/** Disable the entire control. */
|
|
58
|
+
disabled?: boolean;
|
|
59
|
+
/** Render the error state (sets `aria-invalid` on the trigger). */
|
|
60
|
+
invalid?: boolean;
|
|
61
|
+
/** Trigger height. */
|
|
62
|
+
size?: 'sm' | 'default';
|
|
63
|
+
/** Class applied to the trigger. Defaults to a full-width, non-growing trigger; pass e.g. `w-[260px]` to fix the width. */
|
|
64
|
+
className?: string;
|
|
65
|
+
/** Class applied to the dropdown content. */
|
|
66
|
+
contentClassName?: string;
|
|
67
|
+
/** Form field name for the hidden inputs base-ui renders for each value. */
|
|
68
|
+
name?: string;
|
|
69
|
+
/** Whether a selection is required for form submission. */
|
|
70
|
+
required?: boolean;
|
|
71
|
+
/** id forwarded to the trigger. */
|
|
72
|
+
id?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function MultiSelect<T>({
|
|
76
|
+
items,
|
|
77
|
+
itemToLabel = String as unknown as Accessor<T, React.ReactNode>,
|
|
78
|
+
itemToValue,
|
|
79
|
+
value,
|
|
80
|
+
defaultValue,
|
|
81
|
+
onValueChange,
|
|
82
|
+
placeholder = 'Select…',
|
|
83
|
+
renderValue,
|
|
84
|
+
disabled,
|
|
85
|
+
invalid,
|
|
86
|
+
size = 'default',
|
|
87
|
+
className,
|
|
88
|
+
contentClassName,
|
|
89
|
+
name,
|
|
90
|
+
required,
|
|
91
|
+
id,
|
|
92
|
+
}: MultiSelectProps<T>) {
|
|
93
|
+
const getValue = React.useCallback(
|
|
94
|
+
(item: T): ItemValue =>
|
|
95
|
+
itemToValue == null ? (item as unknown as ItemValue) : resolveAccessor(item, itemToValue),
|
|
96
|
+
[itemToValue],
|
|
97
|
+
);
|
|
98
|
+
const getLabel = React.useCallback(
|
|
99
|
+
(item: T): React.ReactNode => resolveAccessor(item, itemToLabel),
|
|
100
|
+
[itemToLabel],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const itemByValue = React.useMemo(() => {
|
|
104
|
+
const map = new Map<ItemValue, T>();
|
|
105
|
+
for (const item of items) {
|
|
106
|
+
const itemValue = getValue(item);
|
|
107
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
108
|
+
if (typeof itemValue !== 'string' && typeof itemValue !== 'number') {
|
|
109
|
+
console.warn(
|
|
110
|
+
'MultiSelect: `itemToValue` must resolve to a string or number. When `items` are ' +
|
|
111
|
+
'objects, pass `itemToValue` (e.g. itemToValue="id") so the selection survives the ' +
|
|
112
|
+
'items array being recreated.',
|
|
113
|
+
);
|
|
114
|
+
} else if (map.has(itemValue)) {
|
|
115
|
+
console.warn(
|
|
116
|
+
`MultiSelect: duplicate item value ${JSON.stringify(itemValue)} — each item must ` +
|
|
117
|
+
'resolve to a unique `itemToValue`; later items override earlier ones.',
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
map.set(itemValue, item);
|
|
122
|
+
}
|
|
123
|
+
return map;
|
|
124
|
+
}, [items, getValue]);
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<Select
|
|
128
|
+
multiple
|
|
129
|
+
value={value as ItemValue[] | undefined}
|
|
130
|
+
defaultValue={defaultValue as ItemValue[] | undefined}
|
|
131
|
+
onValueChange={onValueChange}
|
|
132
|
+
disabled={disabled}
|
|
133
|
+
name={name}
|
|
134
|
+
required={required}
|
|
135
|
+
>
|
|
136
|
+
{/* [vendure] default to a stable, container-width trigger so it doesn't grow with the
|
|
137
|
+
selection — the underlying SelectTrigger is `w-fit`, which we override here. */}
|
|
138
|
+
<SelectTrigger
|
|
139
|
+
id={id}
|
|
140
|
+
size={size}
|
|
141
|
+
className={cn('w-full', className)}
|
|
142
|
+
aria-invalid={invalid || undefined}
|
|
143
|
+
>
|
|
144
|
+
<SelectValue placeholder={placeholder}>
|
|
145
|
+
{(selected: ItemValue[]) => {
|
|
146
|
+
const selectedItems = selected
|
|
147
|
+
.map((v) => itemByValue.get(v))
|
|
148
|
+
.filter((item): item is T => item !== undefined);
|
|
149
|
+
if (selectedItems.length === 0) return placeholder;
|
|
150
|
+
if (renderValue) return renderValue(selectedItems);
|
|
151
|
+
return selectedItems.map((item, index) => (
|
|
152
|
+
<React.Fragment key={getValue(item)}>
|
|
153
|
+
{index > 0 ? ', ' : null}
|
|
154
|
+
{getLabel(item)}
|
|
155
|
+
</React.Fragment>
|
|
156
|
+
));
|
|
157
|
+
}}
|
|
158
|
+
</SelectValue>
|
|
159
|
+
</SelectTrigger>
|
|
160
|
+
<SelectContent className={contentClassName}>
|
|
161
|
+
<SelectGroup>
|
|
162
|
+
{items.map((item) => {
|
|
163
|
+
const itemValue = getValue(item);
|
|
164
|
+
return (
|
|
165
|
+
<SelectItem key={itemValue} value={itemValue}>
|
|
166
|
+
{getLabel(item)}
|
|
167
|
+
</SelectItem>
|
|
168
|
+
);
|
|
169
|
+
})}
|
|
170
|
+
</SelectGroup>
|
|
171
|
+
</SelectContent>
|
|
172
|
+
</Select>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export { MultiSelect };
|
|
@@ -9,6 +9,10 @@ function Popover({ ...props }: PopoverPrimitive.Root.Props) {
|
|
|
9
9
|
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
function PopoverPortal({ ...props }: PopoverPrimitive.Portal.Props) {
|
|
13
|
+
return <PopoverPrimitive.Portal data-slot="popover-portal" {...props} />
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) {
|
|
13
17
|
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
|
14
18
|
}
|
|
@@ -82,6 +86,7 @@ function PopoverDescription({
|
|
|
82
86
|
|
|
83
87
|
export {
|
|
84
88
|
Popover,
|
|
89
|
+
PopoverPortal,
|
|
85
90
|
PopoverContent,
|
|
86
91
|
PopoverDescription,
|
|
87
92
|
PopoverHeader,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-exports of `@base-ui/react` primitive namespaces.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: the wrapper components in `@vendure-io/ui/components/ui/*`
|
|
5
|
+
* are intentionally thin layers over `@base-ui/react` primitives. Consumers
|
|
6
|
+
* (notably `@vendure/dashboard`) often need to override a single subcomponent
|
|
7
|
+
* — e.g. swap `DialogTitle` for one that uses a different font — while
|
|
8
|
+
* re-exporting the rest of the wrapper. To do that they need access to the
|
|
9
|
+
* underlying primitive's types and components (e.g. `Dialog.Title.Props`).
|
|
10
|
+
*
|
|
11
|
+
* Exposing the primitives here means consumers can drop `@base-ui/react` as
|
|
12
|
+
* a direct dependency entirely — `@vendure-io/ui` becomes the single source
|
|
13
|
+
* of truth for both the wrapper and the primitive substrate.
|
|
14
|
+
*
|
|
15
|
+
* Each primitive is a TS namespace, so both values (`<DialogPrimitive.Title />`)
|
|
16
|
+
* and types (`DialogPrimitive.Title.Props`) are available from the same
|
|
17
|
+
* import.
|
|
18
|
+
*
|
|
19
|
+
* Where two wrapper files share a base-ui primitive (e.g. `Dialog` is used
|
|
20
|
+
* by both `dialog.tsx` and `sheet.tsx`; `Menu` by `dropdown-menu.tsx` and
|
|
21
|
+
* `menubar.tsx`), we expose the canonical base-ui name only — they resolve
|
|
22
|
+
* to the same underlying type.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export { Accordion as AccordionPrimitive } from '@base-ui/react/accordion';
|
|
26
|
+
export { AlertDialog as AlertDialogPrimitive } from '@base-ui/react/alert-dialog';
|
|
27
|
+
export { Avatar as AvatarPrimitive } from '@base-ui/react/avatar';
|
|
28
|
+
export { Button as ButtonPrimitive } from '@base-ui/react/button';
|
|
29
|
+
export { Checkbox as CheckboxPrimitive } from '@base-ui/react/checkbox';
|
|
30
|
+
export { Collapsible as CollapsiblePrimitive } from '@base-ui/react/collapsible';
|
|
31
|
+
export { Combobox as ComboboxPrimitive } from '@base-ui/react/combobox';
|
|
32
|
+
export { ContextMenu as ContextMenuPrimitive } from '@base-ui/react/context-menu';
|
|
33
|
+
export { Dialog as DialogPrimitive } from '@base-ui/react/dialog';
|
|
34
|
+
export { Input as InputPrimitive } from '@base-ui/react/input';
|
|
35
|
+
export { Menu as MenuPrimitive } from '@base-ui/react/menu';
|
|
36
|
+
export { Menubar as MenubarPrimitive } from '@base-ui/react/menubar';
|
|
37
|
+
export { mergeProps } from '@base-ui/react/merge-props';
|
|
38
|
+
export { NavigationMenu as NavigationMenuPrimitive } from '@base-ui/react/navigation-menu';
|
|
39
|
+
export { Popover as PopoverPrimitive } from '@base-ui/react/popover';
|
|
40
|
+
export { PreviewCard as PreviewCardPrimitive } from '@base-ui/react/preview-card';
|
|
41
|
+
export { Progress as ProgressPrimitive } from '@base-ui/react/progress';
|
|
42
|
+
export { Radio as RadioPrimitive } from '@base-ui/react/radio';
|
|
43
|
+
export { RadioGroup as RadioGroupPrimitive } from '@base-ui/react/radio-group';
|
|
44
|
+
export { ScrollArea as ScrollAreaPrimitive } from '@base-ui/react/scroll-area';
|
|
45
|
+
export { Select as SelectPrimitive } from '@base-ui/react/select';
|
|
46
|
+
export { Separator as SeparatorPrimitive } from '@base-ui/react/separator';
|
|
47
|
+
export { Slider as SliderPrimitive } from '@base-ui/react/slider';
|
|
48
|
+
export { Switch as SwitchPrimitive } from '@base-ui/react/switch';
|
|
49
|
+
export { Tabs as TabsPrimitive } from '@base-ui/react/tabs';
|
|
50
|
+
export { Toggle as TogglePrimitive } from '@base-ui/react/toggle';
|
|
51
|
+
export { ToggleGroup as ToggleGroupPrimitive } from '@base-ui/react/toggle-group';
|
|
52
|
+
export { Tooltip as TooltipPrimitive } from '@base-ui/react/tooltip';
|
|
53
|
+
export { useRender } from '@base-ui/react/use-render';
|