@rovula/ui 0.0.78 → 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.
Files changed (42) hide show
  1. package/dist/cjs/bundle.css +15 -3
  2. package/dist/cjs/bundle.js +3 -3
  3. package/dist/cjs/bundle.js.map +1 -1
  4. package/dist/cjs/types/components/Dropdown/Dropdown.d.ts +3 -0
  5. package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +5 -1
  6. package/dist/cjs/types/components/Menu/Menu.d.ts +65 -0
  7. package/dist/cjs/types/components/Menu/Menu.stories.d.ts +31 -0
  8. package/dist/cjs/types/components/Menu/helpers.d.ts +19 -0
  9. package/dist/cjs/types/components/Menu/index.d.ts +4 -0
  10. package/dist/cjs/types/components/Search/Search.d.ts +46 -3
  11. package/dist/cjs/types/components/Search/Search.stories.d.ts +46 -27
  12. package/dist/cjs/types/index.d.ts +1 -0
  13. package/dist/components/Dropdown/Dropdown.js +41 -19
  14. package/dist/components/Dropdown/Dropdown.stories.js +13 -0
  15. package/dist/components/Menu/Menu.js +64 -0
  16. package/dist/components/Menu/Menu.stories.js +406 -0
  17. package/dist/components/Menu/helpers.js +28 -0
  18. package/dist/components/Menu/index.js +3 -0
  19. package/dist/esm/bundle.css +15 -3
  20. package/dist/esm/bundle.js +3 -3
  21. package/dist/esm/bundle.js.map +1 -1
  22. package/dist/esm/types/components/Dropdown/Dropdown.d.ts +3 -0
  23. package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +5 -1
  24. package/dist/esm/types/components/Menu/Menu.d.ts +65 -0
  25. package/dist/esm/types/components/Menu/Menu.stories.d.ts +31 -0
  26. package/dist/esm/types/components/Menu/helpers.d.ts +19 -0
  27. package/dist/esm/types/components/Menu/index.d.ts +4 -0
  28. package/dist/esm/types/components/Search/Search.d.ts +46 -3
  29. package/dist/esm/types/components/Search/Search.stories.d.ts +46 -27
  30. package/dist/esm/types/index.d.ts +1 -0
  31. package/dist/index.d.ts +111 -3
  32. package/dist/index.js +1 -0
  33. package/dist/src/theme/global.css +20 -4
  34. package/package.json +1 -1
  35. package/src/components/Dropdown/Dropdown.stories.tsx +31 -0
  36. package/src/components/Dropdown/Dropdown.tsx +73 -54
  37. package/src/components/Menu/Menu.stories.tsx +586 -0
  38. package/src/components/Menu/Menu.tsx +235 -0
  39. package/src/components/Menu/helpers.ts +45 -0
  40. package/src/components/Menu/index.ts +7 -0
  41. package/src/components/Search/Search.tsx +24 -11
  42. package/src/index.ts +1 -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
+ }
@@ -0,0 +1,7 @@
1
+ export { Menu, MenuItem, MenuSeparator, MenuLabel } from "./Menu";
2
+
3
+ export type { MenuOption, MenuItemType, MenuProps } from "./Menu";
4
+
5
+ export { optionsToMenuItems, withSeparator, withLabel } from "./helpers";
6
+
7
+ export { default } from "./Menu";
@@ -1,16 +1,29 @@
1
1
  import React, { forwardRef } from "react";
2
- import Dropdown, { DropdownProps } from "../Dropdown/Dropdown";
2
+ import Dropdown, { Options } from "../Dropdown/Dropdown";
3
+ import { InputProps } from "../TextInput/TextInput";
3
4
 
4
- export type SearchProps = Omit<
5
- DropdownProps,
6
- | "isFloatingLabel"
7
- | "keepCloseIconOnValue"
8
- | "hasClearIcon"
9
- | "hasSearchIcon"
10
- | "endIcon"
11
- | "filterMode"
12
- | "isFloatingLabel"
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
@@ -37,6 +37,7 @@ export * from "./components/InputFilter/InputFilter";
37
37
  export * from "./components/Slider/Slider";
38
38
  export * from "./components/Switch/Switch";
39
39
  export * from "./components/DropdownMenu/DropdownMenu";
40
+ export * from "./components/Menu/Menu";
40
41
  export * from "./components/Tooltip/Tooltip";
41
42
  export * from "./components/Tooltip/TooltipSimple";
42
43
  export * from "./components/Toast/Toast";