@rovula/ui 0.1.20 → 0.1.22

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 (99) hide show
  1. package/dist/cjs/bundle.css +316 -43
  2. package/dist/cjs/bundle.js +675 -675
  3. package/dist/cjs/bundle.js.map +1 -1
  4. package/dist/cjs/types/components/Badge/Badge.d.ts +40 -0
  5. package/dist/cjs/types/components/Badge/Badge.stories.d.ts +295 -0
  6. package/dist/cjs/types/components/Badge/Badge.styles.d.ts +7 -0
  7. package/dist/cjs/types/components/Badge/index.d.ts +2 -0
  8. package/dist/cjs/types/components/Dropdown/Dropdown.d.ts +4 -8
  9. package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +1 -6
  10. package/dist/cjs/types/components/DropdownMenu/DropdownMenu.d.ts +5 -1
  11. package/dist/cjs/types/components/DropdownMenu/DropdownMenu.stories.d.ts +45 -30
  12. package/dist/cjs/types/components/Form/Form.d.ts +2 -1
  13. package/dist/cjs/types/components/Form/Form.stories.d.ts +4 -0
  14. package/dist/cjs/types/components/ScrollArea/ScrollArea.d.ts +38 -0
  15. package/dist/cjs/types/components/ScrollArea/ScrollArea.stories.d.ts +301 -0
  16. package/dist/cjs/types/index.d.ts +4 -1
  17. package/dist/cjs/types/patterns/menu/Menu.d.ts +70 -0
  18. package/dist/cjs/types/{components/Menu → patterns/menu}/Menu.stories.d.ts +17 -10
  19. package/dist/cjs/types/utils/mergeRefs.d.ts +20 -0
  20. package/dist/components/Avatar/Avatar.styles.js +2 -2
  21. package/dist/components/Badge/Badge.js +36 -0
  22. package/dist/components/Badge/Badge.stories.js +51 -0
  23. package/dist/components/Badge/Badge.styles.js +62 -0
  24. package/dist/components/Badge/index.js +2 -0
  25. package/dist/components/Dropdown/Dropdown.js +54 -163
  26. package/dist/components/Dropdown/Dropdown.stories.js +29 -0
  27. package/dist/components/DropdownMenu/DropdownMenu.js +24 -13
  28. package/dist/components/DropdownMenu/DropdownMenu.stories.js +120 -88
  29. package/dist/components/Form/Form.js +11 -4
  30. package/dist/components/Form/Form.stories.js +27 -0
  31. package/dist/components/ScrollArea/ScrollArea.js +50 -0
  32. package/dist/components/ScrollArea/ScrollArea.stories.js +56 -0
  33. package/dist/components/TextInput/TextInput.js +6 -3
  34. package/dist/esm/bundle.css +316 -43
  35. package/dist/esm/bundle.js +1545 -1545
  36. package/dist/esm/bundle.js.map +1 -1
  37. package/dist/esm/types/components/Badge/Badge.d.ts +40 -0
  38. package/dist/esm/types/components/Badge/Badge.stories.d.ts +295 -0
  39. package/dist/esm/types/components/Badge/Badge.styles.d.ts +7 -0
  40. package/dist/esm/types/components/Badge/index.d.ts +2 -0
  41. package/dist/esm/types/components/Dropdown/Dropdown.d.ts +4 -8
  42. package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +1 -6
  43. package/dist/esm/types/components/DropdownMenu/DropdownMenu.d.ts +5 -1
  44. package/dist/esm/types/components/DropdownMenu/DropdownMenu.stories.d.ts +45 -30
  45. package/dist/esm/types/components/Form/Form.d.ts +2 -1
  46. package/dist/esm/types/components/Form/Form.stories.d.ts +4 -0
  47. package/dist/esm/types/components/ScrollArea/ScrollArea.d.ts +38 -0
  48. package/dist/esm/types/components/ScrollArea/ScrollArea.stories.d.ts +301 -0
  49. package/dist/esm/types/index.d.ts +4 -1
  50. package/dist/esm/types/patterns/menu/Menu.d.ts +70 -0
  51. package/dist/esm/types/{components/Menu → patterns/menu}/Menu.stories.d.ts +17 -10
  52. package/dist/esm/types/utils/mergeRefs.d.ts +20 -0
  53. package/dist/index.d.ts +156 -74
  54. package/dist/index.js +3 -1
  55. package/dist/patterns/menu/Menu.js +95 -0
  56. package/dist/patterns/menu/Menu.stories.js +611 -0
  57. package/dist/src/theme/global.css +485 -57
  58. package/dist/utils/mergeRefs.js +42 -0
  59. package/package.json +1 -1
  60. package/src/components/Avatar/Avatar.styles.ts +2 -2
  61. package/src/components/Badge/Badge.stories.tsx +128 -0
  62. package/src/components/Badge/Badge.styles.ts +70 -0
  63. package/src/components/Badge/Badge.tsx +103 -0
  64. package/src/components/Badge/index.ts +3 -0
  65. package/src/components/Dropdown/Dropdown.stories.tsx +170 -1
  66. package/src/components/Dropdown/Dropdown.tsx +186 -276
  67. package/src/components/DropdownMenu/DropdownMenu.stories.tsx +1375 -253
  68. package/src/components/DropdownMenu/DropdownMenu.tsx +118 -55
  69. package/src/components/Form/Form.stories.tsx +70 -0
  70. package/src/components/Form/Form.tsx +23 -0
  71. package/src/components/ScrollArea/ScrollArea.stories.tsx +229 -0
  72. package/src/components/ScrollArea/ScrollArea.tsx +72 -0
  73. package/src/components/TextInput/TextInput.tsx +6 -3
  74. package/src/index.ts +4 -1
  75. package/src/patterns/menu/Menu.stories.tsx +1100 -0
  76. package/src/patterns/menu/Menu.tsx +282 -0
  77. package/src/theme/global.css +84 -11
  78. package/src/theme/themes/xspector/baseline.css +1 -1
  79. package/src/theme/themes/xspector/components/scrollbar.css +12 -0
  80. package/src/theme/tokens/baseline.css +3 -1
  81. package/src/theme/tokens/components/badge.css +54 -0
  82. package/src/theme/tokens/components/dropdown-menu.css +16 -5
  83. package/src/theme/tokens/components/scrollbar.css +18 -0
  84. package/src/utils/mergeRefs.ts +46 -0
  85. package/dist/cjs/types/components/Menu/Menu.d.ts +0 -65
  86. package/dist/cjs/types/components/Menu/helpers.d.ts +0 -19
  87. package/dist/cjs/types/components/Menu/index.d.ts +0 -4
  88. package/dist/components/Menu/Menu.js +0 -64
  89. package/dist/components/Menu/Menu.stories.js +0 -406
  90. package/dist/components/Menu/helpers.js +0 -28
  91. package/dist/components/Menu/index.js +0 -3
  92. package/dist/esm/types/components/Menu/Menu.d.ts +0 -65
  93. package/dist/esm/types/components/Menu/helpers.d.ts +0 -19
  94. package/dist/esm/types/components/Menu/index.d.ts +0 -4
  95. package/src/components/Menu/Menu.stories.tsx +0 -586
  96. package/src/components/Menu/Menu.tsx +0 -235
  97. package/src/components/Menu/helpers.ts +0 -45
  98. package/src/components/Menu/index.ts +0 -7
  99. package/src/theme/themes/xspector/components/dropdown-menu.css +0 -28
@@ -0,0 +1,282 @@
1
+ "use client";
2
+
3
+ import React, { ReactNode } from "react";
4
+ import { cn } from "@/utils/cn";
5
+ import {
6
+ DropdownMenu as DropdownMenuRoot,
7
+ DropdownMenuTrigger,
8
+ DropdownMenuContent,
9
+ DropdownMenuItem,
10
+ DropdownMenuCheckboxItem,
11
+ DropdownMenuRadioItem,
12
+ DropdownMenuRadioGroup,
13
+ DropdownMenuSeparator,
14
+ DropdownMenuLabel,
15
+ DropdownMenuSub,
16
+ DropdownMenuSubTrigger,
17
+ DropdownMenuSubContent,
18
+ } from "../../components/DropdownMenu/DropdownMenu";
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Types
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export type MenuOption = {
25
+ value: string;
26
+ label: ReactNode;
27
+ /**
28
+ * Visual type of the item
29
+ * - "default": selected indicator (check icon on left)
30
+ * - "checkbox": visual checkbox
31
+ * - "radio": radio selection (single-select in group)
32
+ */
33
+ type?: "default" | "checkbox" | "radio";
34
+ icon?: ReactNode;
35
+ disabled?: boolean;
36
+ danger?: boolean;
37
+ checked?: boolean;
38
+ onClick?: () => void;
39
+ };
40
+
41
+ export type MenuItemType =
42
+ | { type: "item"; item: MenuOption }
43
+ | { type: "separator" }
44
+ | { type: "label"; label: string }
45
+ | { type: "custom"; render: () => ReactNode }
46
+ | {
47
+ type: "submenu";
48
+ label: string;
49
+ icon?: ReactNode;
50
+ disabled?: boolean;
51
+ /** Nested items — rendered via DropdownMenuSub */
52
+ items: MenuItemType[];
53
+ };
54
+
55
+ export type MenuProps = {
56
+ /** Trigger element — optional, ถ้าไม่ส่งให้ใช้ open/onOpenChange ควบคุมเอง */
57
+ trigger?: ReactNode;
58
+ items: MenuItemType[];
59
+ selectedValues?: string[];
60
+ /**
61
+ * Callback on item select
62
+ * - "checkbox" → caller toggles in selectedValues
63
+ * - "radio" → caller sets single value in selectedValues
64
+ * - "default" → same
65
+ */
66
+ onSelect?: (value: string, item: MenuOption) => void;
67
+ /**
68
+ * Optional header rendered above the item list — stays fixed while list scrolls.
69
+ * ใช้สำหรับ pattern เช่น "Change Status" (title + close button)
70
+ * หรือ "Manage Column" (title + Hide all / Show all / Done)
71
+ */
72
+ header?: ReactNode;
73
+ /** Controlled open state */
74
+ open?: boolean;
75
+ /** Called when open state changes (controlled or uncontrolled) */
76
+ onOpenChange?: (open: boolean) => void;
77
+ /** Alignment of the menu content relative to the trigger */
78
+ align?: "start" | "center" | "end";
79
+ side?: "top" | "right" | "bottom" | "left";
80
+ sideOffset?: number;
81
+ contentClassName?: string;
82
+ };
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // renderMenuItems — recursive, used for both root and submenu levels
86
+ // ---------------------------------------------------------------------------
87
+
88
+ function renderMenuItems(
89
+ items: MenuItemType[],
90
+ selectedValues: string[],
91
+ onSelect?: MenuProps["onSelect"],
92
+ ): ReactNode[] {
93
+ const result: ReactNode[] = [];
94
+ let radioBuffer: MenuOption[] = [];
95
+
96
+ const flushRadioBuffer = (key: string) => {
97
+ if (radioBuffer.length === 0) return;
98
+ const buffer = radioBuffer;
99
+ radioBuffer = [];
100
+
101
+ const radioValue =
102
+ buffer.find((o) => selectedValues.includes(o.value))?.value ?? "";
103
+
104
+ result.push(
105
+ <DropdownMenuRadioGroup
106
+ key={key}
107
+ value={radioValue}
108
+ onValueChange={(val: string) => {
109
+ const opt = buffer.find((o) => o.value === val);
110
+ if (opt) {
111
+ onSelect?.(val, opt);
112
+ opt.onClick?.();
113
+ }
114
+ }}
115
+ >
116
+ {buffer.map((opt) => (
117
+ <DropdownMenuRadioItem
118
+ key={opt.value}
119
+ value={opt.value}
120
+ disabled={opt.disabled}
121
+ icon={opt.icon as React.ReactNode}
122
+ >
123
+ {opt.label}
124
+ </DropdownMenuRadioItem>
125
+ ))}
126
+ </DropdownMenuRadioGroup>,
127
+ );
128
+ };
129
+
130
+ items.forEach((item, index) => {
131
+ // Accumulate consecutive radio items
132
+ if (item.type === "item" && item.item.type === "radio") {
133
+ radioBuffer.push(item.item);
134
+ return;
135
+ }
136
+
137
+ // Flush radio buffer before non-radio item
138
+ flushRadioBuffer(`radio-${index}`);
139
+
140
+ if (item.type === "separator") {
141
+ result.push(<DropdownMenuSeparator key={`sep-${index}`} />);
142
+ return;
143
+ }
144
+
145
+ if (item.type === "label") {
146
+ result.push(
147
+ <DropdownMenuLabel key={`lbl-${index}`}>{item.label}</DropdownMenuLabel>,
148
+ );
149
+ return;
150
+ }
151
+
152
+ if (item.type === "custom") {
153
+ result.push(
154
+ <React.Fragment key={`custom-${index}`}>{item.render()}</React.Fragment>,
155
+ );
156
+ return;
157
+ }
158
+
159
+ if (item.type === "submenu") {
160
+ result.push(
161
+ <DropdownMenuSub key={`sub-${index}`}>
162
+ <DropdownMenuSubTrigger disabled={item.disabled}>
163
+ {/* mirror DropdownMenuItem icon layout */}
164
+ <div className="flex shrink-0 flex-row gap-1">
165
+ <span className="size-4" />
166
+ {item.icon}
167
+ </div>
168
+ {item.label}
169
+ </DropdownMenuSubTrigger>
170
+ <DropdownMenuSubContent>
171
+ {renderMenuItems(item.items, selectedValues, onSelect)}
172
+ </DropdownMenuSubContent>
173
+ </DropdownMenuSub>,
174
+ );
175
+ return;
176
+ }
177
+
178
+ if (item.type === "item") {
179
+ const opt = item.item;
180
+ const isSelected = opt.checked ?? selectedValues.includes(opt.value);
181
+
182
+ if (opt.type === "checkbox") {
183
+ result.push(
184
+ <DropdownMenuCheckboxItem
185
+ key={opt.value}
186
+ checked={isSelected}
187
+ disabled={opt.disabled}
188
+ onCheckedChange={() => {
189
+ onSelect?.(opt.value, opt);
190
+ opt.onClick?.();
191
+ }}
192
+ >
193
+ {opt.label}
194
+ </DropdownMenuCheckboxItem>,
195
+ );
196
+ return;
197
+ }
198
+
199
+ // default item
200
+ result.push(
201
+ <DropdownMenuItem
202
+ key={opt.value}
203
+ selected={isSelected}
204
+ icon={opt.icon as React.ReactNode}
205
+ disabled={opt.disabled}
206
+ className={cn(opt.danger && "text-red-500")}
207
+ onSelect={() => {
208
+ onSelect?.(opt.value, opt);
209
+ opt.onClick?.();
210
+ }}
211
+ >
212
+ {opt.label}
213
+ </DropdownMenuItem>,
214
+ );
215
+ }
216
+ });
217
+
218
+ flushRadioBuffer("radio-end");
219
+ return result;
220
+ }
221
+
222
+ // ---------------------------------------------------------------------------
223
+ // Menu — top-level pattern component built on DropdownMenu
224
+ //
225
+ // ใช้งาน:
226
+ // <Menu trigger={<Button>Open</Button>} items={[...]} onSelect={...} />
227
+ //
228
+ // ได้ของฟรีจาก DropdownMenu:
229
+ // - Radix positioning (flip, scroll, sideOffset)
230
+ // - Portal (z-index, escape stacking context)
231
+ // - Escape to close, click-outside to close
232
+ // - Keyboard navigation (Arrow, Enter, Escape)
233
+ // - Full a11y / WAI-ARIA
234
+ // - Sub-menu support via DropdownMenuSub
235
+ // ---------------------------------------------------------------------------
236
+
237
+ export const Menu = ({
238
+ trigger,
239
+ items,
240
+ selectedValues = [],
241
+ onSelect,
242
+ header,
243
+ open,
244
+ onOpenChange,
245
+ align = "start",
246
+ side = "bottom",
247
+ sideOffset = 4,
248
+ contentClassName,
249
+ }: MenuProps) => (
250
+ <DropdownMenuRoot open={open} onOpenChange={onOpenChange}>
251
+ {trigger && (
252
+ <DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
253
+ )}
254
+ <DropdownMenuContent
255
+ align={align}
256
+ side={side}
257
+ sideOffset={sideOffset}
258
+ className={contentClassName}
259
+ >
260
+ {header && (
261
+ <div className="sticky top-0 z-10 bg-modal-surface border-b border-[var(--dropdown-menu-seperator-bg)]">
262
+ {header}
263
+ </div>
264
+ )}
265
+ {renderMenuItems(items, selectedValues, onSelect)}
266
+ </DropdownMenuContent>
267
+ </DropdownMenuRoot>
268
+ );
269
+
270
+ Menu.displayName = "Menu";
271
+
272
+ // ---------------------------------------------------------------------------
273
+ // Re-exports — backward compat for consumers using Menu's sub-components
274
+ // ---------------------------------------------------------------------------
275
+
276
+ export {
277
+ DropdownMenuItem as MenuItem,
278
+ DropdownMenuSeparator as MenuSeparator,
279
+ DropdownMenuLabel as MenuLabel,
280
+ } from "../../components/DropdownMenu/DropdownMenu";
281
+
282
+ export default Menu;
@@ -14,7 +14,37 @@
14
14
  @layer base {
15
15
  * {
16
16
  /* @apply border-border; */
17
+ scrollbar-width: thin;
18
+ scrollbar-color: var(--scrollbar-thumb-default-color) transparent;
17
19
  }
20
+
21
+ *::-webkit-scrollbar {
22
+ width: var(--scrollbar-s-thickness);
23
+ height: var(--scrollbar-s-thickness);
24
+ background: transparent;
25
+ }
26
+
27
+ *::-webkit-scrollbar-thumb {
28
+ border-radius: 12px;
29
+ background: var(--scrollbar-thumb-default-color);
30
+ }
31
+
32
+ *::-webkit-scrollbar-thumb:hover {
33
+ background: var(--scrollbar-thumb-hover-color);
34
+ }
35
+
36
+ *::-webkit-scrollbar-track:vertical {
37
+ border-left: var(--scrollbar-track-border-width) solid var(--scrollbar-track-border-color);
38
+ }
39
+
40
+ *::-webkit-scrollbar-track:horizontal {
41
+ border-top: var(--scrollbar-track-border-width) solid var(--scrollbar-track-border-color);
42
+ }
43
+
44
+ *::-webkit-scrollbar-corner {
45
+ background: transparent;
46
+ }
47
+
18
48
  body {
19
49
  /* @apply bg-background text-foreground; */
20
50
  /* @apply bg-[var(--background)] text-[var(--foreground)]; */
@@ -36,26 +66,69 @@
36
66
 
37
67
  @layer utilities {
38
68
 
69
+ /* ------------------------------------------------------------------ */
70
+ /* Scrollbar utility — applies the design-system scrollbar style */
71
+ /* Default size: S (6px thumb, matches Figma Size=S) */
72
+ /* Usage: add `ui-scrollbar` + optional size modifier to any */
73
+ /* overflow-auto / overflow-y-auto / overflow-x-auto container. */
74
+ /* ------------------------------------------------------------------ */
75
+
76
+ /* --- Track --- */
39
77
  .ui-scrollbar::-webkit-scrollbar {
40
- height: 6px;
41
- width: 6px;
42
- background: rgba(0, 0, 0, 0.08);
78
+ width: var(--scrollbar-s-thickness);
79
+ height: var(--scrollbar-s-thickness);
80
+ background: transparent;
43
81
  }
44
-
82
+
83
+ /* --- Thumb --- */
45
84
  .ui-scrollbar::-webkit-scrollbar-thumb {
46
- border-radius: 6px;
47
- background: rgba(121, 141, 150, 0.48);
48
- width: 6px;
85
+ border-radius: 12px;
86
+ background: var(--scrollbar-thumb-default-color);
49
87
  }
50
-
88
+
51
89
  .ui-scrollbar::-webkit-scrollbar-thumb:hover {
52
- background: rgba(251, 252, 253, 0.48);
90
+ background: var(--scrollbar-thumb-hover-color);
53
91
  cursor: pointer;
54
92
  }
55
93
 
94
+ /* --- Track border (vertical) --- */
95
+ .ui-scrollbar::-webkit-scrollbar-track:vertical {
96
+ border-left: var(--scrollbar-track-border-width) solid var(--scrollbar-track-border-color);
97
+ }
98
+
99
+ /* --- Track border (horizontal) --- */
100
+ .ui-scrollbar::-webkit-scrollbar-track:horizontal {
101
+ border-top: var(--scrollbar-track-border-width) solid var(--scrollbar-track-border-color);
102
+ }
103
+
56
104
  .ui-scrollbar::-webkit-scrollbar-corner {
57
- display: none;
58
- /* background: transparent; */
105
+ background: transparent;
106
+ }
107
+
108
+ /* --- Size variants --- */
109
+
110
+ /* Size M — 12px */
111
+ .ui-scrollbar-m::-webkit-scrollbar {
112
+ width: var(--scrollbar-m-thickness);
113
+ height: var(--scrollbar-m-thickness);
114
+ }
115
+
116
+ /* Size S — 6px (default, same as base .ui-scrollbar) */
117
+ .ui-scrollbar-s::-webkit-scrollbar {
118
+ width: var(--scrollbar-s-thickness);
119
+ height: var(--scrollbar-s-thickness);
120
+ }
121
+
122
+ /* Size XS — 2px */
123
+ .ui-scrollbar-xs::-webkit-scrollbar {
124
+ width: var(--scrollbar-xs-thickness);
125
+ height: var(--scrollbar-xs-thickness);
126
+ }
127
+
128
+ /* XS size has no track border */
129
+ .ui-scrollbar-xs::-webkit-scrollbar-track:vertical,
130
+ .ui-scrollbar-xs::-webkit-scrollbar-track:horizontal {
131
+ border: none;
59
132
  }
60
133
 
61
134
  }
@@ -1,4 +1,4 @@
1
1
  @import url(typography.css);
2
2
  @import url(components/action-button.css);
3
3
  @import url(components/loading.css);
4
- @import url(components/dropdown-menu.css);
4
+ @import url(components/scrollbar.css);
@@ -0,0 +1,12 @@
1
+ :root[data-theme="xspector"] {
2
+ /* ------------------------------------------------------------------ */
3
+ /* Scrollbar Component Tokens — Xspector Theme */
4
+ /* ------------------------------------------------------------------ */
5
+
6
+ /* Track border uses the same transparent-grey token as other surfaces */
7
+ --scrollbar-track-border-color: var(--transparent-grey-16, rgba(158, 158, 158, 0.16));
8
+
9
+ /* Thumb — inherits design token colours from baseline */
10
+ --scrollbar-thumb-default-color: color-mix(in srgb, var(--text-g-contrast-low) 48%, transparent);
11
+ --scrollbar-thumb-hover-color: color-mix(in srgb, var(--text-g-contrast-high) 48%, transparent);
12
+ }
@@ -7,4 +7,6 @@
7
7
  @import url(components/navbar.css);
8
8
  @import url(components/footer.css);
9
9
  @import url(components/dropdown-menu.css);
10
- @import url(components/switch.css);
10
+ @import url(components/switch.css);
11
+ @import url(components/scrollbar.css);
12
+ @import url(components/badge.css);
@@ -0,0 +1,54 @@
1
+ :root {
2
+ /* --------------------------------------------------------------------------------- */
3
+ /* Badge Component Tokens */
4
+ /* --------------------------------------------------------------------------------- */
5
+ /* Naming Convention: --badge-[color]-[property] */
6
+ /* Colors: [default, success, warning, info, error] */
7
+ /* Severity: [highest, high, medium, low, lowest] */
8
+ /* --------------------------------------------------------------------------------- */
9
+
10
+ /* ------------------------------------------------------------------ */
11
+ /* Status Badge — Default (grey/white) */
12
+ /* ------------------------------------------------------------------ */
13
+ --badge-default-bg: rgba(145, 158, 171, 0.12);
14
+ --badge-default-border: rgba(158, 158, 158, 0.48);
15
+ --badge-default-text: var(--text-contrast-max, white);
16
+
17
+ /* ------------------------------------------------------------------ */
18
+ /* Status Badge — Success (green) */
19
+ /* ------------------------------------------------------------------ */
20
+ --badge-success-bg: rgba(84, 214, 44, 0.12);
21
+ --badge-success-border: rgba(84, 214, 44, 0.48);
22
+ --badge-success-text: var(--state-success-default);
23
+
24
+ /* ------------------------------------------------------------------ */
25
+ /* Status Badge — Warning (yellow) */
26
+ /* ------------------------------------------------------------------ */
27
+ --badge-warning-bg: rgba(255, 193, 7, 0.12);
28
+ --badge-warning-border: rgba(255, 193, 7, 0.48);
29
+ --badge-warning-text: var(--state-warning-default);
30
+
31
+ /* ------------------------------------------------------------------ */
32
+ /* Status Badge — Info (blue) */
33
+ /* ------------------------------------------------------------------ */
34
+ --badge-info-bg: rgba(41, 152, 255, 0.12);
35
+ --badge-info-border: rgba(41, 152, 255, 0.48);
36
+ --badge-info-text: var(--state-info-default);
37
+
38
+ /* ------------------------------------------------------------------ */
39
+ /* Status Badge — Error (red) */
40
+ /* ------------------------------------------------------------------ */
41
+ --badge-error-bg: rgba(255, 72, 66, 0.12);
42
+ --badge-error-border: rgba(255, 72, 66, 0.48);
43
+ --badge-error-text: var(--state-error-default);
44
+
45
+ /* ------------------------------------------------------------------ */
46
+ /* Severity Badge — solid pill */
47
+ /* ------------------------------------------------------------------ */
48
+ --badge-severity-text: var(--text-white, white);
49
+ --badge-severity-highest-bg: var(--state-error-default);
50
+ --badge-severity-high-bg: var(--ramps-warning-800);
51
+ --badge-severity-medium-bg: var(--state-warning-default);
52
+ --badge-severity-low-bg: var(--state-success-default);
53
+ --badge-severity-lowest-bg: var(--state-info-default);
54
+ }
@@ -6,22 +6,33 @@
6
6
  /* Element: [default, hover, selected, disabled] */
7
7
  /* ------------------------------------------------------------------ */
8
8
 
9
- --dropdown-menu-seperator-bg: var(--base-color-workspace-stroke);
10
- --dropdown-menu-shadow: 0px 2px 24px 0px rgba(145, 158, 171, 0.24);
9
+ --dropdown-menu-seperator-bg: var(--modal-line);
10
+ --dropdown-menu-shadow: 0px 12px 24px -4px rgba(0, 0, 0, 0.12);
11
11
 
12
12
  /* Default State */
13
13
  --dropdown-menu-default-bg: transparent;
14
14
  --dropdown-menu-default-text: var(--text-g-contrast-high);
15
15
 
16
16
  /* Hover State */
17
- --dropdown-menu-hover-bg: var(--state-primary-hover-bg);
17
+ --dropdown-menu-hover-bg: var(--modal-highlight);
18
18
  --dropdown-menu-hover-text: var(--text-g-contrast-high);
19
19
 
20
20
  /* Selected State */
21
- --dropdown-menu-selected-bg: transparent;
21
+ --dropdown-menu-selected-bg: var(--modal-highlight);
22
22
  --dropdown-menu-selected-text: var(--text-g-contrast-high);
23
23
 
24
24
  /* Disabled State */
25
25
  --dropdown-menu-disabled-bg: transparent;
26
- --dropdown-menu-disabled-text: var(--state-disable-outline);
26
+ --dropdown-menu-disabled-text: var(--state-disable-solid);
27
+
28
+ /* Checkbox Item */
29
+ --dropdown-menu-checkbox-border: var(--function-default-solid);
30
+ --dropdown-menu-checkbox-checked-bg: var(--function-active-solid);
31
+ --dropdown-menu-checkbox-checked-icon: var(--function-active-icon);
32
+ --dropdown-menu-checkbox-disabled-border: var(--state-disable-outline);
33
+ --dropdown-menu-checkbox-disabled-checked-bg: var(--state-disable-solid);
34
+
35
+ /* Radio Item */
36
+ --dropdown-menu-radio-disabled-text: var(--state-disable-solid);
37
+ --dropdown-menu-radio-selected-disabled-text: var(--state-disable-outline);
27
38
  }
@@ -0,0 +1,18 @@
1
+ :root {
2
+ /* ------------------------------------------------------------------ */
3
+ /* Scrollbar Component Tokens */
4
+ /* ------------------------------------------------------------------ */
5
+
6
+ /* Thumb sizes (track width/height) */
7
+ --scrollbar-m-thickness: 12px;
8
+ --scrollbar-s-thickness: 6px;
9
+ --scrollbar-xs-thickness: 2px;
10
+
11
+ /* Track border — visible on M size only */
12
+ --scrollbar-track-border-width: 1px;
13
+ --scrollbar-track-border-color: rgba(158, 158, 158, 0.16);
14
+
15
+ /* Thumb colours */
16
+ --scrollbar-thumb-default-color: color-mix(in srgb, var(--text-g-contrast-low) 48%, transparent);
17
+ --scrollbar-thumb-hover-color: color-mix(in srgb, var(--text-g-contrast-high) 48%, transparent);
18
+ }
@@ -0,0 +1,46 @@
1
+ import React, { useCallback, useRef } from "react";
2
+
3
+ /**
4
+ * Merges multiple refs into a single callback ref.
5
+ * NOTE: This creates a new function on every call — do NOT use inline in render.
6
+ * Use `useStableMergedRef` instead when you need a stable ref identity.
7
+ */
8
+ export function mergeRefs<T>(
9
+ ...refs: (React.Ref<T> | undefined | null)[]
10
+ ): React.RefCallback<T> {
11
+ return (node) => {
12
+ refs.forEach((ref) => {
13
+ if (!ref) return;
14
+ if (typeof ref === "function") {
15
+ ref(node);
16
+ } else {
17
+ (ref as React.MutableRefObject<T | null>).current = node;
18
+ }
19
+ });
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Returns a **stable** callback ref (never changes identity) that forwards the
25
+ * node to all given refs. Safe to use inline in JSX — will not cause
26
+ * detach/re-attach loops in libraries like Headless UI that watch refs.
27
+ *
28
+ * @example
29
+ * const MyInput = forwardRef((props, ref) => {
30
+ * const internalRef = useRef(null);
31
+ * const stableRef = useStableMergedRef(ref, internalRef);
32
+ * return <input ref={stableRef} />;
33
+ * });
34
+ */
35
+ export function useStableMergedRef<T>(
36
+ ...refs: (React.Ref<T> | undefined | null)[]
37
+ ): React.RefCallback<T> {
38
+ // Store the latest merge logic in a ref so the callback never goes stale,
39
+ // while the callback itself keeps a stable identity (empty useCallback deps).
40
+ const latestImpl = useRef<React.RefCallback<T>>();
41
+ latestImpl.current = mergeRefs(...refs);
42
+
43
+ return useCallback((node: T | null) => {
44
+ latestImpl.current?.(node);
45
+ }, []); // ← empty deps = identity never changes = no detach/re-attach loop
46
+ }
@@ -1,65 +0,0 @@
1
- import React, { CSSProperties, ReactNode } from "react";
2
- export type MenuOption = {
3
- value: string;
4
- label: ReactNode;
5
- /**
6
- * Visual type - กำหนดว่าจะแสดง icon อะไร
7
- * - "default": ไม่มี icon (แค่ highlight background)
8
- * - "checkbox": แสดง ✓ icon
9
- * - "radio": แสดง ● icon
10
- */
11
- type?: "default" | "checkbox" | "radio";
12
- icon?: ReactNode;
13
- disabled?: boolean;
14
- danger?: boolean;
15
- checked?: boolean;
16
- onClick?: () => void;
17
- };
18
- export type MenuItemType = {
19
- type: "item";
20
- item: MenuOption;
21
- } | {
22
- type: "separator";
23
- } | {
24
- type: "label";
25
- label: string;
26
- } | {
27
- type: "custom";
28
- render: () => ReactNode;
29
- };
30
- export type MenuProps = {
31
- items: MenuItemType[];
32
- /**
33
- * Selected values - ใช้กับ type="item"
34
- */
35
- selectedValues?: string[];
36
- /**
37
- * Callback เมื่อเลือก item
38
- * - ถ้า item.type="checkbox" → toggle checked state
39
- * - ถ้า item.type="radio" → single select (clear others)
40
- * - ถ้า item.type="default" หรือไม่ระบุ → ตาม selectedValues
41
- */
42
- onSelect?: (value: string, item: MenuOption) => void;
43
- className?: string;
44
- style?: CSSProperties;
45
- isAbove?: boolean;
46
- };
47
- export declare const Menu: React.ForwardRefExoticComponent<MenuProps & React.RefAttributes<HTMLDivElement>>;
48
- type MenuItemProps = {
49
- option: MenuOption;
50
- visualType: "default" | "checkbox" | "radio";
51
- isChecked: boolean;
52
- onSelect: () => void;
53
- className?: string;
54
- };
55
- export declare const MenuItem: React.ForwardRefExoticComponent<MenuItemProps & React.RefAttributes<HTMLDivElement>>;
56
- type MenuSeparatorProps = {
57
- className?: string;
58
- };
59
- export declare const MenuSeparator: React.ForwardRefExoticComponent<MenuSeparatorProps & React.RefAttributes<HTMLDivElement>>;
60
- type MenuLabelProps = {
61
- children: ReactNode;
62
- className?: string;
63
- };
64
- export declare const MenuLabel: React.ForwardRefExoticComponent<MenuLabelProps & React.RefAttributes<HTMLDivElement>>;
65
- export default Menu;
@@ -1,19 +0,0 @@
1
- import { MenuItemType } from "./Menu";
2
- /**
3
- * Helper function to convert simple options to MenuItemType
4
- * Useful for integrating with Dropdown component
5
- */
6
- export declare function optionsToMenuItems(options: Array<{
7
- value: string;
8
- label: string | React.ReactNode;
9
- disabled?: boolean;
10
- renderLabel?: any;
11
- }>): MenuItemType[];
12
- /**
13
- * Helper to add separator between menu items
14
- */
15
- export declare function withSeparator(items: MenuItemType[], atIndex: number): MenuItemType[];
16
- /**
17
- * Helper to add label/header to menu items
18
- */
19
- export declare function withLabel(label: string, items: MenuItemType[]): MenuItemType[];
@@ -1,4 +0,0 @@
1
- export { Menu, MenuItem, MenuSeparator, MenuLabel } from "./Menu";
2
- export type { MenuOption, MenuItemType, MenuProps } from "./Menu";
3
- export { optionsToMenuItems, withSeparator, withLabel } from "./helpers";
4
- export { default } from "./Menu";