@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.
- package/dist/cjs/bundle.css +316 -43
- package/dist/cjs/bundle.js +675 -675
- package/dist/cjs/bundle.js.map +1 -1
- package/dist/cjs/types/components/Badge/Badge.d.ts +40 -0
- package/dist/cjs/types/components/Badge/Badge.stories.d.ts +295 -0
- package/dist/cjs/types/components/Badge/Badge.styles.d.ts +7 -0
- package/dist/cjs/types/components/Badge/index.d.ts +2 -0
- package/dist/cjs/types/components/Dropdown/Dropdown.d.ts +4 -8
- package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +1 -6
- package/dist/cjs/types/components/DropdownMenu/DropdownMenu.d.ts +5 -1
- package/dist/cjs/types/components/DropdownMenu/DropdownMenu.stories.d.ts +45 -30
- package/dist/cjs/types/components/Form/Form.d.ts +2 -1
- package/dist/cjs/types/components/Form/Form.stories.d.ts +4 -0
- package/dist/cjs/types/components/ScrollArea/ScrollArea.d.ts +38 -0
- package/dist/cjs/types/components/ScrollArea/ScrollArea.stories.d.ts +301 -0
- package/dist/cjs/types/index.d.ts +4 -1
- package/dist/cjs/types/patterns/menu/Menu.d.ts +70 -0
- package/dist/cjs/types/{components/Menu → patterns/menu}/Menu.stories.d.ts +17 -10
- package/dist/cjs/types/utils/mergeRefs.d.ts +20 -0
- package/dist/components/Avatar/Avatar.styles.js +2 -2
- package/dist/components/Badge/Badge.js +36 -0
- package/dist/components/Badge/Badge.stories.js +51 -0
- package/dist/components/Badge/Badge.styles.js +62 -0
- package/dist/components/Badge/index.js +2 -0
- package/dist/components/Dropdown/Dropdown.js +54 -163
- package/dist/components/Dropdown/Dropdown.stories.js +29 -0
- package/dist/components/DropdownMenu/DropdownMenu.js +24 -13
- package/dist/components/DropdownMenu/DropdownMenu.stories.js +120 -88
- package/dist/components/Form/Form.js +11 -4
- package/dist/components/Form/Form.stories.js +27 -0
- package/dist/components/ScrollArea/ScrollArea.js +50 -0
- package/dist/components/ScrollArea/ScrollArea.stories.js +56 -0
- package/dist/components/TextInput/TextInput.js +6 -3
- package/dist/esm/bundle.css +316 -43
- package/dist/esm/bundle.js +1545 -1545
- package/dist/esm/bundle.js.map +1 -1
- package/dist/esm/types/components/Badge/Badge.d.ts +40 -0
- package/dist/esm/types/components/Badge/Badge.stories.d.ts +295 -0
- package/dist/esm/types/components/Badge/Badge.styles.d.ts +7 -0
- package/dist/esm/types/components/Badge/index.d.ts +2 -0
- package/dist/esm/types/components/Dropdown/Dropdown.d.ts +4 -8
- package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +1 -6
- package/dist/esm/types/components/DropdownMenu/DropdownMenu.d.ts +5 -1
- package/dist/esm/types/components/DropdownMenu/DropdownMenu.stories.d.ts +45 -30
- package/dist/esm/types/components/Form/Form.d.ts +2 -1
- package/dist/esm/types/components/Form/Form.stories.d.ts +4 -0
- package/dist/esm/types/components/ScrollArea/ScrollArea.d.ts +38 -0
- package/dist/esm/types/components/ScrollArea/ScrollArea.stories.d.ts +301 -0
- package/dist/esm/types/index.d.ts +4 -1
- package/dist/esm/types/patterns/menu/Menu.d.ts +70 -0
- package/dist/esm/types/{components/Menu → patterns/menu}/Menu.stories.d.ts +17 -10
- package/dist/esm/types/utils/mergeRefs.d.ts +20 -0
- package/dist/index.d.ts +156 -74
- package/dist/index.js +3 -1
- package/dist/patterns/menu/Menu.js +95 -0
- package/dist/patterns/menu/Menu.stories.js +611 -0
- package/dist/src/theme/global.css +485 -57
- package/dist/utils/mergeRefs.js +42 -0
- package/package.json +1 -1
- package/src/components/Avatar/Avatar.styles.ts +2 -2
- package/src/components/Badge/Badge.stories.tsx +128 -0
- package/src/components/Badge/Badge.styles.ts +70 -0
- package/src/components/Badge/Badge.tsx +103 -0
- package/src/components/Badge/index.ts +3 -0
- package/src/components/Dropdown/Dropdown.stories.tsx +170 -1
- package/src/components/Dropdown/Dropdown.tsx +186 -276
- package/src/components/DropdownMenu/DropdownMenu.stories.tsx +1375 -253
- package/src/components/DropdownMenu/DropdownMenu.tsx +118 -55
- package/src/components/Form/Form.stories.tsx +70 -0
- package/src/components/Form/Form.tsx +23 -0
- package/src/components/ScrollArea/ScrollArea.stories.tsx +229 -0
- package/src/components/ScrollArea/ScrollArea.tsx +72 -0
- package/src/components/TextInput/TextInput.tsx +6 -3
- package/src/index.ts +4 -1
- package/src/patterns/menu/Menu.stories.tsx +1100 -0
- package/src/patterns/menu/Menu.tsx +282 -0
- package/src/theme/global.css +84 -11
- package/src/theme/themes/xspector/baseline.css +1 -1
- package/src/theme/themes/xspector/components/scrollbar.css +12 -0
- package/src/theme/tokens/baseline.css +3 -1
- package/src/theme/tokens/components/badge.css +54 -0
- package/src/theme/tokens/components/dropdown-menu.css +16 -5
- package/src/theme/tokens/components/scrollbar.css +18 -0
- package/src/utils/mergeRefs.ts +46 -0
- package/dist/cjs/types/components/Menu/Menu.d.ts +0 -65
- package/dist/cjs/types/components/Menu/helpers.d.ts +0 -19
- package/dist/cjs/types/components/Menu/index.d.ts +0 -4
- package/dist/components/Menu/Menu.js +0 -64
- package/dist/components/Menu/Menu.stories.js +0 -406
- package/dist/components/Menu/helpers.js +0 -28
- package/dist/components/Menu/index.js +0 -3
- package/dist/esm/types/components/Menu/Menu.d.ts +0 -65
- package/dist/esm/types/components/Menu/helpers.d.ts +0 -19
- package/dist/esm/types/components/Menu/index.d.ts +0 -4
- package/src/components/Menu/Menu.stories.tsx +0 -586
- package/src/components/Menu/Menu.tsx +0 -235
- package/src/components/Menu/helpers.ts +0 -45
- package/src/components/Menu/index.ts +0 -7
- package/src/theme/themes/xspector/components/dropdown-menu.css +0 -28
|
@@ -27,14 +27,13 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
|
|
27
27
|
<DropdownMenuPrimitive.SubTrigger
|
|
28
28
|
ref={ref}
|
|
29
29
|
className={cn(
|
|
30
|
-
|
|
31
|
-
"relative flex gap-3 cursor-pointer select-none box-border items-center py-4 pl-9 pr-4 typography-subtitle4 outline-none transition-colors data-[disabled]:pointer-events-none",
|
|
30
|
+
"relative flex gap-4 cursor-pointer select-none box-border items-center py-4 pl-4 pr-4 typography-subtitle4 outline-none transition-colors data-[disabled]:pointer-events-none",
|
|
32
31
|
"bg-[var(--dropdown-menu-default-bg)] text-[var(--dropdown-menu-default-text)]",
|
|
33
32
|
"active:opacity-75",
|
|
34
33
|
"focus:!bg-[var(--dropdown-menu-hover-bg)] focus:!text-[var(--dropdown-menu-hover-text)]",
|
|
35
34
|
"data-[disabled]:!bg-[var(--dropdown-menu-disabled-bg)] data-[disabled]:!text-[var(--dropdown-menu-disabled-text)]",
|
|
36
35
|
inset && "pl-8",
|
|
37
|
-
className
|
|
36
|
+
className,
|
|
38
37
|
)}
|
|
39
38
|
{...props}
|
|
40
39
|
>
|
|
@@ -52,8 +51,8 @@ const DropdownMenuSubContent = React.forwardRef<
|
|
|
52
51
|
<DropdownMenuPrimitive.SubContent
|
|
53
52
|
ref={ref}
|
|
54
53
|
className={cn(
|
|
55
|
-
"z-50 min-w-[154px] overflow-hidden rounded-
|
|
56
|
-
className
|
|
54
|
+
"z-50 min-w-[154px] overflow-hidden rounded-lg bg-modal-surface text-text-contrast-low data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
55
|
+
className,
|
|
57
56
|
)}
|
|
58
57
|
{...props}
|
|
59
58
|
style={{
|
|
@@ -74,8 +73,8 @@ const DropdownMenuContent = React.forwardRef<
|
|
|
74
73
|
ref={ref}
|
|
75
74
|
sideOffset={sideOffset}
|
|
76
75
|
className={cn(
|
|
77
|
-
"z-50 min-w-[154px] overflow-hidden rounded-
|
|
78
|
-
className
|
|
76
|
+
"z-50 min-w-[154px] overflow-hidden rounded-lg bg-modal-surface text-text-contrast-low data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
77
|
+
className,
|
|
79
78
|
)}
|
|
80
79
|
{...props}
|
|
81
80
|
style={{
|
|
@@ -91,45 +90,88 @@ const DropdownMenuItem = React.forwardRef<
|
|
|
91
90
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
92
91
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
93
92
|
inset?: boolean;
|
|
93
|
+
selected?: boolean;
|
|
94
|
+
icon?: React.ReactNode;
|
|
94
95
|
}
|
|
95
|
-
>(({ className, inset, ...props }, ref) =>
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
96
|
+
>(({ className, inset, selected, icon, children, ...props }, ref) => {
|
|
97
|
+
const hasIcon = !!icon;
|
|
98
|
+
return (
|
|
99
|
+
<DropdownMenuPrimitive.Item
|
|
100
|
+
ref={ref}
|
|
101
|
+
className={cn(
|
|
102
|
+
"relative flex cursor-pointer select-none box-border items-center py-4 pl-4 pr-8 typography-subtitle4 outline-none transition-colors data-[disabled]:pointer-events-none",
|
|
103
|
+
"bg-[var(--dropdown-menu-default-bg)] text-[var(--dropdown-menu-default-text)]",
|
|
104
|
+
"active:opacity-75",
|
|
105
|
+
"focus:!bg-[var(--dropdown-menu-hover-bg)] focus:!text-[var(--dropdown-menu-hover-text)]",
|
|
106
|
+
selected &&
|
|
107
|
+
"bg-[var(--dropdown-menu-selected-bg)] text-[var(--dropdown-menu-selected-text)] typography-subtitle5",
|
|
108
|
+
"data-[disabled]:!bg-[var(--dropdown-menu-disabled-bg)] data-[disabled]:!text-[var(--dropdown-menu-disabled-text)]",
|
|
109
|
+
inset && "pl-8",
|
|
110
|
+
className,
|
|
111
|
+
hasIcon ? "gap-4" : "gap-1",
|
|
112
|
+
)}
|
|
113
|
+
{...props}
|
|
114
|
+
>
|
|
115
|
+
<div className="flex shrink-0 flex-row gap-1">
|
|
116
|
+
<span className="size-4 flex items-center justify-center">
|
|
117
|
+
{selected && (
|
|
118
|
+
<Icon
|
|
119
|
+
type="heroicons"
|
|
120
|
+
name="check"
|
|
121
|
+
className="size-4 text-[var(--dropdown-menu-selected-text)]"
|
|
122
|
+
/>
|
|
123
|
+
)}
|
|
124
|
+
</span>
|
|
125
|
+
{icon}
|
|
126
|
+
</div>
|
|
127
|
+
{children}
|
|
128
|
+
</DropdownMenuPrimitive.Item>
|
|
129
|
+
);
|
|
130
|
+
});
|
|
110
131
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
|
111
132
|
|
|
112
133
|
const DropdownMenuCheckboxItem = React.forwardRef<
|
|
113
134
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
|
114
135
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
|
115
|
-
>(({ className, children, checked, ...props }, ref) => (
|
|
136
|
+
>(({ className, children, checked, disabled, ...props }, ref) => (
|
|
116
137
|
<DropdownMenuPrimitive.CheckboxItem
|
|
117
138
|
ref={ref}
|
|
118
139
|
className={cn(
|
|
119
|
-
"relative flex gap-3 cursor-pointer select-none box-border items-center py-4 pl-
|
|
140
|
+
"relative flex gap-3 cursor-pointer select-none box-border items-center py-4 pl-4 pr-8 typography-subtitle4 outline-none transition-colors data-[disabled]:pointer-events-none",
|
|
120
141
|
"bg-[var(--dropdown-menu-default-bg)] text-[var(--dropdown-menu-default-text)]",
|
|
121
142
|
"active:opacity-75",
|
|
122
143
|
"focus:!bg-[var(--dropdown-menu-hover-bg)] focus:!text-[var(--dropdown-menu-hover-text)]",
|
|
123
|
-
"data-[state='checked']:bg-[var(--dropdown-menu-selected-bg)] data-[state='checked']:text-[var(--dropdown-menu-selected-text)]
|
|
144
|
+
"data-[state='checked']:bg-[var(--dropdown-menu-selected-bg)] data-[state='checked']:text-[var(--dropdown-menu-selected-text)]",
|
|
124
145
|
"data-[disabled]:!bg-[var(--dropdown-menu-disabled-bg)] data-[disabled]:!text-[var(--dropdown-menu-disabled-text)]",
|
|
125
|
-
className
|
|
146
|
+
className,
|
|
126
147
|
)}
|
|
127
148
|
checked={checked}
|
|
149
|
+
disabled={disabled}
|
|
128
150
|
{...props}
|
|
129
151
|
>
|
|
130
|
-
<span
|
|
152
|
+
<span
|
|
153
|
+
className={cn(
|
|
154
|
+
"shrink-0 size-4 rounded-[2px] border flex items-center justify-center transition-all overflow-hidden",
|
|
155
|
+
checked &&
|
|
156
|
+
!disabled &&
|
|
157
|
+
"bg-[var(--dropdown-menu-checkbox-checked-bg)] border-[var(--dropdown-menu-checkbox-checked-bg)]",
|
|
158
|
+
checked &&
|
|
159
|
+
disabled &&
|
|
160
|
+
"bg-[var(--dropdown-menu-checkbox-disabled-checked-bg)] border-transparent",
|
|
161
|
+
!checked &&
|
|
162
|
+
disabled &&
|
|
163
|
+
"border-[var(--dropdown-menu-checkbox-disabled-border)]",
|
|
164
|
+
!checked &&
|
|
165
|
+
!disabled &&
|
|
166
|
+
"border-[var(--dropdown-menu-checkbox-border)]",
|
|
167
|
+
)}
|
|
168
|
+
>
|
|
131
169
|
<DropdownMenuPrimitive.ItemIndicator>
|
|
132
|
-
<Icon
|
|
170
|
+
<Icon
|
|
171
|
+
type="heroicons"
|
|
172
|
+
name="check"
|
|
173
|
+
className="size-3 text-[var(--dropdown-menu-checkbox-checked-icon)]"
|
|
174
|
+
/>
|
|
133
175
|
</DropdownMenuPrimitive.ItemIndicator>
|
|
134
176
|
</span>
|
|
135
177
|
{children}
|
|
@@ -140,29 +182,50 @@ DropdownMenuCheckboxItem.displayName =
|
|
|
140
182
|
|
|
141
183
|
const DropdownMenuRadioItem = React.forwardRef<
|
|
142
184
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
|
143
|
-
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
185
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> & {
|
|
186
|
+
icon?: React.ReactNode;
|
|
187
|
+
}
|
|
188
|
+
>(({ className, children, disabled, icon, ...props }, ref) => {
|
|
189
|
+
const hasIconSlot = !!icon;
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<DropdownMenuPrimitive.RadioItem
|
|
193
|
+
ref={ref}
|
|
194
|
+
className={cn(
|
|
195
|
+
"relative flex cursor-pointer select-none box-border items-center py-4 pl-4 pr-8 typography-subtitle4 outline-none transition-colors data-[disabled]:pointer-events-none",
|
|
196
|
+
"bg-[var(--dropdown-menu-default-bg)] text-[var(--dropdown-menu-default-text)]",
|
|
197
|
+
"active:opacity-75",
|
|
198
|
+
"focus:!bg-[var(--dropdown-menu-hover-bg)] focus:!text-[var(--dropdown-menu-hover-text)]",
|
|
199
|
+
"data-[state='checked']:bg-[var(--dropdown-menu-selected-bg)] data-[state='checked']:text-[var(--dropdown-menu-selected-text)]",
|
|
200
|
+
"data-[disabled]:!bg-[var(--dropdown-menu-disabled-bg)] data-[disabled]:!text-[var(--dropdown-menu-radio-disabled-text)]",
|
|
201
|
+
"data-[state='checked']:data-[disabled]:!text-[var(--dropdown-menu-radio-selected-disabled-text)]",
|
|
202
|
+
className,
|
|
203
|
+
hasIconSlot ? "gap-4" : "gap-1",
|
|
204
|
+
)}
|
|
205
|
+
{...props}
|
|
206
|
+
disabled={disabled}
|
|
207
|
+
>
|
|
208
|
+
<div className="flex shrink-0 flex-row gap-1">
|
|
209
|
+
<span className="size-4">
|
|
210
|
+
<DropdownMenuPrimitive.ItemIndicator className="shrink-0">
|
|
211
|
+
<Icon
|
|
212
|
+
type="heroicons"
|
|
213
|
+
name="check"
|
|
214
|
+
className={cn(
|
|
215
|
+
"size-4",
|
|
216
|
+
disabled
|
|
217
|
+
? "text-[var(--dropdown-menu-radio-selected-disabled-text)]"
|
|
218
|
+
: "text-[var(--dropdown-menu-selected-text)]",
|
|
219
|
+
)}
|
|
220
|
+
/>
|
|
221
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
222
|
+
</span>
|
|
223
|
+
{icon}
|
|
224
|
+
</div>
|
|
225
|
+
{children}
|
|
226
|
+
</DropdownMenuPrimitive.RadioItem>
|
|
227
|
+
);
|
|
228
|
+
});
|
|
166
229
|
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
|
167
230
|
|
|
168
231
|
const DropdownMenuLabel = React.forwardRef<
|
|
@@ -174,9 +237,9 @@ const DropdownMenuLabel = React.forwardRef<
|
|
|
174
237
|
<DropdownMenuPrimitive.Label
|
|
175
238
|
ref={ref}
|
|
176
239
|
className={cn(
|
|
177
|
-
"px-3 py-2 typography-small4 text-text-g-contrast-
|
|
240
|
+
"px-3 py-2 typography-small4 text-text-g-contrast-high",
|
|
178
241
|
inset && "pl-8",
|
|
179
|
-
className
|
|
242
|
+
className,
|
|
180
243
|
)}
|
|
181
244
|
{...props}
|
|
182
245
|
/>
|
|
@@ -190,8 +253,8 @@ const DropdownMenuSeparator = React.forwardRef<
|
|
|
190
253
|
<DropdownMenuPrimitive.Separator
|
|
191
254
|
ref={ref}
|
|
192
255
|
className={cn(
|
|
193
|
-
"
|
|
194
|
-
className
|
|
256
|
+
"h-px bg-[var(--dropdown-menu-seperator-bg)]",
|
|
257
|
+
className,
|
|
195
258
|
)}
|
|
196
259
|
{...props}
|
|
197
260
|
/>
|
|
@@ -362,6 +362,76 @@ export const RenderPropsCodeControl = {
|
|
|
362
362
|
},
|
|
363
363
|
} satisfies Story;
|
|
364
364
|
|
|
365
|
+
export const FormStateChangeCallback = {
|
|
366
|
+
args: {},
|
|
367
|
+
render: () => {
|
|
368
|
+
const [isFormValid, setIsFormValid] = useState(false);
|
|
369
|
+
const [isDirty, setIsDirty] = useState(false);
|
|
370
|
+
const [submitCount, setSubmitCount] = useState(0);
|
|
371
|
+
|
|
372
|
+
const schema = yup.object({
|
|
373
|
+
title: yup.string().required("Title is required"),
|
|
374
|
+
description: yup
|
|
375
|
+
.string()
|
|
376
|
+
.required("Description is required")
|
|
377
|
+
.min(10, "Description must be at least 10 characters"),
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
return (
|
|
381
|
+
<div className="flex flex-col gap-4">
|
|
382
|
+
{/* Simulates a parent component (e.g. modal footer) that controls the submit button */}
|
|
383
|
+
<div className="rounded-md border border-bg-stroke1 bg-bg-bg2 p-3 text-xs text-text-g-contrast-medium space-y-1">
|
|
384
|
+
<div>
|
|
385
|
+
Validity:{" "}
|
|
386
|
+
<span className={isFormValid ? "text-state-success-default" : "text-state-error-default"}>
|
|
387
|
+
{isFormValid ? "valid" : "invalid"}
|
|
388
|
+
</span>
|
|
389
|
+
</div>
|
|
390
|
+
<div>Dirty: {isDirty ? "yes" : "no"}</div>
|
|
391
|
+
<div>Submit count: {submitCount}</div>
|
|
392
|
+
</div>
|
|
393
|
+
|
|
394
|
+
<Form<CodeControlFormValues>
|
|
395
|
+
className="flex flex-col gap-3"
|
|
396
|
+
defaultValues={{ title: "", description: "" }}
|
|
397
|
+
mode="onTouched"
|
|
398
|
+
reValidateMode="onChange"
|
|
399
|
+
validationSchema={schema}
|
|
400
|
+
onSubmit={(values) => {
|
|
401
|
+
setSubmitCount((prev) => prev + 1);
|
|
402
|
+
// eslint-disable-next-line no-console
|
|
403
|
+
console.log("Submitted:", values);
|
|
404
|
+
}}
|
|
405
|
+
onFormStateChange={(formState) => {
|
|
406
|
+
setIsFormValid(formState.isValid);
|
|
407
|
+
setIsDirty(formState.isDirty);
|
|
408
|
+
}}
|
|
409
|
+
>
|
|
410
|
+
<Field<CodeControlFormValues, "title">
|
|
411
|
+
name="title"
|
|
412
|
+
component={TextInput}
|
|
413
|
+
componentProps={{ label: "Title", required: true }}
|
|
414
|
+
/>
|
|
415
|
+
<Field<CodeControlFormValues, "description">
|
|
416
|
+
name="description"
|
|
417
|
+
component={TextInput}
|
|
418
|
+
componentProps={{
|
|
419
|
+
label: "Description",
|
|
420
|
+
helperText: "At least 10 characters",
|
|
421
|
+
required: true,
|
|
422
|
+
}}
|
|
423
|
+
/>
|
|
424
|
+
|
|
425
|
+
{/* Submit button lives inside Form — receives isFormValid from parent state */}
|
|
426
|
+
<Button type="submit" disabled={!isFormValid}>
|
|
427
|
+
Submit
|
|
428
|
+
</Button>
|
|
429
|
+
</Form>
|
|
430
|
+
</div>
|
|
431
|
+
);
|
|
432
|
+
},
|
|
433
|
+
} satisfies Story;
|
|
434
|
+
|
|
365
435
|
export const HigherLayerCodeControl = {
|
|
366
436
|
args: {},
|
|
367
437
|
render: () => {
|
|
@@ -3,11 +3,13 @@ import {
|
|
|
3
3
|
DefaultValues,
|
|
4
4
|
FieldValues,
|
|
5
5
|
FormProvider,
|
|
6
|
+
FormState,
|
|
6
7
|
Mode,
|
|
7
8
|
Resolver,
|
|
8
9
|
SubmitErrorHandler,
|
|
9
10
|
SubmitHandler,
|
|
10
11
|
useForm,
|
|
12
|
+
useFormContext,
|
|
11
13
|
UseFormReturn,
|
|
12
14
|
} from "react-hook-form";
|
|
13
15
|
import { yupResolver } from "@hookform/resolvers/yup";
|
|
@@ -35,6 +37,7 @@ export type FormProps<TFieldValues extends FieldValues> = Omit<
|
|
|
35
37
|
controllerRef?: React.MutableRefObject<FormController<TFieldValues> | null>;
|
|
36
38
|
onSubmit: SubmitHandler<TFieldValues>;
|
|
37
39
|
onInvalidSubmit?: SubmitErrorHandler<TFieldValues>;
|
|
40
|
+
onFormStateChange?: (formState: FormState<TFieldValues>) => void;
|
|
38
41
|
resolver?: Resolver<TFieldValues>;
|
|
39
42
|
validationSchema?: yup.ObjectSchema<any>;
|
|
40
43
|
mode?: Mode;
|
|
@@ -132,6 +135,20 @@ export const useControlledForm = <TFieldValues extends FieldValues>({
|
|
|
132
135
|
};
|
|
133
136
|
};
|
|
134
137
|
|
|
138
|
+
const FormStateObserver = <TFieldValues extends FieldValues>({
|
|
139
|
+
onFormStateChange,
|
|
140
|
+
}: {
|
|
141
|
+
onFormStateChange: (formState: FormState<TFieldValues>) => void;
|
|
142
|
+
}) => {
|
|
143
|
+
const { formState } = useFormContext<TFieldValues>();
|
|
144
|
+
|
|
145
|
+
React.useEffect(() => {
|
|
146
|
+
onFormStateChange(formState);
|
|
147
|
+
}, [formState, onFormStateChange]);
|
|
148
|
+
|
|
149
|
+
return null;
|
|
150
|
+
};
|
|
151
|
+
|
|
135
152
|
const FormInner = <TFieldValues extends FieldValues>(
|
|
136
153
|
{
|
|
137
154
|
children,
|
|
@@ -140,6 +157,7 @@ const FormInner = <TFieldValues extends FieldValues>(
|
|
|
140
157
|
controllerRef,
|
|
141
158
|
onSubmit,
|
|
142
159
|
onInvalidSubmit,
|
|
160
|
+
onFormStateChange,
|
|
143
161
|
resolver,
|
|
144
162
|
validationSchema,
|
|
145
163
|
mode = "onSubmit",
|
|
@@ -195,6 +213,11 @@ const FormInner = <TFieldValues extends FieldValues>(
|
|
|
195
213
|
noValidate={noValidate}
|
|
196
214
|
onSubmit={methods.handleSubmit(onSubmit, onInvalidSubmit)}
|
|
197
215
|
>
|
|
216
|
+
{onFormStateChange && (
|
|
217
|
+
<FormStateObserver<TFieldValues>
|
|
218
|
+
onFormStateChange={onFormStateChange}
|
|
219
|
+
/>
|
|
220
|
+
)}
|
|
198
221
|
{typeof children === "function" ? children(methods) : children}
|
|
199
222
|
</form>
|
|
200
223
|
</FormProvider>
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { ScrollArea } from "./ScrollArea";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Components/ScrollArea",
|
|
7
|
+
component: ScrollArea,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: "fullscreen",
|
|
10
|
+
},
|
|
11
|
+
decorators: [
|
|
12
|
+
(Story) => (
|
|
13
|
+
<div className="p-10 flex gap-8 flex-wrap bg-workspace-surface min-h-screen">
|
|
14
|
+
<Story />
|
|
15
|
+
</div>
|
|
16
|
+
),
|
|
17
|
+
],
|
|
18
|
+
} satisfies Meta<typeof ScrollArea>;
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
|
|
22
|
+
const ITEMS = Array.from({ length: 12 }, (_, i) => `Option Description ${i + 1}`);
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Size variants (Figma: Size=M / S / XS)
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
export const Sizes = {
|
|
28
|
+
name: "Size Variants",
|
|
29
|
+
render: () => (
|
|
30
|
+
<div className="flex gap-10 items-start">
|
|
31
|
+
{(["m", "s", "xs"] as const).map((size) => (
|
|
32
|
+
<div key={size}>
|
|
33
|
+
<p className="typography-small4 text-text-g-contrast-medium mb-2 uppercase">
|
|
34
|
+
Size {size}
|
|
35
|
+
</p>
|
|
36
|
+
<ScrollArea
|
|
37
|
+
scrollbarSize={size}
|
|
38
|
+
className="max-h-[270px] w-[200px] rounded-lg bg-modal-surface"
|
|
39
|
+
>
|
|
40
|
+
{ITEMS.map((label) => (
|
|
41
|
+
<div
|
|
42
|
+
key={label}
|
|
43
|
+
className="px-4 py-3 typography-subtitle4 text-text-g-contrast-high"
|
|
44
|
+
>
|
|
45
|
+
{label}
|
|
46
|
+
</div>
|
|
47
|
+
))}
|
|
48
|
+
</ScrollArea>
|
|
49
|
+
</div>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
),
|
|
53
|
+
} satisfies StoryObj;
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Vertical scroll (Figma: Can Scroll)
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
export const VerticalScroll = {
|
|
59
|
+
name: "Vertical Scroll",
|
|
60
|
+
render: () => (
|
|
61
|
+
<div>
|
|
62
|
+
<p className="typography-small4 text-text-g-contrast-medium mb-2">
|
|
63
|
+
Single scrollable list — use{" "}
|
|
64
|
+
<code className="text-xs"><ScrollArea className="max-h-[...]"></code>
|
|
65
|
+
</p>
|
|
66
|
+
<ScrollArea className="max-h-[270px] w-[230px] rounded-lg bg-modal-surface">
|
|
67
|
+
{ITEMS.map((label) => (
|
|
68
|
+
<div
|
|
69
|
+
key={label}
|
|
70
|
+
className="px-4 py-3 typography-subtitle4 text-text-g-contrast-high"
|
|
71
|
+
>
|
|
72
|
+
{label}
|
|
73
|
+
</div>
|
|
74
|
+
))}
|
|
75
|
+
</ScrollArea>
|
|
76
|
+
</div>
|
|
77
|
+
),
|
|
78
|
+
} satisfies StoryObj;
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Horizontal scroll (Figma: Horizontal=Yes)
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
export const HorizontalScroll = {
|
|
84
|
+
name: "Horizontal Scroll",
|
|
85
|
+
render: () => (
|
|
86
|
+
<div>
|
|
87
|
+
<p className="typography-small4 text-text-g-contrast-medium mb-2">
|
|
88
|
+
Horizontal — pass{" "}
|
|
89
|
+
<code className="text-xs">direction="horizontal"</code>
|
|
90
|
+
</p>
|
|
91
|
+
<ScrollArea
|
|
92
|
+
direction="horizontal"
|
|
93
|
+
className="max-w-[300px] rounded-lg bg-modal-surface"
|
|
94
|
+
>
|
|
95
|
+
<div className="flex">
|
|
96
|
+
{ITEMS.map((label) => (
|
|
97
|
+
<div
|
|
98
|
+
key={label}
|
|
99
|
+
className="shrink-0 w-[140px] px-4 py-3 typography-subtitle4 text-text-g-contrast-high border-r border-[var(--dropdown-menu-seperator-bg)]"
|
|
100
|
+
>
|
|
101
|
+
{label}
|
|
102
|
+
</div>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
</ScrollArea>
|
|
106
|
+
</div>
|
|
107
|
+
),
|
|
108
|
+
} satisfies StoryObj;
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Double scroll (Figma: Double Scroll — two independent sections)
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
export const DoubleScroll = {
|
|
114
|
+
name: "Double Scroll",
|
|
115
|
+
render: () => (
|
|
116
|
+
<div>
|
|
117
|
+
<p className="typography-small4 text-text-g-contrast-medium mb-4">
|
|
118
|
+
Two independent scrollable sections inside one container.
|
|
119
|
+
<br />
|
|
120
|
+
Wrap each section in its own{" "}
|
|
121
|
+
<code className="text-xs"><ScrollArea></code>.
|
|
122
|
+
</p>
|
|
123
|
+
<div className="w-[230px] rounded-lg bg-modal-surface overflow-hidden">
|
|
124
|
+
{/* Section A */}
|
|
125
|
+
<div className="px-3 pt-4 pb-2 typography-small4 text-text-g-contrast-high">
|
|
126
|
+
Section A
|
|
127
|
+
</div>
|
|
128
|
+
<ScrollArea className="max-h-[160px]">
|
|
129
|
+
{ITEMS.slice(0, 8).map((label) => (
|
|
130
|
+
<div
|
|
131
|
+
key={label}
|
|
132
|
+
className="px-4 py-3 typography-subtitle4 text-text-g-contrast-high"
|
|
133
|
+
>
|
|
134
|
+
{label}
|
|
135
|
+
</div>
|
|
136
|
+
))}
|
|
137
|
+
</ScrollArea>
|
|
138
|
+
|
|
139
|
+
{/* Separator */}
|
|
140
|
+
<div className="h-px bg-[var(--dropdown-menu-seperator-bg)] my-2" />
|
|
141
|
+
|
|
142
|
+
{/* Section B */}
|
|
143
|
+
<div className="px-3 pt-2 pb-2 typography-small4 text-text-g-contrast-high">
|
|
144
|
+
Section B
|
|
145
|
+
</div>
|
|
146
|
+
<ScrollArea className="max-h-[160px]">
|
|
147
|
+
{ITEMS.slice(0, 8).map((label) => (
|
|
148
|
+
<div
|
|
149
|
+
key={label}
|
|
150
|
+
className="px-4 py-3 typography-subtitle4 text-text-g-contrast-high"
|
|
151
|
+
>
|
|
152
|
+
{label}
|
|
153
|
+
</div>
|
|
154
|
+
))}
|
|
155
|
+
</ScrollArea>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
),
|
|
159
|
+
} satisfies StoryObj;
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Usage with DropdownMenu (Figma: Can Scroll / Double Scroll in dropdown)
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
export const WithDropdownMenu = {
|
|
165
|
+
name: "Usage with DropdownMenu",
|
|
166
|
+
render: () => {
|
|
167
|
+
// Inline-import to avoid circular storybook deps — client should import from "@core/ui"
|
|
168
|
+
const {
|
|
169
|
+
DropdownMenu,
|
|
170
|
+
DropdownMenuContent,
|
|
171
|
+
DropdownMenuItem,
|
|
172
|
+
DropdownMenuLabel,
|
|
173
|
+
DropdownMenuSeparator,
|
|
174
|
+
DropdownMenuTrigger,
|
|
175
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
176
|
+
} = require("../DropdownMenu/DropdownMenu");
|
|
177
|
+
const Button = require("../Button/Button").default;
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<div className="flex gap-8 items-start flex-wrap">
|
|
181
|
+
{/* Single scrollable dropdown */}
|
|
182
|
+
<div>
|
|
183
|
+
<p className="typography-small4 text-text-g-contrast-medium mb-2">
|
|
184
|
+
Can Scroll
|
|
185
|
+
</p>
|
|
186
|
+
<DropdownMenu>
|
|
187
|
+
<DropdownMenuTrigger asChild>
|
|
188
|
+
<Button variant="outline">Open Menu</Button>
|
|
189
|
+
</DropdownMenuTrigger>
|
|
190
|
+
<DropdownMenuContent>
|
|
191
|
+
<ScrollArea className="max-h-[270px]">
|
|
192
|
+
{ITEMS.map((label) => (
|
|
193
|
+
<DropdownMenuItem key={label}>{label}</DropdownMenuItem>
|
|
194
|
+
))}
|
|
195
|
+
</ScrollArea>
|
|
196
|
+
</DropdownMenuContent>
|
|
197
|
+
</DropdownMenu>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
{/* Double scroll dropdown */}
|
|
201
|
+
<div>
|
|
202
|
+
<p className="typography-small4 text-text-g-contrast-medium mb-2">
|
|
203
|
+
Double Scroll
|
|
204
|
+
</p>
|
|
205
|
+
<DropdownMenu>
|
|
206
|
+
<DropdownMenuTrigger asChild>
|
|
207
|
+
<Button variant="outline">Open Menu</Button>
|
|
208
|
+
</DropdownMenuTrigger>
|
|
209
|
+
<DropdownMenuContent>
|
|
210
|
+
<DropdownMenuLabel>Section A</DropdownMenuLabel>
|
|
211
|
+
<ScrollArea className="max-h-[160px]">
|
|
212
|
+
{ITEMS.slice(0, 8).map((label) => (
|
|
213
|
+
<DropdownMenuItem key={label}>{label}</DropdownMenuItem>
|
|
214
|
+
))}
|
|
215
|
+
</ScrollArea>
|
|
216
|
+
<DropdownMenuSeparator />
|
|
217
|
+
<DropdownMenuLabel>Section B</DropdownMenuLabel>
|
|
218
|
+
<ScrollArea className="max-h-[160px]">
|
|
219
|
+
{ITEMS.slice(0, 8).map((label) => (
|
|
220
|
+
<DropdownMenuItem key={label}>{label}</DropdownMenuItem>
|
|
221
|
+
))}
|
|
222
|
+
</ScrollArea>
|
|
223
|
+
</DropdownMenuContent>
|
|
224
|
+
</DropdownMenu>
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
},
|
|
229
|
+
} satisfies StoryObj;
|