@rovula/ui 0.0.77 → 0.0.79
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/dist/cjs/bundle.css +43 -3
- package/dist/cjs/bundle.js +3 -3
- package/dist/cjs/bundle.js.map +1 -1
- package/dist/cjs/types/components/Dropdown/Dropdown.d.ts +3 -0
- package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +5 -1
- package/dist/cjs/types/components/MaskedTextInput/MaskedTextInput.d.ts +75 -0
- package/dist/cjs/types/components/MaskedTextInput/MaskedTextInput.stories.d.ts +491 -0
- package/dist/cjs/types/components/MaskedTextInput/index.d.ts +3 -0
- package/dist/cjs/types/components/Menu/Menu.d.ts +65 -0
- package/dist/cjs/types/components/Menu/Menu.stories.d.ts +31 -0
- package/dist/cjs/types/components/Menu/helpers.d.ts +19 -0
- package/dist/cjs/types/components/Menu/index.d.ts +4 -0
- package/dist/cjs/types/components/Search/Search.d.ts +46 -3
- package/dist/cjs/types/components/Search/Search.stories.d.ts +46 -27
- package/dist/cjs/types/index.d.ts +3 -0
- package/dist/components/Dropdown/Dropdown.js +41 -19
- package/dist/components/Dropdown/Dropdown.stories.js +13 -0
- package/dist/components/MaskedTextInput/MaskedTextInput.js +267 -0
- package/dist/components/MaskedTextInput/MaskedTextInput.stories.js +167 -0
- package/dist/components/MaskedTextInput/index.js +2 -0
- package/dist/components/Menu/Menu.js +64 -0
- package/dist/components/Menu/Menu.stories.js +406 -0
- package/dist/components/Menu/helpers.js +28 -0
- package/dist/components/Menu/index.js +3 -0
- package/dist/components/Toast/Toast.styles.js +1 -1
- package/dist/esm/bundle.css +43 -3
- package/dist/esm/bundle.js +3 -3
- package/dist/esm/bundle.js.map +1 -1
- package/dist/esm/types/components/Dropdown/Dropdown.d.ts +3 -0
- package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +5 -1
- package/dist/esm/types/components/MaskedTextInput/MaskedTextInput.d.ts +75 -0
- package/dist/esm/types/components/MaskedTextInput/MaskedTextInput.stories.d.ts +491 -0
- package/dist/esm/types/components/MaskedTextInput/index.d.ts +3 -0
- package/dist/esm/types/components/Menu/Menu.d.ts +65 -0
- package/dist/esm/types/components/Menu/Menu.stories.d.ts +31 -0
- package/dist/esm/types/components/Menu/helpers.d.ts +19 -0
- package/dist/esm/types/components/Menu/index.d.ts +4 -0
- package/dist/esm/types/components/Search/Search.d.ts +46 -3
- package/dist/esm/types/components/Search/Search.stories.d.ts +46 -27
- package/dist/esm/types/index.d.ts +3 -0
- package/dist/index.d.ts +169 -3
- package/dist/index.js +2 -0
- package/dist/src/theme/global.css +55 -4
- package/package.json +1 -1
- package/src/components/Dropdown/Dropdown.stories.tsx +31 -0
- package/src/components/Dropdown/Dropdown.tsx +73 -54
- package/src/components/MaskedTextInput/MaskedTextInput.stories.tsx +414 -0
- package/src/components/MaskedTextInput/MaskedTextInput.tsx +391 -0
- package/src/components/MaskedTextInput/README.md +202 -0
- package/src/components/MaskedTextInput/index.ts +3 -0
- package/src/components/Menu/Menu.stories.tsx +586 -0
- package/src/components/Menu/Menu.tsx +235 -0
- package/src/components/Menu/helpers.ts +45 -0
- package/src/components/Menu/index.ts +7 -0
- package/src/components/Search/Search.tsx +24 -11
- package/src/components/Toast/Toast.styles.tsx +1 -1
- package/src/index.ts +6 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
CSSProperties,
|
|
3
|
+
ReactNode,
|
|
4
|
+
forwardRef,
|
|
5
|
+
useCallback,
|
|
6
|
+
useState,
|
|
7
|
+
} from "react";
|
|
8
|
+
import { cn } from "@/utils/cn";
|
|
9
|
+
import Icon from "../Icon/Icon";
|
|
10
|
+
|
|
11
|
+
// ==================== Types ====================
|
|
12
|
+
|
|
13
|
+
export type MenuOption = {
|
|
14
|
+
value: string;
|
|
15
|
+
label: ReactNode;
|
|
16
|
+
/**
|
|
17
|
+
* Visual type - กำหนดว่าจะแสดง icon อะไร
|
|
18
|
+
* - "default": ไม่มี icon (แค่ highlight background)
|
|
19
|
+
* - "checkbox": แสดง ✓ icon
|
|
20
|
+
* - "radio": แสดง ● icon
|
|
21
|
+
*/
|
|
22
|
+
type?: "default" | "checkbox" | "radio";
|
|
23
|
+
icon?: ReactNode;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
danger?: boolean;
|
|
26
|
+
checked?: boolean; // สำหรับ type="checkbox" หรือ "radio"
|
|
27
|
+
onClick?: () => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type MenuItemType =
|
|
31
|
+
| { type: "item"; item: MenuOption }
|
|
32
|
+
| { type: "separator" }
|
|
33
|
+
| { type: "label"; label: string }
|
|
34
|
+
| { type: "custom"; render: () => ReactNode };
|
|
35
|
+
|
|
36
|
+
export type MenuProps = {
|
|
37
|
+
items: MenuItemType[];
|
|
38
|
+
/**
|
|
39
|
+
* Selected values - ใช้กับ type="item"
|
|
40
|
+
*/
|
|
41
|
+
selectedValues?: string[];
|
|
42
|
+
/**
|
|
43
|
+
* Callback เมื่อเลือก item
|
|
44
|
+
* - ถ้า item.type="checkbox" → toggle checked state
|
|
45
|
+
* - ถ้า item.type="radio" → single select (clear others)
|
|
46
|
+
* - ถ้า item.type="default" หรือไม่ระบุ → ตาม selectedValues
|
|
47
|
+
*/
|
|
48
|
+
onSelect?: (value: string, item: MenuOption) => void;
|
|
49
|
+
className?: string;
|
|
50
|
+
style?: CSSProperties;
|
|
51
|
+
isAbove?: boolean;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ==================== Menu Container ====================
|
|
55
|
+
|
|
56
|
+
export const Menu = forwardRef<HTMLDivElement, MenuProps>(
|
|
57
|
+
(
|
|
58
|
+
{ items, selectedValues = [], onSelect, className, style, isAbove = false },
|
|
59
|
+
ref
|
|
60
|
+
) => {
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
ref={ref}
|
|
64
|
+
className={cn(
|
|
65
|
+
"z-50 min-w-[154px] overflow-hidden rounded-md bg-base-popup text-base-popup-foreground",
|
|
66
|
+
"border border-base-popup",
|
|
67
|
+
className
|
|
68
|
+
)}
|
|
69
|
+
style={{
|
|
70
|
+
boxShadow: "var(--dropdown-menu-shadow)",
|
|
71
|
+
...style,
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
{items.map((item, index) => {
|
|
75
|
+
if (item.type === "separator") {
|
|
76
|
+
return <MenuSeparator key={`separator-${index}`} />;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (item.type === "label") {
|
|
80
|
+
return <MenuLabel key={`label-${index}`}>{item.label}</MenuLabel>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (item.type === "custom") {
|
|
84
|
+
return (
|
|
85
|
+
<React.Fragment key={`custom-${index}`}>
|
|
86
|
+
{item.render()}
|
|
87
|
+
</React.Fragment>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const itemOption = item.item;
|
|
92
|
+
const visualType = itemOption.type || "default";
|
|
93
|
+
|
|
94
|
+
// Determine checked/selected state
|
|
95
|
+
let isChecked = false;
|
|
96
|
+
if (visualType === "checkbox" || visualType === "radio") {
|
|
97
|
+
isChecked =
|
|
98
|
+
itemOption.checked ?? selectedValues.includes(itemOption.value);
|
|
99
|
+
} else {
|
|
100
|
+
isChecked = selectedValues.includes(itemOption.value);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<MenuItem
|
|
105
|
+
key={itemOption.value}
|
|
106
|
+
option={itemOption}
|
|
107
|
+
visualType={visualType}
|
|
108
|
+
isChecked={isChecked}
|
|
109
|
+
onSelect={() => {
|
|
110
|
+
onSelect?.(itemOption.value, itemOption);
|
|
111
|
+
itemOption.onClick?.();
|
|
112
|
+
}}
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
})}
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
Menu.displayName = "Menu";
|
|
122
|
+
|
|
123
|
+
// ==================== Menu Item ====================
|
|
124
|
+
|
|
125
|
+
type MenuItemProps = {
|
|
126
|
+
option: MenuOption;
|
|
127
|
+
visualType: "default" | "checkbox" | "radio";
|
|
128
|
+
isChecked: boolean;
|
|
129
|
+
onSelect: () => void;
|
|
130
|
+
className?: string;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const MenuItem = forwardRef<HTMLDivElement, MenuItemProps>(
|
|
134
|
+
({ option, visualType, isChecked, onSelect, className }, ref) => {
|
|
135
|
+
// Render indicator based on visual type
|
|
136
|
+
const renderIndicator = () => {
|
|
137
|
+
if (visualType === "checkbox" && isChecked) {
|
|
138
|
+
return (
|
|
139
|
+
<span className="absolute left-4 flex items-center justify-center">
|
|
140
|
+
<Icon type="heroicons" name="check" className="size-4" />
|
|
141
|
+
</span>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (visualType === "radio" && isChecked) {
|
|
145
|
+
return (
|
|
146
|
+
<span className="absolute left-4 flex items-center justify-center">
|
|
147
|
+
<Icon
|
|
148
|
+
type="heroicons"
|
|
149
|
+
name="circle"
|
|
150
|
+
className="h-2 w-2 fill-current"
|
|
151
|
+
/>
|
|
152
|
+
</span>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div
|
|
160
|
+
ref={ref}
|
|
161
|
+
className={cn(
|
|
162
|
+
"relative flex gap-3 cursor-pointer select-none box-border items-center py-4 pl-9 pr-4 typography-subtitile4 outline-none transition-colors",
|
|
163
|
+
"bg-[var(--dropdown-menu-default-bg)] text-[var(--dropdown-menu-default-text)]",
|
|
164
|
+
"active:opacity-75",
|
|
165
|
+
"hover:bg-[var(--dropdown-menu-hover-bg)] hover:text-[var(--dropdown-menu-hover-text)]",
|
|
166
|
+
{
|
|
167
|
+
"bg-[var(--dropdown-menu-selected-bg)] text-[var(--dropdown-menu-selected-text)] typography-subtitile5":
|
|
168
|
+
isChecked,
|
|
169
|
+
"pointer-events-none opacity-50": option.disabled,
|
|
170
|
+
"text-red-500": option.danger,
|
|
171
|
+
},
|
|
172
|
+
className
|
|
173
|
+
)}
|
|
174
|
+
onClick={option.disabled ? undefined : onSelect}
|
|
175
|
+
>
|
|
176
|
+
{renderIndicator()}
|
|
177
|
+
{option.icon && <span className="flex-shrink-0">{option.icon}</span>}
|
|
178
|
+
{option.label}
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
MenuItem.displayName = "MenuItem";
|
|
185
|
+
|
|
186
|
+
// ==================== Menu Separator ====================
|
|
187
|
+
|
|
188
|
+
type MenuSeparatorProps = {
|
|
189
|
+
className?: string;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export const MenuSeparator = forwardRef<HTMLDivElement, MenuSeparatorProps>(
|
|
193
|
+
({ className }, ref) => {
|
|
194
|
+
return (
|
|
195
|
+
<div
|
|
196
|
+
ref={ref}
|
|
197
|
+
className={cn(
|
|
198
|
+
"-mx-2 my-2 h-px bg-[var(--dropdown-menu-seperator-bg)]",
|
|
199
|
+
className
|
|
200
|
+
)}
|
|
201
|
+
/>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
MenuSeparator.displayName = "MenuSeparator";
|
|
207
|
+
|
|
208
|
+
// ==================== Menu Label ====================
|
|
209
|
+
|
|
210
|
+
type MenuLabelProps = {
|
|
211
|
+
children: ReactNode;
|
|
212
|
+
className?: string;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export const MenuLabel = forwardRef<HTMLDivElement, MenuLabelProps>(
|
|
216
|
+
({ children, className }, ref) => {
|
|
217
|
+
return (
|
|
218
|
+
<div
|
|
219
|
+
ref={ref}
|
|
220
|
+
className={cn(
|
|
221
|
+
"px-3 py-2 typography-small4 text-text-grey-medium",
|
|
222
|
+
className
|
|
223
|
+
)}
|
|
224
|
+
>
|
|
225
|
+
{children}
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
MenuLabel.displayName = "MenuLabel";
|
|
232
|
+
|
|
233
|
+
// ==================== Exports ====================
|
|
234
|
+
|
|
235
|
+
export default Menu;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { MenuItemType, MenuOption } from "./Menu";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper function to convert simple options to MenuItemType
|
|
5
|
+
* Useful for integrating with Dropdown component
|
|
6
|
+
*/
|
|
7
|
+
export function optionsToMenuItems(
|
|
8
|
+
options: Array<{
|
|
9
|
+
value: string;
|
|
10
|
+
label: string | React.ReactNode;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
renderLabel?: any;
|
|
13
|
+
}>
|
|
14
|
+
): MenuItemType[] {
|
|
15
|
+
return options.map((option) => ({
|
|
16
|
+
type: "item" as const,
|
|
17
|
+
item: {
|
|
18
|
+
value: option.value,
|
|
19
|
+
label: option.label,
|
|
20
|
+
disabled: option.disabled,
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Helper to add separator between menu items
|
|
27
|
+
*/
|
|
28
|
+
export function withSeparator(
|
|
29
|
+
items: MenuItemType[],
|
|
30
|
+
atIndex: number
|
|
31
|
+
): MenuItemType[] {
|
|
32
|
+
const result = [...items];
|
|
33
|
+
result.splice(atIndex, 0, { type: "separator" });
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Helper to add label/header to menu items
|
|
39
|
+
*/
|
|
40
|
+
export function withLabel(
|
|
41
|
+
label: string,
|
|
42
|
+
items: MenuItemType[]
|
|
43
|
+
): MenuItemType[] {
|
|
44
|
+
return [{ type: "label", label }, ...items];
|
|
45
|
+
}
|
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
import React, { forwardRef } from "react";
|
|
2
|
-
import Dropdown, {
|
|
2
|
+
import Dropdown, { Options } from "../Dropdown/Dropdown";
|
|
3
|
+
import { InputProps } from "../TextInput/TextInput";
|
|
3
4
|
|
|
4
|
-
export type SearchProps =
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
| "
|
|
8
|
-
| "
|
|
9
|
-
| "
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
export type SearchProps = {
|
|
6
|
+
id?: string;
|
|
7
|
+
label?: string;
|
|
8
|
+
size?: "sm" | "md" | "lg";
|
|
9
|
+
rounded?: "none" | "normal" | "full";
|
|
10
|
+
variant?: "flat" | "outline" | "underline";
|
|
11
|
+
helperText?: string;
|
|
12
|
+
errorMessage?: string;
|
|
13
|
+
fullwidth?: boolean;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
error?: boolean;
|
|
16
|
+
required?: boolean;
|
|
17
|
+
modal?: boolean;
|
|
18
|
+
className?: string;
|
|
19
|
+
optionContainerClassName?: string;
|
|
20
|
+
optionItemClassName?: string;
|
|
21
|
+
optionNotFoundItemClassName?: string;
|
|
22
|
+
options: Options[];
|
|
23
|
+
value?: Options;
|
|
24
|
+
onChangeText?: InputProps["onChange"];
|
|
25
|
+
onSelect?: (value: Options) => void;
|
|
26
|
+
} & Omit<InputProps, "value" | "onSelect">;
|
|
14
27
|
|
|
15
28
|
const Search = forwardRef<HTMLInputElement, SearchProps>((props, ref) => {
|
|
16
29
|
return (
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import "./icons/iconConfig";
|
|
|
5
5
|
|
|
6
6
|
export { default as Button } from "./components/Button/Button";
|
|
7
7
|
export { default as TextInput } from "./components/TextInput/TextInput";
|
|
8
|
+
export { default as MaskedTextInput } from "./components/MaskedTextInput";
|
|
8
9
|
export { NumberInput } from "./components/NumberInput/NumberInput";
|
|
9
10
|
export { default as TextArea } from "./components/TextArea/TextArea";
|
|
10
11
|
export { default as Text } from "./components/Text/Text";
|
|
@@ -36,6 +37,7 @@ export * from "./components/InputFilter/InputFilter";
|
|
|
36
37
|
export * from "./components/Slider/Slider";
|
|
37
38
|
export * from "./components/Switch/Switch";
|
|
38
39
|
export * from "./components/DropdownMenu/DropdownMenu";
|
|
40
|
+
export * from "./components/Menu/Menu";
|
|
39
41
|
export * from "./components/Tooltip/Tooltip";
|
|
40
42
|
export * from "./components/Tooltip/TooltipSimple";
|
|
41
43
|
export * from "./components/Toast/Toast";
|
|
@@ -48,6 +50,10 @@ export * from "./components/RadioGroup/RadioGroup";
|
|
|
48
50
|
// Export component types
|
|
49
51
|
export type { ButtonProps } from "./components/Button/Button";
|
|
50
52
|
export type { InputProps } from "./components/TextInput/TextInput";
|
|
53
|
+
export type {
|
|
54
|
+
MaskedTextInputProps,
|
|
55
|
+
MaskRule,
|
|
56
|
+
} from "./components/MaskedTextInput";
|
|
51
57
|
export type { NumberInputProps } from "./components/NumberInput/NumberInput";
|
|
52
58
|
export type { TextAreaProps } from "./components/TextArea/TextArea";
|
|
53
59
|
export type { DropdownProps, Options } from "./components/Dropdown/Dropdown";
|