@alpic-ai/ui 0.0.0-dev.c4d7c77 → 0.0.0-dev.c4dcd8d
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/components/form.d.mts +36 -1
- package/dist/components/form.mjs +111 -1
- package/dist/components/table.mjs +3 -3
- package/dist/components/tabs.mjs +4 -4
- package/dist/components/wizard.d.mts +34 -0
- package/dist/components/wizard.mjs +46 -0
- package/package.json +10 -10
- package/src/components/form.tsx +161 -0
- package/src/components/table.tsx +2 -3
- package/src/components/tabs.tsx +4 -4
- package/src/components/wizard.tsx +69 -0
- package/src/stories/form.stories.tsx +64 -2
- package/src/stories/tabs.stories.tsx +4 -2
- package/src/stories/wizard.stories.tsx +64 -0
|
@@ -115,5 +115,40 @@ declare function SelectField<TFieldValues extends FieldValues, TName extends Fie
|
|
|
115
115
|
options,
|
|
116
116
|
placeholder
|
|
117
117
|
}: SelectFieldProps<TFieldValues, TName>): _$react_jsx_runtime0.JSX.Element;
|
|
118
|
+
interface RadioFieldProps<TFieldValues extends FieldValues, TName extends FieldPath<TFieldValues>> extends FormFieldBaseProps<TFieldValues, TName> {
|
|
119
|
+
options: SelectFieldOption[];
|
|
120
|
+
}
|
|
121
|
+
declare function RadioField<TFieldValues extends FieldValues, TName extends FieldPath<TFieldValues>>({
|
|
122
|
+
control,
|
|
123
|
+
name,
|
|
124
|
+
rules,
|
|
125
|
+
required,
|
|
126
|
+
label,
|
|
127
|
+
description,
|
|
128
|
+
tooltip,
|
|
129
|
+
options
|
|
130
|
+
}: RadioFieldProps<TFieldValues, TName>): _$react_jsx_runtime0.JSX.Element;
|
|
131
|
+
interface ChecklistFieldProps<TFieldValues extends FieldValues, TName extends FieldPath<TFieldValues>> extends FormFieldBaseProps<TFieldValues, TName> {
|
|
132
|
+
options: SelectFieldOption[];
|
|
133
|
+
}
|
|
134
|
+
declare function ChecklistField<TFieldValues extends FieldValues, TName extends FieldPath<TFieldValues>>({
|
|
135
|
+
control,
|
|
136
|
+
name,
|
|
137
|
+
rules,
|
|
138
|
+
required,
|
|
139
|
+
label,
|
|
140
|
+
description,
|
|
141
|
+
tooltip,
|
|
142
|
+
options
|
|
143
|
+
}: ChecklistFieldProps<TFieldValues, TName>): _$react_jsx_runtime0.JSX.Element;
|
|
144
|
+
interface CheckboxFieldProps<TFieldValues extends FieldValues, TName extends FieldPath<TFieldValues>> extends Omit<FormFieldBaseProps<TFieldValues, TName>, "required"> {}
|
|
145
|
+
declare function CheckboxField<TFieldValues extends FieldValues, TName extends FieldPath<TFieldValues>>({
|
|
146
|
+
control,
|
|
147
|
+
name,
|
|
148
|
+
rules,
|
|
149
|
+
label,
|
|
150
|
+
description,
|
|
151
|
+
tooltip
|
|
152
|
+
}: CheckboxFieldProps<TFieldValues, TName>): _$react_jsx_runtime0.JSX.Element;
|
|
118
153
|
//#endregion
|
|
119
|
-
export { Form, FormControl, FormDescription, FormField, FormFields, FormHeader, FormItem, FormLabel, FormMessage, InputField, SelectField, type SelectFieldOption, TextareaField, useFormField };
|
|
154
|
+
export { CheckboxField, ChecklistField, Form, FormControl, FormDescription, FormField, FormFields, FormHeader, FormItem, FormLabel, FormMessage, InputField, RadioField, SelectField, type SelectFieldOption, TextareaField, useFormField };
|
package/dist/components/form.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { cn } from "../lib/cn.mjs";
|
|
3
3
|
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip.mjs";
|
|
4
|
+
import { Checkbox } from "./checkbox.mjs";
|
|
4
5
|
import { Label } from "./label.mjs";
|
|
5
6
|
import { Input } from "./input.mjs";
|
|
7
|
+
import { RadioGroup, RadioGroupItem } from "./radio-group.mjs";
|
|
6
8
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select.mjs";
|
|
7
9
|
import { Textarea } from "./textarea.mjs";
|
|
8
10
|
import { Info } from "lucide-react";
|
|
@@ -188,5 +190,113 @@ function SelectField({ control, name, rules, required, label, description, toolt
|
|
|
188
190
|
] })
|
|
189
191
|
});
|
|
190
192
|
}
|
|
193
|
+
function RadioField({ control, name, rules, required, label, description, tooltip, options }) {
|
|
194
|
+
return /* @__PURE__ */ jsx(FormField, {
|
|
195
|
+
control,
|
|
196
|
+
name,
|
|
197
|
+
rules,
|
|
198
|
+
render: ({ field }) => /* @__PURE__ */ jsxs(FormItem, { children: [
|
|
199
|
+
label && /* @__PURE__ */ jsx(FormLabel, {
|
|
200
|
+
required,
|
|
201
|
+
tooltip,
|
|
202
|
+
children: label
|
|
203
|
+
}),
|
|
204
|
+
/* @__PURE__ */ jsx(FormControl, { children: /* @__PURE__ */ jsx(RadioGroup, {
|
|
205
|
+
value: field.value ?? "",
|
|
206
|
+
onValueChange: field.onChange,
|
|
207
|
+
onBlur: field.onBlur,
|
|
208
|
+
children: options.map((option) => {
|
|
209
|
+
const itemId = `${field.name}-${option.value}`;
|
|
210
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
211
|
+
className: "flex items-center gap-2",
|
|
212
|
+
children: [/* @__PURE__ */ jsx(RadioGroupItem, {
|
|
213
|
+
id: itemId,
|
|
214
|
+
value: option.value,
|
|
215
|
+
disabled: option.disabled
|
|
216
|
+
}), /* @__PURE__ */ jsx(Label, {
|
|
217
|
+
htmlFor: itemId,
|
|
218
|
+
className: "type-text-sm font-normal",
|
|
219
|
+
children: option.label
|
|
220
|
+
})]
|
|
221
|
+
}, option.value);
|
|
222
|
+
})
|
|
223
|
+
}) }),
|
|
224
|
+
description && /* @__PURE__ */ jsx(FormDescription, { children: description }),
|
|
225
|
+
/* @__PURE__ */ jsx(FormMessage, {})
|
|
226
|
+
] })
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
function ChecklistField({ control, name, rules, required, label, description, tooltip, options }) {
|
|
230
|
+
return /* @__PURE__ */ jsx(FormField, {
|
|
231
|
+
control,
|
|
232
|
+
name,
|
|
233
|
+
rules,
|
|
234
|
+
render: ({ field }) => {
|
|
235
|
+
const selected = Array.isArray(field.value) ? field.value : [];
|
|
236
|
+
const toggle = (value, checked) => {
|
|
237
|
+
if (checked) field.onChange([...selected, value]);
|
|
238
|
+
else field.onChange(selected.filter((existing) => existing !== value));
|
|
239
|
+
};
|
|
240
|
+
return /* @__PURE__ */ jsxs(FormItem, { children: [
|
|
241
|
+
label && /* @__PURE__ */ jsx(FormLabel, {
|
|
242
|
+
required,
|
|
243
|
+
tooltip,
|
|
244
|
+
children: label
|
|
245
|
+
}),
|
|
246
|
+
/* @__PURE__ */ jsx(FormControl, { children: /* @__PURE__ */ jsx("fieldset", {
|
|
247
|
+
className: "m-0 flex flex-col gap-2 border-0 p-0",
|
|
248
|
+
onBlur: field.onBlur,
|
|
249
|
+
children: options.map((option) => {
|
|
250
|
+
const itemId = `${field.name}-${option.value}`;
|
|
251
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
252
|
+
className: "flex items-start gap-2",
|
|
253
|
+
children: [/* @__PURE__ */ jsx(Checkbox, {
|
|
254
|
+
id: itemId,
|
|
255
|
+
checked: selected.includes(option.value),
|
|
256
|
+
onCheckedChange: (next) => toggle(option.value, Boolean(next)),
|
|
257
|
+
disabled: option.disabled
|
|
258
|
+
}), /* @__PURE__ */ jsx(Label, {
|
|
259
|
+
htmlFor: itemId,
|
|
260
|
+
className: "type-text-sm font-normal leading-tight",
|
|
261
|
+
children: option.label
|
|
262
|
+
})]
|
|
263
|
+
}, option.value);
|
|
264
|
+
})
|
|
265
|
+
}) }),
|
|
266
|
+
description && /* @__PURE__ */ jsx(FormDescription, { children: description }),
|
|
267
|
+
/* @__PURE__ */ jsx(FormMessage, {})
|
|
268
|
+
] });
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
function CheckboxField({ control, name, rules, label, description, tooltip }) {
|
|
273
|
+
return /* @__PURE__ */ jsx(FormField, {
|
|
274
|
+
control,
|
|
275
|
+
name,
|
|
276
|
+
rules,
|
|
277
|
+
render: ({ field }) => /* @__PURE__ */ jsxs(FormItem, {
|
|
278
|
+
className: "gap-1.5",
|
|
279
|
+
children: [
|
|
280
|
+
/* @__PURE__ */ jsxs("div", {
|
|
281
|
+
className: "flex items-start gap-2",
|
|
282
|
+
children: [/* @__PURE__ */ jsx(FormControl, { children: /* @__PURE__ */ jsx(Checkbox, {
|
|
283
|
+
checked: field.value ?? false,
|
|
284
|
+
onCheckedChange: (next) => field.onChange(Boolean(next)),
|
|
285
|
+
onBlur: field.onBlur
|
|
286
|
+
}) }), label && /* @__PURE__ */ jsx(FormLabel, {
|
|
287
|
+
tooltip,
|
|
288
|
+
className: "font-normal",
|
|
289
|
+
children: label
|
|
290
|
+
})]
|
|
291
|
+
}),
|
|
292
|
+
description && /* @__PURE__ */ jsx(FormDescription, {
|
|
293
|
+
className: "ml-6",
|
|
294
|
+
children: description
|
|
295
|
+
}),
|
|
296
|
+
/* @__PURE__ */ jsx(FormMessage, { className: "ml-6" })
|
|
297
|
+
]
|
|
298
|
+
})
|
|
299
|
+
});
|
|
300
|
+
}
|
|
191
301
|
//#endregion
|
|
192
|
-
export { Form, FormControl, FormDescription, FormField, FormFields, FormHeader, FormItem, FormLabel, FormMessage, InputField, SelectField, TextareaField, useFormField };
|
|
302
|
+
export { CheckboxField, ChecklistField, Form, FormControl, FormDescription, FormField, FormFields, FormHeader, FormItem, FormLabel, FormMessage, InputField, RadioField, SelectField, TextareaField, useFormField };
|
|
@@ -36,21 +36,21 @@ function TableFooter({ className, ...props }) {
|
|
|
36
36
|
function TableRow({ className, ...props }) {
|
|
37
37
|
return /* @__PURE__ */ jsx("tr", {
|
|
38
38
|
"data-slot": "table-row",
|
|
39
|
-
className: cn("border-b border-border-secondary transition-colors", "data-[state=selected]:bg-muted", "[@media(hover:hover)]:hover:bg-background-hover dark:[@media(hover:hover)]:hover:bg-muted",
|
|
39
|
+
className: cn("border-b border-border-secondary transition-colors", "data-[state=selected]:bg-muted", "[@media(hover:hover)]:hover:bg-background-hover dark:[@media(hover:hover)]:hover:bg-muted", className),
|
|
40
40
|
...props
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
43
|
function TableHead({ className, ...props }) {
|
|
44
44
|
return /* @__PURE__ */ jsx("th", {
|
|
45
45
|
"data-slot": "table-head",
|
|
46
|
-
className: cn("h-11 px-6 py-3 bg-muted text-left align-middle type-text-xs font-semibold text-placeholder dark:text-subtle-foreground whitespace-nowrap", "[&:has([role=checkbox])]:w-px [&:has([role=checkbox])]:
|
|
46
|
+
className: cn("h-11 px-6 py-3 bg-muted text-left align-middle type-text-xs font-semibold text-placeholder dark:text-subtle-foreground whitespace-nowrap", "[&:has([role=checkbox])]:w-px [&:has([role=checkbox])]:px-0", className),
|
|
47
47
|
...props
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
50
|
function TableCell({ className, interactive = false, ...props }) {
|
|
51
51
|
return /* @__PURE__ */ jsx("td", {
|
|
52
52
|
"data-slot": "table-cell",
|
|
53
|
-
className: cn("align-middle", interactive ? "h-px p-0" : "px-6 py-2", "[&:has([role=checkbox])]:w-px [&:has([role=checkbox])]:
|
|
53
|
+
className: cn("align-middle", interactive ? "h-px p-0" : "px-6 py-2", "[&:has([role=checkbox])]:w-px [&:has([role=checkbox])]:px-0", className),
|
|
54
54
|
...props
|
|
55
55
|
});
|
|
56
56
|
}
|
package/dist/components/tabs.mjs
CHANGED
|
@@ -21,10 +21,10 @@ const tabsTriggerVariants = cva([
|
|
|
21
21
|
"data-[state=active]:border-b-2 data-[state=active]:border-foreground data-[state=active]:text-foreground"
|
|
22
22
|
],
|
|
23
23
|
pill: [
|
|
24
|
-
"rounded-
|
|
25
|
-
"text-
|
|
26
|
-
"[@media(hover:hover)]:hover:bg-accent [@media(hover:hover)]:hover:text-
|
|
27
|
-
"data-[state=active]:bg-accent data-[state=active]:text-
|
|
24
|
+
"rounded-md px-2 py-1.5",
|
|
25
|
+
"text-quaternary-foreground",
|
|
26
|
+
"[@media(hover:hover)]:hover:bg-accent [@media(hover:hover)]:hover:text-muted-foreground",
|
|
27
|
+
"data-[state=active]:bg-accent data-[state=active]:text-muted-foreground"
|
|
28
28
|
]
|
|
29
29
|
} },
|
|
30
30
|
defaultVariants: { variant: "default" }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as _$react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/components/wizard.d.ts
|
|
5
|
+
interface WizardStep {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
9
|
+
interface WizardStepsProps {
|
|
10
|
+
steps: readonly WizardStep[];
|
|
11
|
+
activeIdx: number;
|
|
12
|
+
onSelect: (idx: number) => void;
|
|
13
|
+
ariaLabel?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
declare function WizardSteps({
|
|
17
|
+
steps,
|
|
18
|
+
activeIdx,
|
|
19
|
+
onSelect,
|
|
20
|
+
ariaLabel,
|
|
21
|
+
className
|
|
22
|
+
}: WizardStepsProps): _$react_jsx_runtime0.JSX.Element;
|
|
23
|
+
interface WizardProgressProps extends React.ComponentProps<"div"> {
|
|
24
|
+
current: number;
|
|
25
|
+
total: number;
|
|
26
|
+
}
|
|
27
|
+
declare function WizardProgress({
|
|
28
|
+
current,
|
|
29
|
+
total,
|
|
30
|
+
className,
|
|
31
|
+
...props
|
|
32
|
+
}: WizardProgressProps): _$react_jsx_runtime0.JSX.Element;
|
|
33
|
+
//#endregion
|
|
34
|
+
export { WizardProgress, type WizardStep, WizardSteps };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/cn.mjs";
|
|
3
|
+
import { TabsNav, TabsNavList, TabsNavTrigger } from "./tabs.mjs";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/components/wizard.tsx
|
|
6
|
+
function WizardSteps({ steps, activeIdx, onSelect, ariaLabel = "Wizard steps", className }) {
|
|
7
|
+
return /* @__PURE__ */ jsx(TabsNav, {
|
|
8
|
+
orientation: "vertical",
|
|
9
|
+
"aria-label": ariaLabel,
|
|
10
|
+
className,
|
|
11
|
+
children: /* @__PURE__ */ jsx(TabsNavList, { children: steps.map((step, idx) => /* @__PURE__ */ jsx(TabsNavTrigger, {
|
|
12
|
+
active: idx === activeIdx,
|
|
13
|
+
asChild: true,
|
|
14
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
15
|
+
type: "button",
|
|
16
|
+
onClick: () => onSelect(idx),
|
|
17
|
+
className: "w-full justify-start text-left",
|
|
18
|
+
children: step.label
|
|
19
|
+
})
|
|
20
|
+
}, step.id)) })
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function WizardProgress({ current, total, className, ...props }) {
|
|
24
|
+
const percent = total > 0 ? Math.round(current / total * 100) : 0;
|
|
25
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
26
|
+
className: cn("flex flex-col gap-1.5 px-2", className),
|
|
27
|
+
...props,
|
|
28
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
29
|
+
className: "text-muted-foreground flex items-center justify-between text-xs",
|
|
30
|
+
children: [/* @__PURE__ */ jsxs("span", { children: [
|
|
31
|
+
"Step ",
|
|
32
|
+
current,
|
|
33
|
+
"/",
|
|
34
|
+
total
|
|
35
|
+
] }), /* @__PURE__ */ jsxs("span", { children: [percent, "%"] })]
|
|
36
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
37
|
+
className: "bg-muted h-1.5 overflow-hidden rounded-full",
|
|
38
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
39
|
+
className: "bg-primary h-full transition-all",
|
|
40
|
+
style: { width: `${percent}%` }
|
|
41
|
+
})
|
|
42
|
+
})]
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { WizardProgress, WizardSteps };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alpic-ai/ui",
|
|
3
|
-
"version": "0.0.0-dev.
|
|
3
|
+
"version": "0.0.0-dev.c4dcd8d",
|
|
4
4
|
"description": "Alpic design system — shared UI components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
],
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"lucide-react": "^1.14.0",
|
|
27
|
-
"react": "^19.2.
|
|
28
|
-
"react-dom": "^19.2.
|
|
29
|
-
"react-hook-form": "^7.
|
|
27
|
+
"react": "^19.2.6",
|
|
28
|
+
"react-dom": "^19.2.6",
|
|
29
|
+
"react-hook-form": "^7.75.0",
|
|
30
30
|
"sonner": "^2.0.7",
|
|
31
|
-
"tailwindcss": "^4.
|
|
31
|
+
"tailwindcss": "^4.3.0",
|
|
32
32
|
"tw-animate-css": "^1.4.0"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
@@ -52,19 +52,19 @@
|
|
|
52
52
|
"class-variance-authority": "^0.7.1",
|
|
53
53
|
"clsx": "^2.1.1",
|
|
54
54
|
"cmdk": "^1.1.1",
|
|
55
|
-
"tailwind-merge": "^3.
|
|
55
|
+
"tailwind-merge": "^3.6.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@ladle/react": "^5.1.1",
|
|
59
|
-
"@tailwindcss/postcss": "^4.
|
|
59
|
+
"@tailwindcss/postcss": "^4.3.0",
|
|
60
60
|
"@types/react": "19.2.14",
|
|
61
61
|
"@types/react-dom": "19.2.3",
|
|
62
62
|
"lucide-react": "^1.14.0",
|
|
63
|
-
"react-hook-form": "^7.
|
|
63
|
+
"react-hook-form": "^7.75.0",
|
|
64
64
|
"shx": "^0.4.0",
|
|
65
65
|
"sonner": "^2.0.7",
|
|
66
|
-
"tailwindcss": "^4.
|
|
67
|
-
"tsdown": "^0.
|
|
66
|
+
"tailwindcss": "^4.3.0",
|
|
67
|
+
"tsdown": "^0.22.0",
|
|
68
68
|
"tw-animate-css": "^1.4.0",
|
|
69
69
|
"typescript": "^6.0.3"
|
|
70
70
|
},
|
package/src/components/form.tsx
CHANGED
|
@@ -14,8 +14,10 @@ import {
|
|
|
14
14
|
} from "react-hook-form";
|
|
15
15
|
|
|
16
16
|
import { cn } from "../lib/cn";
|
|
17
|
+
import { Checkbox } from "./checkbox";
|
|
17
18
|
import { Input, type InputProps } from "./input";
|
|
18
19
|
import { Label } from "./label";
|
|
20
|
+
import { RadioGroup, RadioGroupItem } from "./radio-group";
|
|
19
21
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
|
|
20
22
|
import { Textarea, type TextareaProps } from "./textarea";
|
|
21
23
|
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip";
|
|
@@ -325,8 +327,166 @@ function SelectField<TFieldValues extends FieldValues, TName extends FieldPath<T
|
|
|
325
327
|
);
|
|
326
328
|
}
|
|
327
329
|
|
|
330
|
+
interface RadioFieldProps<TFieldValues extends FieldValues, TName extends FieldPath<TFieldValues>>
|
|
331
|
+
extends FormFieldBaseProps<TFieldValues, TName> {
|
|
332
|
+
options: SelectFieldOption[];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function RadioField<TFieldValues extends FieldValues, TName extends FieldPath<TFieldValues>>({
|
|
336
|
+
control,
|
|
337
|
+
name,
|
|
338
|
+
rules,
|
|
339
|
+
required,
|
|
340
|
+
label,
|
|
341
|
+
description,
|
|
342
|
+
tooltip,
|
|
343
|
+
options,
|
|
344
|
+
}: RadioFieldProps<TFieldValues, TName>) {
|
|
345
|
+
return (
|
|
346
|
+
<FormField
|
|
347
|
+
control={control}
|
|
348
|
+
name={name}
|
|
349
|
+
rules={rules}
|
|
350
|
+
render={({ field }) => (
|
|
351
|
+
<FormItem>
|
|
352
|
+
{label && (
|
|
353
|
+
<FormLabel required={required} tooltip={tooltip}>
|
|
354
|
+
{label}
|
|
355
|
+
</FormLabel>
|
|
356
|
+
)}
|
|
357
|
+
<FormControl>
|
|
358
|
+
<RadioGroup value={field.value ?? ""} onValueChange={field.onChange} onBlur={field.onBlur}>
|
|
359
|
+
{options.map((option) => {
|
|
360
|
+
const itemId = `${field.name}-${option.value}`;
|
|
361
|
+
return (
|
|
362
|
+
<div key={option.value} className="flex items-center gap-2">
|
|
363
|
+
<RadioGroupItem id={itemId} value={option.value} disabled={option.disabled} />
|
|
364
|
+
<Label htmlFor={itemId} className="type-text-sm font-normal">
|
|
365
|
+
{option.label}
|
|
366
|
+
</Label>
|
|
367
|
+
</div>
|
|
368
|
+
);
|
|
369
|
+
})}
|
|
370
|
+
</RadioGroup>
|
|
371
|
+
</FormControl>
|
|
372
|
+
{description && <FormDescription>{description}</FormDescription>}
|
|
373
|
+
<FormMessage />
|
|
374
|
+
</FormItem>
|
|
375
|
+
)}
|
|
376
|
+
/>
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
interface ChecklistFieldProps<TFieldValues extends FieldValues, TName extends FieldPath<TFieldValues>>
|
|
381
|
+
extends FormFieldBaseProps<TFieldValues, TName> {
|
|
382
|
+
options: SelectFieldOption[];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function ChecklistField<TFieldValues extends FieldValues, TName extends FieldPath<TFieldValues>>({
|
|
386
|
+
control,
|
|
387
|
+
name,
|
|
388
|
+
rules,
|
|
389
|
+
required,
|
|
390
|
+
label,
|
|
391
|
+
description,
|
|
392
|
+
tooltip,
|
|
393
|
+
options,
|
|
394
|
+
}: ChecklistFieldProps<TFieldValues, TName>) {
|
|
395
|
+
return (
|
|
396
|
+
<FormField
|
|
397
|
+
control={control}
|
|
398
|
+
name={name}
|
|
399
|
+
rules={rules}
|
|
400
|
+
render={({ field }) => {
|
|
401
|
+
const selected: string[] = Array.isArray(field.value) ? field.value : [];
|
|
402
|
+
const toggle = (value: string, checked: boolean) => {
|
|
403
|
+
if (checked) {
|
|
404
|
+
field.onChange([...selected, value]);
|
|
405
|
+
} else {
|
|
406
|
+
field.onChange(selected.filter((existing) => existing !== value));
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
return (
|
|
410
|
+
<FormItem>
|
|
411
|
+
{label && (
|
|
412
|
+
<FormLabel required={required} tooltip={tooltip}>
|
|
413
|
+
{label}
|
|
414
|
+
</FormLabel>
|
|
415
|
+
)}
|
|
416
|
+
<FormControl>
|
|
417
|
+
<fieldset className="m-0 flex flex-col gap-2 border-0 p-0" onBlur={field.onBlur}>
|
|
418
|
+
{options.map((option) => {
|
|
419
|
+
const itemId = `${field.name}-${option.value}`;
|
|
420
|
+
const checked = selected.includes(option.value);
|
|
421
|
+
return (
|
|
422
|
+
<div key={option.value} className="flex items-start gap-2">
|
|
423
|
+
<Checkbox
|
|
424
|
+
id={itemId}
|
|
425
|
+
checked={checked}
|
|
426
|
+
onCheckedChange={(next) => toggle(option.value, Boolean(next))}
|
|
427
|
+
disabled={option.disabled}
|
|
428
|
+
/>
|
|
429
|
+
<Label htmlFor={itemId} className="type-text-sm font-normal leading-tight">
|
|
430
|
+
{option.label}
|
|
431
|
+
</Label>
|
|
432
|
+
</div>
|
|
433
|
+
);
|
|
434
|
+
})}
|
|
435
|
+
</fieldset>
|
|
436
|
+
</FormControl>
|
|
437
|
+
{description && <FormDescription>{description}</FormDescription>}
|
|
438
|
+
<FormMessage />
|
|
439
|
+
</FormItem>
|
|
440
|
+
);
|
|
441
|
+
}}
|
|
442
|
+
/>
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
interface CheckboxFieldProps<TFieldValues extends FieldValues, TName extends FieldPath<TFieldValues>>
|
|
447
|
+
extends Omit<FormFieldBaseProps<TFieldValues, TName>, "required"> {}
|
|
448
|
+
|
|
449
|
+
function CheckboxField<TFieldValues extends FieldValues, TName extends FieldPath<TFieldValues>>({
|
|
450
|
+
control,
|
|
451
|
+
name,
|
|
452
|
+
rules,
|
|
453
|
+
label,
|
|
454
|
+
description,
|
|
455
|
+
tooltip,
|
|
456
|
+
}: CheckboxFieldProps<TFieldValues, TName>) {
|
|
457
|
+
return (
|
|
458
|
+
<FormField
|
|
459
|
+
control={control}
|
|
460
|
+
name={name}
|
|
461
|
+
rules={rules}
|
|
462
|
+
render={({ field }) => (
|
|
463
|
+
<FormItem className="gap-1.5">
|
|
464
|
+
<div className="flex items-start gap-2">
|
|
465
|
+
<FormControl>
|
|
466
|
+
<Checkbox
|
|
467
|
+
checked={field.value ?? false}
|
|
468
|
+
onCheckedChange={(next) => field.onChange(Boolean(next))}
|
|
469
|
+
onBlur={field.onBlur}
|
|
470
|
+
/>
|
|
471
|
+
</FormControl>
|
|
472
|
+
{label && (
|
|
473
|
+
<FormLabel tooltip={tooltip} className="font-normal">
|
|
474
|
+
{label}
|
|
475
|
+
</FormLabel>
|
|
476
|
+
)}
|
|
477
|
+
</div>
|
|
478
|
+
{description && <FormDescription className="ml-6">{description}</FormDescription>}
|
|
479
|
+
<FormMessage className="ml-6" />
|
|
480
|
+
</FormItem>
|
|
481
|
+
)}
|
|
482
|
+
/>
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
328
486
|
export type { SelectFieldOption };
|
|
329
487
|
export {
|
|
488
|
+
CheckboxField,
|
|
489
|
+
ChecklistField,
|
|
330
490
|
Form,
|
|
331
491
|
FormControl,
|
|
332
492
|
FormDescription,
|
|
@@ -337,6 +497,7 @@ export {
|
|
|
337
497
|
FormLabel,
|
|
338
498
|
FormMessage,
|
|
339
499
|
InputField,
|
|
500
|
+
RadioField,
|
|
340
501
|
SelectField,
|
|
341
502
|
TextareaField,
|
|
342
503
|
useFormField,
|
package/src/components/table.tsx
CHANGED
|
@@ -44,7 +44,6 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
|
|
44
44
|
"border-b border-border-secondary transition-colors",
|
|
45
45
|
"data-[state=selected]:bg-muted",
|
|
46
46
|
"[@media(hover:hover)]:hover:bg-background-hover dark:[@media(hover:hover)]:hover:bg-muted",
|
|
47
|
-
"[@media(hover:hover)]:[&:hover_button:hover]:bg-subtle",
|
|
48
47
|
className,
|
|
49
48
|
)}
|
|
50
49
|
{...props}
|
|
@@ -58,7 +57,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
|
|
58
57
|
data-slot="table-head"
|
|
59
58
|
className={cn(
|
|
60
59
|
"h-11 px-6 py-3 bg-muted text-left align-middle type-text-xs font-semibold text-placeholder dark:text-subtle-foreground whitespace-nowrap",
|
|
61
|
-
"[&:has([role=checkbox])]:w-px [&:has([role=checkbox])]:
|
|
60
|
+
"[&:has([role=checkbox])]:w-px [&:has([role=checkbox])]:px-0",
|
|
62
61
|
className,
|
|
63
62
|
)}
|
|
64
63
|
{...props}
|
|
@@ -82,7 +81,7 @@ function TableCell({ className, interactive = false, ...props }: TableCellProps)
|
|
|
82
81
|
className={cn(
|
|
83
82
|
"align-middle",
|
|
84
83
|
interactive ? "h-px p-0" : "px-6 py-2",
|
|
85
|
-
"[&:has([role=checkbox])]:w-px [&:has([role=checkbox])]:
|
|
84
|
+
"[&:has([role=checkbox])]:w-px [&:has([role=checkbox])]:px-0",
|
|
86
85
|
className,
|
|
87
86
|
)}
|
|
88
87
|
{...props}
|
package/src/components/tabs.tsx
CHANGED
|
@@ -44,10 +44,10 @@ const tabsTriggerVariants = cva(
|
|
|
44
44
|
"data-[state=active]:border-b-2 data-[state=active]:border-foreground data-[state=active]:text-foreground",
|
|
45
45
|
],
|
|
46
46
|
pill: [
|
|
47
|
-
"rounded-
|
|
48
|
-
"text-
|
|
49
|
-
"[@media(hover:hover)]:hover:bg-accent [@media(hover:hover)]:hover:text-
|
|
50
|
-
"data-[state=active]:bg-accent data-[state=active]:text-
|
|
47
|
+
"rounded-md px-2 py-1.5",
|
|
48
|
+
"text-quaternary-foreground",
|
|
49
|
+
"[@media(hover:hover)]:hover:bg-accent [@media(hover:hover)]:hover:text-muted-foreground",
|
|
50
|
+
"data-[state=active]:bg-accent data-[state=active]:text-muted-foreground",
|
|
51
51
|
],
|
|
52
52
|
},
|
|
53
53
|
},
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Wizard family — primitives for multi-step flows.
|
|
5
|
+
*
|
|
6
|
+
* - WizardSteps — vertical step rail (controlled by activeIdx + onSelect)
|
|
7
|
+
* - WizardProgress — step counter + progress bar
|
|
8
|
+
*
|
|
9
|
+
* Consumers compose these inside whatever container they need (sticky aside, modal, etc.).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type * as React from "react";
|
|
13
|
+
|
|
14
|
+
import { cn } from "../lib/cn";
|
|
15
|
+
import { TabsNav, TabsNavList, TabsNavTrigger } from "./tabs";
|
|
16
|
+
|
|
17
|
+
interface WizardStep {
|
|
18
|
+
id: string;
|
|
19
|
+
label: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface WizardStepsProps {
|
|
23
|
+
steps: readonly WizardStep[];
|
|
24
|
+
activeIdx: number;
|
|
25
|
+
onSelect: (idx: number) => void;
|
|
26
|
+
ariaLabel?: string;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function WizardSteps({ steps, activeIdx, onSelect, ariaLabel = "Wizard steps", className }: WizardStepsProps) {
|
|
31
|
+
return (
|
|
32
|
+
<TabsNav orientation="vertical" aria-label={ariaLabel} className={className}>
|
|
33
|
+
<TabsNavList>
|
|
34
|
+
{steps.map((step, idx) => (
|
|
35
|
+
<TabsNavTrigger key={step.id} active={idx === activeIdx} asChild>
|
|
36
|
+
<button type="button" onClick={() => onSelect(idx)} className="w-full justify-start text-left">
|
|
37
|
+
{step.label}
|
|
38
|
+
</button>
|
|
39
|
+
</TabsNavTrigger>
|
|
40
|
+
))}
|
|
41
|
+
</TabsNavList>
|
|
42
|
+
</TabsNav>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface WizardProgressProps extends React.ComponentProps<"div"> {
|
|
47
|
+
current: number;
|
|
48
|
+
total: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function WizardProgress({ current, total, className, ...props }: WizardProgressProps) {
|
|
52
|
+
const percent = total > 0 ? Math.round((current / total) * 100) : 0;
|
|
53
|
+
return (
|
|
54
|
+
<div className={cn("flex flex-col gap-1.5 px-2", className)} {...props}>
|
|
55
|
+
<div className="text-muted-foreground flex items-center justify-between text-xs">
|
|
56
|
+
<span>
|
|
57
|
+
Step {current}/{total}
|
|
58
|
+
</span>
|
|
59
|
+
<span>{percent}%</span>
|
|
60
|
+
</div>
|
|
61
|
+
<div className="bg-muted h-1.5 overflow-hidden rounded-full">
|
|
62
|
+
<div className="bg-primary h-full transition-all" style={{ width: `${percent}%` }} />
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type { WizardStep };
|
|
69
|
+
export { WizardProgress, WizardSteps };
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import type { Story } from "@ladle/react";
|
|
2
2
|
import { useForm } from "react-hook-form";
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
CheckboxField,
|
|
6
|
+
ChecklistField,
|
|
7
|
+
Form,
|
|
8
|
+
FormFields,
|
|
9
|
+
FormHeader,
|
|
10
|
+
InputField,
|
|
11
|
+
RadioField,
|
|
12
|
+
SelectField,
|
|
13
|
+
TextareaField,
|
|
14
|
+
} from "../components/form";
|
|
4
15
|
|
|
5
16
|
/* ── Types ───────────────────────────────────────────────────────────────── */
|
|
6
17
|
|
|
@@ -8,6 +19,9 @@ interface CreateProjectForm {
|
|
|
8
19
|
email: string;
|
|
9
20
|
role: string;
|
|
10
21
|
description: string;
|
|
22
|
+
visibility: string;
|
|
23
|
+
capabilities: string[];
|
|
24
|
+
acceptTerms: boolean;
|
|
11
25
|
}
|
|
12
26
|
|
|
13
27
|
const roles = [
|
|
@@ -16,11 +30,31 @@ const roles = [
|
|
|
16
30
|
{ value: "viewer", label: "Viewer" },
|
|
17
31
|
];
|
|
18
32
|
|
|
33
|
+
const visibilities = [
|
|
34
|
+
{ value: "public", label: "Public" },
|
|
35
|
+
{ value: "private", label: "Private" },
|
|
36
|
+
{ value: "internal", label: "Internal" },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const capabilities = [
|
|
40
|
+
{ value: "read", label: "Read" },
|
|
41
|
+
{ value: "write", label: "Write" },
|
|
42
|
+
{ value: "deploy", label: "Deploy" },
|
|
43
|
+
{ value: "admin", label: "Admin" },
|
|
44
|
+
];
|
|
45
|
+
|
|
19
46
|
/* ── Composed Form ───────────────────────────────────────────────────────── */
|
|
20
47
|
|
|
21
48
|
function ComposedForm() {
|
|
22
49
|
const form = useForm<CreateProjectForm>({
|
|
23
|
-
defaultValues: {
|
|
50
|
+
defaultValues: {
|
|
51
|
+
email: "",
|
|
52
|
+
role: "",
|
|
53
|
+
description: "",
|
|
54
|
+
visibility: "",
|
|
55
|
+
capabilities: [],
|
|
56
|
+
acceptTerms: false,
|
|
57
|
+
},
|
|
24
58
|
});
|
|
25
59
|
|
|
26
60
|
const onSubmit = (data: CreateProjectForm) => {
|
|
@@ -64,6 +98,34 @@ function ComposedForm() {
|
|
|
64
98
|
placeholder="A short description of your project..."
|
|
65
99
|
required
|
|
66
100
|
/>
|
|
101
|
+
|
|
102
|
+
<RadioField
|
|
103
|
+
control={form.control}
|
|
104
|
+
name="visibility"
|
|
105
|
+
rules={{ required: "Pick a visibility." }}
|
|
106
|
+
label="Visibility"
|
|
107
|
+
description="Who can see and access this project."
|
|
108
|
+
options={visibilities}
|
|
109
|
+
required
|
|
110
|
+
/>
|
|
111
|
+
|
|
112
|
+
<ChecklistField
|
|
113
|
+
control={form.control}
|
|
114
|
+
name="capabilities"
|
|
115
|
+
rules={{ validate: (value) => (value?.length ? true : "Pick at least one capability.") }}
|
|
116
|
+
label="Capabilities"
|
|
117
|
+
description="Select all that apply."
|
|
118
|
+
options={capabilities}
|
|
119
|
+
required
|
|
120
|
+
/>
|
|
121
|
+
|
|
122
|
+
<CheckboxField
|
|
123
|
+
control={form.control}
|
|
124
|
+
name="acceptTerms"
|
|
125
|
+
rules={{ validate: (value) => (value ? true : "You must accept the terms.") }}
|
|
126
|
+
label="I accept the terms and conditions"
|
|
127
|
+
description="Required to create the project."
|
|
128
|
+
/>
|
|
67
129
|
</FormFields>
|
|
68
130
|
|
|
69
131
|
<button
|
|
@@ -18,9 +18,11 @@ const navTabs = [
|
|
|
18
18
|
|
|
19
19
|
const sideNavTabs = [
|
|
20
20
|
{ id: "general", label: "General" },
|
|
21
|
+
{ id: "build-settings", label: "Build settings" },
|
|
22
|
+
{ id: "environment-variables", label: "Environment variables" },
|
|
23
|
+
{ id: "domains", label: "Domains" },
|
|
21
24
|
{ id: "authentication", label: "Authentication" },
|
|
22
|
-
{ id: "
|
|
23
|
-
{ id: "billing", label: "Billing" },
|
|
25
|
+
{ id: "marketplace", label: "Marketplace" },
|
|
24
26
|
];
|
|
25
27
|
|
|
26
28
|
function useHashRoute(fallback: string) {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Story } from "@ladle/react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
|
|
4
|
+
import { WizardProgress, type WizardStep, WizardSteps } from "../components/wizard";
|
|
5
|
+
|
|
6
|
+
const SECTION_HEADER = "type-text-xs font-medium text-muted-foreground uppercase tracking-wide pt-4";
|
|
7
|
+
|
|
8
|
+
const steps: WizardStep[] = [
|
|
9
|
+
{ id: "overview", label: "Overview" },
|
|
10
|
+
{ id: "branding", label: "Branding & metadata" },
|
|
11
|
+
{ id: "auth", label: "Authentication" },
|
|
12
|
+
{ id: "tools", label: "Tools & test cases" },
|
|
13
|
+
{ id: "review", label: "Review & submit" },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export const AllVariants: Story = () => {
|
|
17
|
+
const [activeIdx, setActiveIdx] = useState(1);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="flex flex-col gap-10 p-8">
|
|
21
|
+
{/* Full rail (steps + progress) */}
|
|
22
|
+
<div>
|
|
23
|
+
<p className={SECTION_HEADER}>Full rail — steps + progress</p>
|
|
24
|
+
<div className="mt-4 flex gap-6">
|
|
25
|
+
<aside className="basis-56 shrink-0 flex flex-col gap-4 self-start">
|
|
26
|
+
<WizardSteps steps={steps} activeIdx={activeIdx} onSelect={setActiveIdx} ariaLabel="Submission steps" />
|
|
27
|
+
<WizardProgress current={activeIdx + 1} total={steps.length} />
|
|
28
|
+
</aside>
|
|
29
|
+
<div className="flex-1 rounded-md border p-4">
|
|
30
|
+
<p className="type-text-sm text-muted-foreground">Content for step "{steps[activeIdx]?.label}"</p>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
{/* Steps only */}
|
|
36
|
+
<div>
|
|
37
|
+
<p className={SECTION_HEADER}>Steps only</p>
|
|
38
|
+
<div className="mt-4 max-w-56">
|
|
39
|
+
<WizardSteps steps={steps} activeIdx={activeIdx} onSelect={setActiveIdx} />
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
{/* Progress only — various positions */}
|
|
44
|
+
<div>
|
|
45
|
+
<p className={SECTION_HEADER}>Progress — first step</p>
|
|
46
|
+
<div className="mt-4 max-w-56">
|
|
47
|
+
<WizardProgress current={1} total={5} />
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
<div>
|
|
51
|
+
<p className={SECTION_HEADER}>Progress — mid-flow</p>
|
|
52
|
+
<div className="mt-4 max-w-56">
|
|
53
|
+
<WizardProgress current={3} total={5} />
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<div>
|
|
57
|
+
<p className={SECTION_HEADER}>Progress — complete</p>
|
|
58
|
+
<div className="mt-4 max-w-56">
|
|
59
|
+
<WizardProgress current={5} total={5} />
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
};
|