@a-type/ui 4.1.0-beta.7 → 4.1.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.
Files changed (28) hide show
  1. package/dist/cjs/components/autocomplete/Autocomplete.d.ts +12 -9
  2. package/dist/cjs/components/autocomplete/Autocomplete.js +19 -24
  3. package/dist/cjs/components/autocomplete/Autocomplete.js.map +1 -1
  4. package/dist/cjs/components/combobox/Combobox.d.ts +202 -0
  5. package/dist/cjs/components/combobox/Combobox.js +311 -0
  6. package/dist/cjs/components/combobox/Combobox.js.map +1 -0
  7. package/dist/cjs/components/combobox/Combobox.stories.d.ts +53 -0
  8. package/dist/cjs/components/combobox/Combobox.stories.js +115 -0
  9. package/dist/cjs/components/combobox/Combobox.stories.js.map +1 -0
  10. package/dist/cjs/components/input/Input.js +1 -1
  11. package/dist/cjs/components/input/Input.js.map +1 -1
  12. package/dist/css/main.css +4 -4
  13. package/dist/esm/components/autocomplete/Autocomplete.d.ts +12 -9
  14. package/dist/esm/components/autocomplete/Autocomplete.js +18 -24
  15. package/dist/esm/components/autocomplete/Autocomplete.js.map +1 -1
  16. package/dist/esm/components/combobox/Combobox.d.ts +202 -0
  17. package/dist/esm/components/combobox/Combobox.js +271 -0
  18. package/dist/esm/components/combobox/Combobox.js.map +1 -0
  19. package/dist/esm/components/combobox/Combobox.stories.d.ts +53 -0
  20. package/dist/esm/components/combobox/Combobox.stories.js +112 -0
  21. package/dist/esm/components/combobox/Combobox.stories.js.map +1 -0
  22. package/dist/esm/components/input/Input.js +1 -1
  23. package/dist/esm/components/input/Input.js.map +1 -1
  24. package/package.json +3 -2
  25. package/src/components/autocomplete/Autocomplete.tsx +46 -66
  26. package/src/components/combobox/Combobox.stories.tsx +289 -0
  27. package/src/components/combobox/Combobox.tsx +757 -0
  28. package/src/components/input/Input.tsx +1 -1
@@ -2,7 +2,6 @@ import {
2
2
  AutocompleteArrowProps,
3
3
  Autocomplete as BaseAutocomplete,
4
4
  AutocompleteGroupProps as BaseAutocompleteGroupProps,
5
- AutocompleteInputProps as BaseAutocompleteInputProps,
6
5
  AutocompleteItemProps as BaseAutocompleteItemProps,
7
6
  AutocompleteListProps as BaseAutocompleteListProps,
8
7
  AutocompletePopupProps as BaseAutocompletePopupProps,
@@ -15,36 +14,44 @@ import {
15
14
  ComboboxRowProps as BaseAutocompleteRowProps,
16
15
  } from '@base-ui/react/combobox';
17
16
  import clsx from 'clsx';
18
- import { createContext, ReactNode, useContext } from 'react';
17
+ import { ReactNode } from 'react';
19
18
  import { withClassName } from '../../hooks.js';
20
19
  import { PaletteName } from '../../uno/index.js';
21
20
  import { Button } from '../button/Button.js';
22
21
  import { Chip, ChipProps } from '../chip/Chip.js';
22
+ import {
23
+ comboboxBackdropClassName,
24
+ ComboboxComposedInput,
25
+ comboboxEmptyClassName,
26
+ comboboxGroupClassName,
27
+ comboboxGroupItemClassName,
28
+ comboboxGroupItemListClassName,
29
+ comboboxGroupLabelClassName,
30
+ comboboxIconClassName,
31
+ ComboboxInputProps,
32
+ comboboxListClassName,
33
+ comboboxPopupClassName,
34
+ comboboxRowClassName,
35
+ ComboboxValueContext,
36
+ } from '../combobox/Combobox.js';
23
37
  import { Icon } from '../icon/Icon.js';
24
- import { Input } from '../input/Input.js';
25
38
  import {
26
39
  arrowClassName,
27
40
  itemClassName,
28
- itemListClassName,
29
- popupClassName,
30
41
  separatorClassName,
31
42
  } from '../primitives/menus.js';
32
43
  import { ArrowSvg } from '../utility/ArrowSvg.js';
33
44
  import { SlotDiv } from '../utility/SlotDiv.js';
34
45
 
35
- const ValueContext = createContext<string | number | readonly string[] | null>(
36
- null,
37
- );
38
-
39
46
  const AutocompleteRoot = (props: BaseAutocompleteRootProps<any>) => {
40
47
  return (
41
- <ValueContext.Provider value={props.value || null}>
48
+ <ComboboxValueContext.Provider value={props.value || null}>
42
49
  <BaseAutocomplete.Root {...props} />
43
- </ValueContext.Provider>
50
+ </ComboboxValueContext.Provider>
44
51
  );
45
52
  };
46
53
 
47
- export interface AutocompleteInputProps extends BaseAutocompleteInputProps {
54
+ export interface AutocompleteInputProps extends ComboboxInputProps {
48
55
  ref?: React.Ref<HTMLInputElement>;
49
56
  icon?: ReactNode;
50
57
  disableCaret?: boolean;
@@ -56,47 +63,31 @@ const AutocompleteInput = ({
56
63
  icon,
57
64
  children,
58
65
  disableClear,
59
- ...props
66
+ ...outerProps
60
67
  }: AutocompleteInputProps) => {
61
- const valueFromContext = useContext(ValueContext);
62
68
  return (
63
69
  <BaseAutocomplete.Input
64
- render={({ ref, className, ...props }, state) => (
65
- <Input.Border ref={ref} className={className}>
66
- {icon}
67
- <Input.Input autoComplete="off" {...props} />
68
- {((!disableClear && valueFromContext !== null) || !disableCaret) && (
69
- <div className="flex items-center">
70
- {!disableClear && valueFromContext !== null && (
71
- <AutocompleteClear />
72
- )}
73
- {!disableCaret && (
74
- <BaseAutocomplete.Trigger
75
- render={<Button emphasis="ghost" size="small" />}
76
- >
77
- <AutocompleteIcon data-open={state.open ? true : undefined} />
78
- </BaseAutocomplete.Trigger>
79
- )}
80
- </div>
81
- )}
70
+ render={(props, state) => (
71
+ <ComboboxComposedInput
72
+ {...props}
73
+ open={state.open}
74
+ disableCaret={disableCaret}
75
+ disableClear={disableClear}
76
+ icon={icon}
77
+ >
82
78
  {children}
83
- </Input.Border>
79
+ </ComboboxComposedInput>
84
80
  )}
85
- {...props}
81
+ {...outerProps}
86
82
  />
87
83
  );
88
84
  };
89
85
 
90
- const AutocompleteIcon = withClassName(
86
+ export const AutocompleteIcon = withClassName(
91
87
  ({ className, ...props }: BaseAutocompleteIconProps) => (
92
88
  <BaseAutocomplete.Icon
93
89
  {...props}
94
- className={clsx(
95
- 'icon',
96
- 'layer-components:(flex shrink-0 items-center justify-center transition-transform)',
97
- 'layer-components:data-[open]:(rotate-180)',
98
- className,
99
- )}
90
+ className={clsx(comboboxIconClassName, className)}
100
91
  >
101
92
  <Icon name="chevron" />
102
93
  </BaseAutocomplete.Icon>
@@ -105,13 +96,12 @@ const AutocompleteIcon = withClassName(
105
96
 
106
97
  const AutocompletePopup = withClassName(
107
98
  BaseAutocomplete.Popup,
108
- popupClassName,
109
- 'layer-components:(w-[--anchor-width])',
99
+ comboboxPopupClassName,
110
100
  );
111
101
 
112
102
  const AutocompleteBackdrop = withClassName(
113
103
  BaseAutocomplete.Backdrop,
114
- 'layer-components:(fixed inset-0)',
104
+ comboboxBackdropClassName,
115
105
  );
116
106
 
117
107
  const AutocompleteArrow = ({ className, ...props }: AutocompleteArrowProps) => (
@@ -149,14 +139,12 @@ const AutocompleteContent = ({
149
139
 
150
140
  const AutocompleteList = withClassName(
151
141
  BaseAutocomplete.List,
152
- itemListClassName,
153
- 'layer-components:(flex flex-col overscroll-contain outline-none overflow-y-auto overflow-unstable)',
154
- 'layer-components:empty:(p-0)',
142
+ comboboxListClassName,
155
143
  );
156
144
 
157
145
  const AutocompleteEmpty = withClassName(
158
146
  BaseAutocomplete.Empty,
159
- 'layer-components:[&:not(:empty)]:(p-sm text-sm color-gray-dark)',
147
+ comboboxEmptyClassName,
160
148
  );
161
149
 
162
150
  export interface AutocompleteItemProps extends BaseAutocompleteItemProps {
@@ -180,27 +168,24 @@ const AutocompleteGroup = ({
180
168
  return (
181
169
  <BaseAutocomplete.Group
182
170
  {...props}
183
- className={clsx(
184
- 'layer-components:(flex flex-col gap-xs overflow-hidden p-sm)',
185
- className,
186
- )}
171
+ className={clsx(comboboxGroupClassName, className)}
187
172
  />
188
173
  );
189
174
  };
190
175
 
191
176
  const AutocompleteGroupItemList = withClassName(
192
177
  SlotDiv,
193
- 'layer-components:(flex flex-row flex-wrap gap-xs)',
178
+ comboboxGroupItemListClassName,
194
179
  );
195
180
 
196
181
  const AutocompleteGroupLabel = withClassName(
197
182
  BaseAutocomplete.GroupLabel,
198
- 'layer-components:(w-full px-xs text-xs font-medium uppercase color-gray-dark)',
183
+ comboboxGroupLabelClassName,
199
184
  );
200
185
 
201
186
  const AutocompleteRow: React.FC<BaseAutocompleteRowProps> = withClassName(
202
187
  BaseAutocomplete.Row,
203
- 'layer-components:(flex items-center gap-xs)',
188
+ comboboxRowClassName,
204
189
  );
205
190
 
206
191
  const AutocompleteSeparator = withClassName(
@@ -224,12 +209,7 @@ function AutocompleteGroupItem({
224
209
  <BaseAutocomplete.Item
225
210
  render={replace ?? <Button render={<Chip render={render} />} />}
226
211
  {...props}
227
- className={clsx(
228
- 'palette-primary',
229
- 'layer-composed-2:(bg-white)',
230
- 'layer-composed-2:data-[highlighted]:(ring-2 bg-main-wash ring-primary)',
231
- className,
232
- )}
212
+ className={clsx(comboboxGroupItemClassName, className)}
233
213
  />
234
214
  );
235
215
  }
@@ -238,7 +218,10 @@ export type AutocompleteClearProps = ButtonProps & {
238
218
  ref?: React.Ref<HTMLButtonElement>;
239
219
  children?: ReactNode;
240
220
  };
241
- const AutocompleteClear = ({ children, ...props }: AutocompleteClearProps) => (
221
+ export const AutocompleteClear = ({
222
+ children,
223
+ ...props
224
+ }: AutocompleteClearProps) => (
242
225
  <BaseAutocomplete.Clear
243
226
  render={<Button emphasis="ghost" size="small" />}
244
227
  {...props}
@@ -276,7 +259,7 @@ const baseSubComponents = {
276
259
  function createAutocomplete<TItem>() {
277
260
  function TypedRoot(
278
261
  props: Omit<BaseAutocompleteRootProps<TItem>, 'items'> & {
279
- items: readonly TItem[];
262
+ items?: readonly TItem[];
280
263
  },
281
264
  ) {
282
265
  return <AutocompleteRoot {...(props as any)} />;
@@ -290,10 +273,7 @@ function createAutocomplete<TItem>() {
290
273
  }
291
274
  return Object.assign(TypedRoot, {
292
275
  ...baseSubComponents,
293
- Input: AutocompleteInput,
294
- Content: AutocompleteContent,
295
276
  List: TypedList,
296
- Empty: AutocompleteEmpty,
297
277
  Item: AutocompleteItem,
298
278
  });
299
279
  }
@@ -0,0 +1,289 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { useState } from 'react';
3
+ import { Box } from '../box/Box.js';
4
+ import { Button } from '../button/Button.js';
5
+ import { Icon } from '../icon/Icon.js';
6
+ import { Combobox } from './Combobox.js';
7
+
8
+ interface Args {
9
+ arrow?: boolean;
10
+ autoHighlight?: boolean;
11
+ creatable?: boolean;
12
+ highlightItemOnHover?: boolean;
13
+ }
14
+
15
+ const meta = {
16
+ title: 'Components/Combobox',
17
+ argTypes: {
18
+ arrow: {
19
+ control: 'boolean',
20
+ description: 'Whether to show the arrow on the Combobox popup.',
21
+ defaultValue: false,
22
+ },
23
+ autoHighlight: {
24
+ control: 'boolean',
25
+ description:
26
+ 'If true, the first item will be automatically highlighted when the list opens.',
27
+ defaultValue: false,
28
+ },
29
+ highlightItemOnHover: {
30
+ control: 'boolean',
31
+ description:
32
+ 'If true, items will be highlighted when hovered with the mouse.',
33
+ defaultValue: false,
34
+ },
35
+ creatable: {
36
+ control: 'boolean',
37
+ description:
38
+ 'If true, the Combobox will allow creating new items based on user input.',
39
+ defaultValue: false,
40
+ },
41
+ },
42
+ args: {
43
+ arrow: false,
44
+ autoHighlight: false,
45
+ highlightItemOnHover: true,
46
+ creatable: false,
47
+ },
48
+ parameters: {
49
+ controls: { expanded: true },
50
+ },
51
+ } satisfies Meta<Args>;
52
+
53
+ export default meta;
54
+
55
+ type Story = StoryObj<Args>;
56
+
57
+ export interface Item {
58
+ id: string;
59
+ label: string;
60
+ }
61
+
62
+ const items: Item[] = [
63
+ { id: 'apple', label: 'Apple' },
64
+ { id: 'banana', label: 'Banana' },
65
+ { id: 'cherry', label: 'Cherry' },
66
+ { id: 'date', label: 'Date' },
67
+ { id: 'elderberry', label: 'Elderberry' },
68
+ { id: 'fig', label: 'Fig' },
69
+ { id: 'grape', label: 'Grape' },
70
+ { id: 'honeydew', label: 'Honeydew' },
71
+ ];
72
+
73
+ const ExampleCombobox = Combobox.create<Item>();
74
+
75
+ export const Default: Story = {
76
+ render({ arrow, autoHighlight, highlightItemOnHover, creatable }) {
77
+ const [value, setValue] = useState<Item | null>(null);
78
+ const allItems =
79
+ value && items.includes(value)
80
+ ? items
81
+ : value
82
+ ? [value, ...items]
83
+ : items;
84
+ return (
85
+ <ExampleCombobox
86
+ value={value}
87
+ onValueChange={setValue}
88
+ items={allItems}
89
+ autoHighlight={autoHighlight}
90
+ highlightItemOnHover={highlightItemOnHover}
91
+ showCreatableItem={creatable}
92
+ onCreate={
93
+ creatable
94
+ ? (value: string) => {
95
+ console.log('Creating item:', value);
96
+ setValue({ id: value, label: value });
97
+ }
98
+ : undefined
99
+ }
100
+ >
101
+ <ExampleCombobox.Input
102
+ icon={<Icon name="food" />}
103
+ className="w-[200px]"
104
+ />
105
+ <ExampleCombobox.Content arrow={arrow}>
106
+ <ExampleCombobox.List>
107
+ {(item) => (
108
+ <ExampleCombobox.Item key={item.id} value={item}>
109
+ {item.label}
110
+ </ExampleCombobox.Item>
111
+ )}
112
+ </ExampleCombobox.List>
113
+ <ExampleCombobox.Empty>No results found.</ExampleCombobox.Empty>
114
+ </ExampleCombobox.Content>
115
+ </ExampleCombobox>
116
+ );
117
+ },
118
+ };
119
+
120
+ interface ItemGroup {
121
+ category: string;
122
+ items: Item[];
123
+ }
124
+ const groupedItems: ItemGroup[] = [
125
+ {
126
+ category: 'Fruits',
127
+ items: [
128
+ { id: 'apple', label: 'Apple' },
129
+ { id: 'banana', label: 'Banana' },
130
+ { id: 'cherry', label: 'Cherry' },
131
+ ],
132
+ },
133
+ {
134
+ category: 'Berries',
135
+ items: [
136
+ { id: 'strawberry', label: 'Strawberry' },
137
+ { id: 'blueberry', label: 'Blueberry' },
138
+ { id: 'raspberry', label: 'Raspberry' },
139
+ ],
140
+ },
141
+ ];
142
+
143
+ const GroupedCombobox = Combobox.createGrouped<ItemGroup>();
144
+
145
+ export const Grouped: Story = {
146
+ render({ arrow, autoHighlight, highlightItemOnHover, creatable }) {
147
+ const [value, setValue] = useState<Item | null>(null);
148
+ const [input, setInput] = useState<string>('');
149
+ return (
150
+ <GroupedCombobox
151
+ value={value}
152
+ onValueChange={setValue}
153
+ inputValue={input}
154
+ onInputValueChange={setInput}
155
+ items={groupedItems}
156
+ autoHighlight={autoHighlight}
157
+ highlightItemOnHover={highlightItemOnHover}
158
+ onCreate={
159
+ creatable
160
+ ? (value: string) => alert(`Create item: ${value}`)
161
+ : undefined
162
+ }
163
+ >
164
+ <GroupedCombobox.Input />
165
+ <GroupedCombobox.Content arrow={arrow}>
166
+ <GroupedCombobox.List>
167
+ {(group) => (
168
+ <GroupedCombobox.Group key={group.category}>
169
+ <GroupedCombobox.GroupLabel>
170
+ {group.category}
171
+ </GroupedCombobox.GroupLabel>
172
+ <GroupedCombobox.GroupList>
173
+ {group.items.map((item) => (
174
+ <GroupedCombobox.Item key={item.id} value={item}>
175
+ {item.label}
176
+ </GroupedCombobox.Item>
177
+ ))}
178
+ </GroupedCombobox.GroupList>
179
+ </GroupedCombobox.Group>
180
+ )}
181
+ </GroupedCombobox.List>
182
+ <GroupedCombobox.Empty>
183
+ {creatable ? (
184
+ <div>
185
+ <Icon name="enterKey" /> Create "{input}"
186
+ </div>
187
+ ) : (
188
+ `No results found.`
189
+ )}
190
+ </GroupedCombobox.Empty>
191
+ <GroupedCombobox.Separator />
192
+ <div className="p-sm text-xs color-gray-dark">
193
+ Select your favorite fruit or berry.
194
+ {creatable ? ' Enter creates a new item.' : ''}
195
+ </div>
196
+ </GroupedCombobox.Content>
197
+ </GroupedCombobox>
198
+ );
199
+ },
200
+ };
201
+
202
+ export const NotPopover: Story = {
203
+ render({ autoHighlight, highlightItemOnHover, creatable }) {
204
+ const [value, setValue] = useState<Item | null>(null);
205
+ return (
206
+ <GroupedCombobox
207
+ value={value}
208
+ onValueChange={setValue}
209
+ items={groupedItems}
210
+ autoHighlight={autoHighlight}
211
+ highlightItemOnHover={highlightItemOnHover}
212
+ onCreate={
213
+ creatable
214
+ ? (value: string) => alert(`Create item: ${value}`)
215
+ : undefined
216
+ }
217
+ >
218
+ <Box border p surface="white" col>
219
+ <GroupedCombobox.Input disableCaret className="w-full" disableClear>
220
+ <Button size="small" emphasis="primary" className="aspect-1 h-full">
221
+ <Icon name="plus" />
222
+ </Button>
223
+ </GroupedCombobox.Input>
224
+ <GroupedCombobox.List>
225
+ {(group) => (
226
+ <GroupedCombobox.Group key={group.category}>
227
+ <GroupedCombobox.GroupLabel>
228
+ {group.category}
229
+ </GroupedCombobox.GroupLabel>
230
+ <GroupedCombobox.GroupList>
231
+ {group.items.map((item) => (
232
+ <GroupedCombobox.Item key={item.id} value={item}>
233
+ {item.label}
234
+ </GroupedCombobox.Item>
235
+ ))}
236
+ </GroupedCombobox.GroupList>
237
+ </GroupedCombobox.Group>
238
+ )}
239
+ </GroupedCombobox.List>
240
+ <GroupedCombobox.Empty>No results found.</GroupedCombobox.Empty>
241
+ </Box>
242
+ </GroupedCombobox>
243
+ );
244
+ },
245
+ };
246
+
247
+ export const MultiSelect: Story = {
248
+ render({ arrow, autoHighlight, highlightItemOnHover, creatable }) {
249
+ const [value, setValue] = useState<Item[]>([]);
250
+ return (
251
+ <ExampleCombobox.Multi
252
+ multiple
253
+ value={value}
254
+ onValueChange={setValue}
255
+ items={items}
256
+ autoHighlight={autoHighlight}
257
+ highlightItemOnHover={highlightItemOnHover}
258
+ showCreatableItem={creatable}
259
+ >
260
+ <ExampleCombobox.Chips className="w-300px">
261
+ <ExampleCombobox.MultiValue>
262
+ {(items) => (
263
+ <>
264
+ <ExampleCombobox.ChipsList>
265
+ {items.map((item) => (
266
+ <ExampleCombobox.Chip key={item.id}>
267
+ {item.label}
268
+ </ExampleCombobox.Chip>
269
+ ))}
270
+ </ExampleCombobox.ChipsList>
271
+ <ExampleCombobox.Input />
272
+ </>
273
+ )}
274
+ </ExampleCombobox.MultiValue>
275
+ </ExampleCombobox.Chips>
276
+ <ExampleCombobox.Content arrow={arrow}>
277
+ <ExampleCombobox.List>
278
+ {(item) => (
279
+ <ExampleCombobox.Item key={item.id} value={item}>
280
+ {item.label}
281
+ </ExampleCombobox.Item>
282
+ )}
283
+ </ExampleCombobox.List>
284
+ <ExampleCombobox.Empty>No results found.</ExampleCombobox.Empty>
285
+ </ExampleCombobox.Content>
286
+ </ExampleCombobox.Multi>
287
+ );
288
+ },
289
+ };