Skip to content

Commit a2c58fd

Browse files
authored
✨ feat(base-ui): add Form, Input family, Slider, AutoComplete and Checkbox (#557)
* ✨ feat(base-ui): add Form, Input family, Slider, AutoComplete and Checkbox * 🔧 chore: remove workspace lockfile
1 parent aa83de6 commit a2c58fd

55 files changed

Lines changed: 3338 additions & 8 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@
140140
},
141141
"dependencies": {
142142
"@ant-design/cssinjs": "^2.1.2",
143-
"@base-ui/react": "1.5.0",
143+
"@base-ui/react": "^1.6.0",
144144
"@dnd-kit/core": "^6.3.1",
145145
"@dnd-kit/modifiers": "^9.0.0",
146146
"@dnd-kit/sortable": "^10.0.0",
@@ -267,7 +267,8 @@
267267
"typescript": "^6.0.3",
268268
"unist-util-is": "^6.0.1",
269269
"unist-util-visit": "^5.1.0",
270-
"vitest": "^3.2.6"
270+
"vitest": "^3.2.6",
271+
"zod": "^4.4.3"
271272
},
272273
"peerDependencies": {
273274
"@lobehub/fluent-emoji": "^4.0.0",
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
'use client';
2+
3+
import { Autocomplete } from '@base-ui/react/autocomplete';
4+
import { cx, useThemeMode } from 'antd-style';
5+
import { XIcon } from 'lucide-react';
6+
import { memo, useMemo, useRef } from 'react';
7+
8+
import { inputStyles, inputVariants } from '@/base-ui/Input';
9+
import Icon from '@/Icon';
10+
import { useAppElement } from '@/ThemeProvider';
11+
12+
import { styles } from './style';
13+
import type { AutoCompleteOption, AutoCompleteProps } from './type';
14+
15+
const AutoComplete = memo<AutoCompleteProps>(
16+
({
17+
className,
18+
classNames,
19+
styles: customStyles,
20+
style,
21+
variant,
22+
shadow,
23+
size = 'middle',
24+
options = [],
25+
onChange,
26+
onSearch,
27+
allowClear,
28+
disabled,
29+
placeholder,
30+
prefix,
31+
suffix,
32+
emptyText,
33+
...rest
34+
}) => {
35+
const { isDarkMode } = useThemeMode();
36+
const appElement = useAppElement();
37+
const anchorRef = useRef<HTMLDivElement>(null);
38+
const mergedVariant = variant || (isDarkMode ? 'filled' : 'outlined');
39+
40+
const items = useMemo<AutoCompleteOption[]>(
41+
() => options.map((option) => (typeof option === 'string' ? { value: option } : option)),
42+
[options],
43+
);
44+
45+
return (
46+
<Autocomplete.Root
47+
openOnInputClick
48+
disabled={disabled}
49+
itemToStringValue={(item) => (item as AutoCompleteOption).value}
50+
items={items}
51+
onValueChange={(value) => {
52+
onChange?.(value);
53+
onSearch?.(value);
54+
}}
55+
{...rest}
56+
>
57+
<div
58+
className={cx(inputVariants({ shadow, size, variant: mergedVariant }), className)}
59+
data-disabled={disabled ? '' : undefined}
60+
ref={anchorRef}
61+
style={style}
62+
>
63+
{prefix && <span className={inputStyles.slot}>{prefix}</span>}
64+
<Autocomplete.Input
65+
className={cx(inputStyles.input, classNames?.input)}
66+
placeholder={placeholder}
67+
style={customStyles?.input}
68+
/>
69+
{allowClear && (
70+
<Autocomplete.Clear aria-label={'Clear'} className={styles.clear}>
71+
<Icon icon={XIcon} size={14} />
72+
</Autocomplete.Clear>
73+
)}
74+
{suffix && <span className={inputStyles.slot}>{suffix}</span>}
75+
</div>
76+
<Autocomplete.Portal container={appElement ?? undefined}>
77+
<Autocomplete.Positioner anchor={anchorRef} className={styles.positioner} sideOffset={4}>
78+
<Autocomplete.Popup
79+
className={cx(styles.popup, classNames?.popup)}
80+
style={customStyles?.popup}
81+
>
82+
{emptyText && (
83+
<Autocomplete.Empty className={styles.empty}>{emptyText}</Autocomplete.Empty>
84+
)}
85+
<Autocomplete.List className={styles.list}>
86+
{(item: AutoCompleteOption) => (
87+
<Autocomplete.Item
88+
className={cx(styles.item, classNames?.item)}
89+
disabled={item.disabled}
90+
key={item.value}
91+
style={customStyles?.item}
92+
value={item}
93+
>
94+
{item.label ?? item.value}
95+
</Autocomplete.Item>
96+
)}
97+
</Autocomplete.List>
98+
</Autocomplete.Popup>
99+
</Autocomplete.Positioner>
100+
</Autocomplete.Portal>
101+
</Autocomplete.Root>
102+
);
103+
},
104+
);
105+
106+
AutoComplete.displayName = 'AutoComplete';
107+
108+
export default AutoComplete;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Flexbox } from '@lobehub/ui';
2+
import { AutoComplete } from '@lobehub/ui/base-ui';
3+
import { useState } from 'react';
4+
5+
const models = [
6+
'claude-fable-5',
7+
'claude-opus-4-8',
8+
'claude-sonnet-5',
9+
'claude-haiku-4-5',
10+
'gpt-5.2',
11+
'gemini-3-pro',
12+
];
13+
14+
export default () => {
15+
const [value, setValue] = useState('');
16+
17+
return (
18+
<Flexbox gap={16} padding={16} style={{ maxWidth: 480 }} width={'100%'}>
19+
<AutoComplete
20+
allowClear
21+
emptyText={'No matching model'}
22+
options={models}
23+
placeholder={'Search a model...'}
24+
value={value}
25+
onChange={setValue}
26+
/>
27+
<AutoComplete
28+
placeholder={'Object options'}
29+
variant={'filled'}
30+
options={[
31+
{ label: 'Production', value: 'prod' },
32+
{ label: 'Staging', value: 'staging' },
33+
{ disabled: true, label: 'Legacy (read-only)', value: 'legacy' },
34+
]}
35+
/>
36+
</Flexbox>
37+
);
38+
};

src/base-ui/AutoComplete/index.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
nav: Components
3+
group: Data Entry
4+
title: AutoComplete
5+
description: Base UI-powered AutoComplete input with filtered suggestions.
6+
subType: base-ui
7+
apiHeader:
8+
pkg: '@lobehub/ui/base-ui'
9+
docUrl: 'https://github.com/lobehub/lobe-ui/tree/master/src/base-ui/AutoComplete/index.md'
10+
sourceUrl: 'https://github.com/lobehub/lobe-ui/tree/master/src/base-ui/AutoComplete/AutoComplete.tsx'
11+
---
12+
13+
## Default
14+
15+
<code src="./demos/index.tsx" nopadding></code>
16+
17+
## APIs
18+
19+
| Property | Description | Type | Default |
20+
| --------------- | ---------------------------------- | -------------------------------------------- | -------- |
21+
| options | Suggestion list | `(string \| { value; label?; disabled? })[]` | `[]` |
22+
| value | Controlled input value | `string` | - |
23+
| onChange | Fires on typing or item pick | `(value: string) => void` | - |
24+
| onSearch | Alias fired together with onChange | `(value: string) => void` | - |
25+
| allowClear | Show clear button | `boolean` | - |
26+
| emptyText | Content when nothing matches | `ReactNode` | - |
27+
| variant | Input visual variant | `'filled' \| 'outlined' \| 'borderless'` | auto |
28+
| size | Input size | `'small' \| 'middle' \| 'large'` | `middle` |
29+
| shadow | Apply lobe shadow style | `boolean` | - |
30+
| prefix / suffix | Input slots | `ReactNode` | - |
31+
32+
Filtering is built into Base UI Autocomplete; items are matched against the typed value automatically.

src/base-ui/AutoComplete/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { default as AutoComplete } from './AutoComplete';
2+
export { styles as autoCompleteStyles } from './style';
3+
export type { AutoCompleteOption, AutoCompleteProps } from './type';

src/base-ui/AutoComplete/style.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { createStaticStyles } from 'antd-style';
2+
3+
import { styles as menuStyles } from '@/base-ui/DropdownMenu/sharedStyle';
4+
5+
const ownStyles = createStaticStyles(({ css, cssVar }) => ({
6+
clear: css`
7+
cursor: pointer;
8+
9+
display: inline-flex;
10+
align-items: center;
11+
justify-content: center;
12+
13+
margin: 0;
14+
padding: 0;
15+
border: none;
16+
17+
color: ${cssVar.colorTextTertiary};
18+
19+
background: none;
20+
outline: none;
21+
22+
transition: color 150ms ${cssVar.motionEaseOut};
23+
24+
&:hover {
25+
color: ${cssVar.colorText};
26+
}
27+
`,
28+
empty: css`
29+
cursor: default;
30+
31+
&:empty {
32+
display: none;
33+
}
34+
35+
&:hover {
36+
background: transparent;
37+
}
38+
`,
39+
list: css`
40+
overflow-y: auto;
41+
max-height: min(320px, var(--available-height));
42+
43+
&:empty {
44+
display: none;
45+
}
46+
`,
47+
popup: css`
48+
transform-origin: var(--transform-origin);
49+
width: var(--anchor-width);
50+
min-width: 0;
51+
transition:
52+
opacity 140ms ${cssVar.motionEaseOut},
53+
transform 140ms ${cssVar.motionEaseOut};
54+
55+
&[data-starting-style],
56+
&[data-ending-style] {
57+
transform: scaleY(0.92);
58+
opacity: 0;
59+
}
60+
`,
61+
}));
62+
63+
export const styles = {
64+
clear: ownStyles.clear,
65+
empty: [menuStyles.item, menuStyles.empty, ownStyles.empty].join(' '),
66+
item: menuStyles.item,
67+
list: ownStyles.list,
68+
popup: [menuStyles.popup, ownStyles.popup].join(' '),
69+
positioner: menuStyles.positioner,
70+
};

src/base-ui/AutoComplete/type.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { Autocomplete } from '@base-ui/react/autocomplete';
2+
import type { ComponentProps, CSSProperties, ReactNode } from 'react';
3+
4+
import type { InputProps } from '@/base-ui/Input';
5+
6+
export interface AutoCompleteOption {
7+
disabled?: boolean;
8+
label?: ReactNode;
9+
value: string;
10+
}
11+
12+
type BaseAutocompleteProps = Omit<
13+
ComponentProps<typeof Autocomplete.Root>,
14+
'className' | 'style' | 'render' | 'children' | 'items' | 'onValueChange' | 'mode'
15+
>;
16+
17+
export interface AutoCompleteProps extends BaseAutocompleteProps {
18+
allowClear?: boolean;
19+
className?: string;
20+
classNames?: {
21+
input?: string;
22+
item?: string;
23+
popup?: string;
24+
};
25+
disabled?: boolean;
26+
emptyText?: ReactNode;
27+
onChange?: (value: string) => void;
28+
onSearch?: (value: string) => void;
29+
options?: (string | AutoCompleteOption)[];
30+
placeholder?: string;
31+
prefix?: InputProps['prefix'];
32+
shadow?: boolean;
33+
size?: InputProps['size'];
34+
style?: CSSProperties;
35+
styles?: {
36+
input?: CSSProperties;
37+
item?: CSSProperties;
38+
popup?: CSSProperties;
39+
};
40+
suffix?: InputProps['suffix'];
41+
variant?: InputProps['variant'];
42+
}

src/base-ui/Button/style.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { createStaticStyles } from 'antd-style';
22

3+
import { controlHeight } from '@/base-ui/controlSize';
4+
35
export const styles = createStaticStyles(({ css, cssVar }) => ({
46
base: css`
57
cursor: pointer;
@@ -37,21 +39,21 @@ export const styles = createStaticStyles(({ css, cssVar }) => ({
3739
`,
3840

3941
sizeSmall: css`
40-
height: 24px;
42+
height: ${controlHeight.small}px;
4143
padding-inline: 8px;
4244
border-radius: ${cssVar.borderRadiusSM};
4345
font-size: 12px;
4446
`,
4547

4648
sizeMiddle: css`
47-
height: 32px;
49+
height: ${controlHeight.middle}px;
4850
padding-inline: 14px;
4951
border-radius: ${cssVar.borderRadiusSM};
5052
font-size: 13px;
5153
`,
5254

5355
sizeLarge: css`
54-
height: 40px;
56+
height: ${controlHeight.large}px;
5557
padding-inline: 16px;
5658
border-radius: ${cssVar.borderRadius};
5759
font-size: 14px;

0 commit comments

Comments
 (0)