@mindlogic-ai/logician-ui 3.0.0-alpha.34 → 3.0.0-alpha.35

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 (75) hide show
  1. package/dist/components/Pagination/Pagination.d.ts.map +1 -1
  2. package/dist/components/Pagination/Pagination.js +7 -20
  3. package/dist/components/Pagination/Pagination.js.map +1 -1
  4. package/dist/components/Pagination/Pagination.mjs +7 -20
  5. package/dist/components/Pagination/Pagination.mjs.map +1 -1
  6. package/dist/components/Select/Combobox.d.ts +28 -0
  7. package/dist/components/Select/Combobox.d.ts.map +1 -0
  8. package/dist/components/Select/Combobox.js +47 -0
  9. package/dist/components/Select/Combobox.js.map +1 -0
  10. package/dist/components/Select/Combobox.mjs +45 -0
  11. package/dist/components/Select/Combobox.mjs.map +1 -0
  12. package/dist/components/Select/ComboboxField.d.ts +12 -0
  13. package/dist/components/Select/ComboboxField.d.ts.map +1 -0
  14. package/dist/components/Select/ComboboxField.js +34 -0
  15. package/dist/components/Select/ComboboxField.js.map +1 -0
  16. package/dist/components/Select/ComboboxField.mjs +32 -0
  17. package/dist/components/Select/ComboboxField.mjs.map +1 -0
  18. package/dist/components/Select/Select.d.ts +29 -3
  19. package/dist/components/Select/Select.d.ts.map +1 -1
  20. package/dist/components/Select/Select.js +41 -156
  21. package/dist/components/Select/Select.js.map +1 -1
  22. package/dist/components/Select/Select.mjs +43 -158
  23. package/dist/components/Select/Select.mjs.map +1 -1
  24. package/dist/components/Select/Select.styles.d.ts +91 -62
  25. package/dist/components/Select/Select.styles.d.ts.map +1 -1
  26. package/dist/components/Select/Select.styles.js +67 -69
  27. package/dist/components/Select/Select.styles.js.map +1 -1
  28. package/dist/components/Select/Select.styles.mjs +63 -66
  29. package/dist/components/Select/Select.styles.mjs.map +1 -1
  30. package/dist/components/Select/Select.types.d.ts +31 -8
  31. package/dist/components/Select/Select.types.d.ts.map +1 -1
  32. package/dist/components/Select/SelectField.d.ts +11 -0
  33. package/dist/components/Select/SelectField.d.ts.map +1 -0
  34. package/dist/components/Select/SelectField.js +32 -0
  35. package/dist/components/Select/SelectField.js.map +1 -0
  36. package/dist/components/Select/SelectField.mjs +30 -0
  37. package/dist/components/Select/SelectField.mjs.map +1 -0
  38. package/dist/components/Select/index.d.ts +4 -0
  39. package/dist/components/Select/index.d.ts.map +1 -1
  40. package/dist/index.js +27 -9
  41. package/dist/index.js.map +1 -1
  42. package/dist/index.mjs +4 -1
  43. package/dist/index.mjs.map +1 -1
  44. package/package.json +1 -3
  45. package/src/components/FormIntegration/FormIntegration.stories.tsx +7 -13
  46. package/src/components/FormIntegration/README.md +1 -1
  47. package/src/components/Pagination/Pagination.tsx +7 -26
  48. package/src/components/Select/Combobox.stories.tsx +172 -0
  49. package/src/components/Select/Combobox.tsx +69 -0
  50. package/src/components/Select/ComboboxField.tsx +86 -0
  51. package/src/components/Select/Comparison.stories.tsx +38 -321
  52. package/src/components/Select/Select.stories.tsx +691 -404
  53. package/src/components/Select/Select.styles.ts +64 -76
  54. package/src/components/Select/Select.tsx +76 -202
  55. package/src/components/Select/Select.types.ts +33 -13
  56. package/src/components/Select/SelectField.tsx +87 -0
  57. package/src/components/Select/index.ts +8 -0
  58. package/dist/components/Select/MenuList/MenuList.d.ts +0 -4
  59. package/dist/components/Select/MenuList/MenuList.d.ts.map +0 -1
  60. package/dist/components/Select/MenuList/MenuList.types.d.ts +0 -9
  61. package/dist/components/Select/MenuList/MenuList.types.d.ts.map +0 -1
  62. package/dist/components/Select/MenuList/VirtualizedMenuList.d.ts +0 -4
  63. package/dist/components/Select/MenuList/VirtualizedMenuList.d.ts.map +0 -1
  64. package/dist/components/Select/MenuList/VirtualizedMenuListContext.d.ts +0 -9
  65. package/dist/components/Select/MenuList/VirtualizedMenuListContext.d.ts.map +0 -1
  66. package/dist/components/Select/MenuList/index.d.ts +0 -4
  67. package/dist/components/Select/MenuList/index.d.ts.map +0 -1
  68. package/dist/components/Select/_utils/resolveStyle.d.ts +0 -5
  69. package/dist/components/Select/_utils/resolveStyle.d.ts.map +0 -1
  70. package/src/components/Select/MenuList/MenuList.tsx +0 -87
  71. package/src/components/Select/MenuList/MenuList.types.ts +0 -21
  72. package/src/components/Select/MenuList/VirtualizedMenuList.tsx +0 -136
  73. package/src/components/Select/MenuList/VirtualizedMenuListContext.tsx +0 -35
  74. package/src/components/Select/MenuList/index.ts +0 -3
  75. package/src/components/Select/_utils/resolveStyle.ts +0 -21
@@ -8,7 +8,7 @@ import { Button } from '../Button';
8
8
  import { FormLabel } from '../FormLabel';
9
9
  import { Input } from '../Input';
10
10
  import { Textarea } from '../Textarea';
11
- import { Select } from '../Select';
11
+ import { SelectField } from '../Select';
12
12
  import { FileInput } from '../FileInput';
13
13
  import { Radio } from '../Radio';
14
14
  import { RadioGroup } from '../Radio/RadioGroup';
@@ -34,7 +34,7 @@ interface FormValues {
34
34
  name: string;
35
35
  email: string;
36
36
  bio: string;
37
- country: { value: string; label: string } | null;
37
+ country: string | null;
38
38
  gender: string;
39
39
  experience: number;
40
40
  files: FileList | null;
@@ -60,13 +60,7 @@ const validationSchema = Yup.object({
60
60
  .min(10, '자기소개는 최소 10글자 이상이어야 합니다')
61
61
  .max(200, '자기소개는 최대 200글자까지 가능합니다')
62
62
  .required('자기소개는 필수입니다'),
63
- country: Yup.object()
64
- .shape({
65
- value: Yup.string().required(),
66
- label: Yup.string().required(),
67
- })
68
- .nullable()
69
- .required('국가를 선택해주세요'),
63
+ country: Yup.string().nullable().required('국가를 선택해주세요'),
70
64
  gender: Yup.string()
71
65
  .oneOf(['male', 'female', 'other'], '성별을 선택해주세요')
72
66
  .required('성별은 필수입니다'),
@@ -115,7 +109,7 @@ export const FormikFormExample: StoryObj = {
115
109
  console.log('👤 이름:', values.name);
116
110
  console.log('📧 이메일:', values.email);
117
111
  console.log('📝 자기소개:', values.bio);
118
- console.log('🌍 국가:', values.country?.label);
112
+ console.log('🌍 국가:', values.country);
119
113
  console.log('👫 성별:', values.gender);
120
114
  console.log('💼 경력:', `${values.experience}년`);
121
115
  console.log(
@@ -231,7 +225,7 @@ export const FormikFormExample: StoryObj = {
231
225
  <FormLabel>
232
226
  국가 <Field.RequiredIndicator />
233
227
  </FormLabel>
234
- <Select
228
+ <SelectField
235
229
  name="country"
236
230
  options={countryOptions}
237
231
  value={values.country}
@@ -445,7 +439,7 @@ export const RegularFormExample: StoryObj = {
445
439
  console.log('👤 이름:', formData.name);
446
440
  console.log('📧 이메일:', formData.email);
447
441
  console.log('📝 자기소개:', formData.bio);
448
- console.log('🌍 국가:', formData.country?.label);
442
+ console.log('🌍 국가:', formData.country);
449
443
  console.log('👫 성별:', formData.gender);
450
444
  console.log('💼 경력:', `${formData.experience}년`);
451
445
  console.log(
@@ -561,7 +555,7 @@ export const RegularFormExample: StoryObj = {
561
555
  <FormLabel>
562
556
  국가 <Field.RequiredIndicator />
563
557
  </FormLabel>
564
- <Select
558
+ <SelectField
565
559
  name="country"
566
560
  options={countryOptions}
567
561
  value={formData.country}
@@ -49,7 +49,7 @@ Formik과 Yup을 사용한 폼 validation 예제입니다.
49
49
 
50
50
  #### 국가 (Select)
51
51
  - 필수 선택
52
- - react-select 사용
52
+ - SelectField 사용
53
53
 
54
54
  #### 성별 (Radio)
55
55
  - 필수 선택
@@ -1,5 +1,4 @@
1
1
  import { useMemo, useState } from 'react';
2
- import { SingleValue } from 'react-select';
3
2
  import { Flex } from '@chakra-ui/react';
4
3
 
5
4
  import { IoChevronForward, IoIosArrowBack } from '@/components/Icon';
@@ -7,8 +6,7 @@ import { IconButton } from '@/components/IconButton';
7
6
  import { Subtext, Subtitle } from '@/components/Typography';
8
7
  import { useTranslate } from '@/hooks/useTranslate';
9
8
 
10
- import { Select } from '../Select';
11
- import { SelectOption } from '../Select/Select.types';
9
+ import { SelectField } from '../Select';
12
10
  import { PaginationProps } from './Pagination.types';
13
11
 
14
12
  /**
@@ -61,11 +59,8 @@ export const Pagination = ({
61
59
  }
62
60
  };
63
61
 
64
- const handleItemsPerPageOptionChange = (
65
- newValue: SingleValue<SelectOption<number>>
66
- ) => {
67
- if (newValue) {
68
- const newItemsPerPage = newValue.value;
62
+ const handleItemsPerPageOptionChange = (newItemsPerPage: number | null) => {
63
+ if (newItemsPerPage != null) {
69
64
  onItemsPerPageOptionChange?.(newItemsPerPage);
70
65
 
71
66
  const newMaxPage = Math.ceil(numTotalItems / newItemsPerPage);
@@ -83,28 +78,14 @@ export const Pagination = ({
83
78
  <Flex align="center" gap={2}>
84
79
  {itemsPerPageOptions && (
85
80
  <Flex align="center" gap={2}>
86
- <Select
87
- menuPlacement="auto"
88
- styles={{
89
- control: (base, _state) => ({
90
- ...base,
91
- fontSize: '0.875rem',
92
- padding: 0,
93
- minHeight: '28px',
94
- }),
95
- dropdownIndicator: (base, _props) => ({
96
- ...base,
97
- padding: '0 4px',
98
- }),
99
- }}
81
+ <SelectField<number>
82
+ size="sm"
83
+ width="5.5rem"
100
84
  options={itemsPerPageOptions.map((option) => ({
101
85
  label: option.toString(),
102
86
  value: option,
103
87
  }))}
104
- value={{
105
- label: itemsPerPage.toString(),
106
- value: itemsPerPage,
107
- }}
88
+ value={itemsPerPage}
108
89
  onChange={handleItemsPerPageOptionChange}
109
90
  />
110
91
  <Subtext whiteSpace="nowrap">
@@ -0,0 +1,172 @@
1
+ import { useState } from 'react';
2
+ import {
3
+ Box,
4
+ Portal,
5
+ Span,
6
+ Stack,
7
+ useFilter,
8
+ useListCollection,
9
+ } from '@chakra-ui/react';
10
+ import { Meta } from '@storybook/react';
11
+
12
+ import { Combobox } from './Combobox';
13
+ import { ComboboxField } from './ComboboxField';
14
+
15
+ const meta = {
16
+ title: 'Components/Combobox',
17
+ } satisfies Meta;
18
+
19
+ export default meta;
20
+
21
+ const fruitOptions = [
22
+ { label: 'Apple', value: 'apple' },
23
+ { label: 'Banana', value: 'banana' },
24
+ { label: 'Blueberry', value: 'blueberry' },
25
+ { label: 'Cherry', value: 'cherry' },
26
+ { label: 'Grape', value: 'grape' },
27
+ { label: 'Mango', value: 'mango' },
28
+ { label: 'Orange', value: 'orange' },
29
+ { label: 'Strawberry', value: 'strawberry', disabled: true },
30
+ ];
31
+
32
+ /**
33
+ * Type to filter the option list. Clear text to see every option again,
34
+ * type something with no match to see the empty state.
35
+ */
36
+ export const Basic = () => {
37
+ const [value, setValue] = useState<string | null>(null);
38
+
39
+ return (
40
+ <Box maxW="320px">
41
+ <ComboboxField
42
+ options={fruitOptions}
43
+ value={value}
44
+ onChange={setValue}
45
+ placeholder="Search a fruit"
46
+ />
47
+ </Box>
48
+ );
49
+ };
50
+
51
+ export const WithLabel = () => (
52
+ <Box maxW="320px">
53
+ <ComboboxField
54
+ label="Favorite fruit"
55
+ options={fruitOptions}
56
+ placeholder="Search a fruit"
57
+ />
58
+ </Box>
59
+ );
60
+
61
+ export const Sizes = () => (
62
+ <Stack maxW="320px" gap={4}>
63
+ {(['xs', 'sm', 'md', 'lg'] as const).map((size) => (
64
+ <ComboboxField
65
+ key={size}
66
+ size={size}
67
+ options={fruitOptions}
68
+ placeholder={`Size: ${size}`}
69
+ />
70
+ ))}
71
+ </Stack>
72
+ );
73
+
74
+ export const Invalid = () => (
75
+ <Box maxW="320px">
76
+ <ComboboxField
77
+ options={fruitOptions}
78
+ placeholder="Invalid combobox"
79
+ invalid
80
+ />
81
+ </Box>
82
+ );
83
+
84
+ export const Disabled = () => (
85
+ <Box maxW="320px">
86
+ <ComboboxField options={fruitOptions} defaultValue="apple" disabled />
87
+ </Box>
88
+ );
89
+
90
+ export const CustomEmptyText = () => (
91
+ <Box maxW="320px">
92
+ <ComboboxField
93
+ options={fruitOptions}
94
+ placeholder="Type 'xyz' to see the empty state"
95
+ emptyText="해당하는 항목이 없습니다"
96
+ />
97
+ </Box>
98
+ );
99
+
100
+ export const ManyOptions = () => {
101
+ const manyOptions = Array.from({ length: 100 }, (_, i) => ({
102
+ label: `Item ${i + 1}`,
103
+ value: `item${i + 1}`,
104
+ }));
105
+
106
+ return (
107
+ <Box maxW="320px">
108
+ <ComboboxField options={manyOptions} placeholder="Search 100 items" />
109
+ </Box>
110
+ );
111
+ };
112
+
113
+ /* -------------------------------------------------------------------------
114
+ * Custom children
115
+ *
116
+ * `ComboboxField` takes a flat `options` array; for richer option content
117
+ * compose the `Combobox` primitives directly with `useListCollection` and
118
+ * `useFilter`. Keep `Combobox.ItemText` so filtering and the selected
119
+ * input value still resolve the option label.
120
+ * ---------------------------------------------------------------------- */
121
+
122
+ const teamMembers = [
123
+ { label: 'Alice Kim', value: 'alice', role: 'Product Designer' },
124
+ { label: 'Brian Lee', value: 'brian', role: 'Frontend Engineer' },
125
+ { label: 'Chloe Park', value: 'chloe', role: 'Backend Engineer' },
126
+ { label: 'David Choi', value: 'david', role: 'Engineering Manager' },
127
+ ];
128
+
129
+ /** Two-line options inside a filterable combobox. */
130
+ export const CustomItems = () => {
131
+ const { contains } = useFilter({ sensitivity: 'base' });
132
+ const { collection, filter } = useListCollection({
133
+ initialItems: teamMembers,
134
+ filter: contains,
135
+ });
136
+
137
+ return (
138
+ <Box maxW="320px">
139
+ <Combobox.Root
140
+ collection={collection}
141
+ onInputValueChange={(details) => filter(details.inputValue)}
142
+ >
143
+ <Combobox.Label>Assignee</Combobox.Label>
144
+ <Combobox.Control>
145
+ <Combobox.Input placeholder="Search team members" />
146
+ <Combobox.IndicatorGroup>
147
+ <Combobox.ClearTrigger />
148
+ <Combobox.Trigger />
149
+ </Combobox.IndicatorGroup>
150
+ </Combobox.Control>
151
+ <Portal>
152
+ <Combobox.Positioner>
153
+ <Combobox.Content>
154
+ <Combobox.Empty>No members found</Combobox.Empty>
155
+ {collection.items.map((item) => (
156
+ <Combobox.Item item={item} key={item.value}>
157
+ <Stack gap={0}>
158
+ <Combobox.ItemText>{item.label}</Combobox.ItemText>
159
+ <Span fontSize="xs" color="gray.600">
160
+ {item.role}
161
+ </Span>
162
+ </Stack>
163
+ <Combobox.ItemIndicator />
164
+ </Combobox.Item>
165
+ ))}
166
+ </Combobox.Content>
167
+ </Combobox.Positioner>
168
+ </Portal>
169
+ </Combobox.Root>
170
+ </Box>
171
+ );
172
+ };
@@ -0,0 +1,69 @@
1
+ import { forwardRef } from 'react';
2
+ import { Combobox as ChakraCombobox } from '@chakra-ui/react';
3
+
4
+ import { mergeCss } from '@/utils/mergeCss';
5
+
6
+ import { contentStyles, inputStyles, itemStyles } from './Select.styles';
7
+
8
+ const Input = forwardRef<HTMLInputElement, ChakraCombobox.InputProps>(
9
+ function ComboboxInput({ css, ...props }, ref) {
10
+ return (
11
+ <ChakraCombobox.Input
12
+ ref={ref}
13
+ {...props}
14
+ css={mergeCss(inputStyles, css)}
15
+ />
16
+ );
17
+ }
18
+ );
19
+
20
+ const Content = forwardRef<HTMLDivElement, ChakraCombobox.ContentProps>(
21
+ function ComboboxContent({ css, ...props }, ref) {
22
+ return (
23
+ <ChakraCombobox.Content
24
+ ref={ref}
25
+ {...props}
26
+ css={mergeCss(contentStyles, css)}
27
+ />
28
+ );
29
+ }
30
+ );
31
+
32
+ const Item = forwardRef<HTMLDivElement, ChakraCombobox.ItemProps>(
33
+ function ComboboxItem({ css, ...props }, ref) {
34
+ return (
35
+ <ChakraCombobox.Item
36
+ ref={ref}
37
+ {...props}
38
+ css={mergeCss(itemStyles, css)}
39
+ />
40
+ );
41
+ }
42
+ );
43
+
44
+ /**
45
+ * Compound Combobox primitives — Chakra's `Combobox` namespace with the
46
+ * Logician design-system styling baked into the visual parts (`Input`,
47
+ * `Content`, `Item`). Use these directly for full compositional control;
48
+ * use `ComboboxField` for the common single-select case.
49
+ */
50
+ export const Combobox = {
51
+ Root: ChakraCombobox.Root,
52
+ PropsProvider: ChakraCombobox.PropsProvider,
53
+ Context: ChakraCombobox.Context,
54
+ Label: ChakraCombobox.Label,
55
+ Control: ChakraCombobox.Control,
56
+ Input,
57
+ Trigger: ChakraCombobox.Trigger,
58
+ IndicatorGroup: ChakraCombobox.IndicatorGroup,
59
+ ClearTrigger: ChakraCombobox.ClearTrigger,
60
+ Positioner: ChakraCombobox.Positioner,
61
+ Content,
62
+ Empty: ChakraCombobox.Empty,
63
+ ItemGroup: ChakraCombobox.ItemGroup,
64
+ ItemGroupLabel: ChakraCombobox.ItemGroupLabel,
65
+ Item,
66
+ ItemText: ChakraCombobox.ItemText,
67
+ ItemIndicator: ChakraCombobox.ItemIndicator,
68
+ ItemContext: ChakraCombobox.ItemContext,
69
+ };
@@ -0,0 +1,86 @@
1
+ import { Portal, useFilter, useListCollection } from '@chakra-ui/react';
2
+
3
+ import { Combobox } from './Combobox';
4
+ import { ComboboxFieldProps, SelectOption } from './Select.types';
5
+
6
+ /**
7
+ * Single-select combobox — a Select with a typeable, filterable input.
8
+ * The ergonomic counterpart to `Input` for searchable option lists; pass
9
+ * `options`, `value` and `onChange` and it renders the full composition
10
+ * internally.
11
+ *
12
+ * For grouped options or custom layouts, compose the `Combobox` primitives
13
+ * directly.
14
+ */
15
+ export function ComboboxField<T = string>({
16
+ options,
17
+ value,
18
+ defaultValue,
19
+ onChange,
20
+ placeholder,
21
+ label,
22
+ size = 'md',
23
+ invalid,
24
+ disabled,
25
+ readOnly,
26
+ required,
27
+ name,
28
+ width = 'full',
29
+ emptyText = 'No results found',
30
+ }: ComboboxFieldProps<T>) {
31
+ const { contains } = useFilter({ sensitivity: 'base' });
32
+ const { collection, filter } = useListCollection<SelectOption<T>>({
33
+ initialItems: options,
34
+ filter: contains,
35
+ itemToValue: (item) => String(item.value),
36
+ itemToString: (item) => item.label,
37
+ isItemDisabled: (item) => Boolean(item.disabled),
38
+ });
39
+
40
+ const toKeys = (val: T | null | undefined) =>
41
+ val === null || val === undefined ? [] : [String(val)];
42
+
43
+ return (
44
+ <Combobox.Root
45
+ collection={collection}
46
+ size={size}
47
+ width={width}
48
+ invalid={invalid}
49
+ disabled={disabled}
50
+ readOnly={readOnly}
51
+ required={required}
52
+ name={name}
53
+ value={value === undefined ? undefined : toKeys(value)}
54
+ defaultValue={
55
+ defaultValue === undefined ? undefined : toKeys(defaultValue)
56
+ }
57
+ onValueChange={(details) => {
58
+ const selected = details.items[0];
59
+ onChange?.(selected ? selected.value : null);
60
+ }}
61
+ onInputValueChange={(details) => filter(details.inputValue)}
62
+ >
63
+ {label != null && <Combobox.Label>{label}</Combobox.Label>}
64
+ <Combobox.Control>
65
+ <Combobox.Input placeholder={placeholder} />
66
+ <Combobox.IndicatorGroup>
67
+ <Combobox.ClearTrigger />
68
+ <Combobox.Trigger />
69
+ </Combobox.IndicatorGroup>
70
+ </Combobox.Control>
71
+ <Portal>
72
+ <Combobox.Positioner>
73
+ <Combobox.Content>
74
+ <Combobox.Empty>{emptyText}</Combobox.Empty>
75
+ {collection.items.map((item) => (
76
+ <Combobox.Item item={item} key={String(item.value)}>
77
+ <Combobox.ItemText>{item.label}</Combobox.ItemText>
78
+ <Combobox.ItemIndicator />
79
+ </Combobox.Item>
80
+ ))}
81
+ </Combobox.Content>
82
+ </Combobox.Positioner>
83
+ </Portal>
84
+ </Combobox.Root>
85
+ );
86
+ }