@zedtech/zedkit-ui 0.0.0-stage → 0.1.1

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 (169) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +48 -2
  3. package/dist/alert.d.ts +9 -0
  4. package/dist/alert.d.ts.map +1 -0
  5. package/dist/alert.js +26 -0
  6. package/dist/alert.js.map +1 -0
  7. package/dist/app-shell.d.ts +9 -0
  8. package/dist/app-shell.d.ts.map +1 -0
  9. package/dist/app-shell.js +6 -0
  10. package/dist/app-shell.js.map +1 -0
  11. package/dist/badge.d.ts +10 -0
  12. package/dist/badge.d.ts.map +1 -0
  13. package/dist/badge.js +19 -0
  14. package/dist/badge.js.map +1 -0
  15. package/dist/button.d.ts +12 -0
  16. package/dist/button.d.ts.map +1 -0
  17. package/dist/button.js +31 -0
  18. package/dist/button.js.map +1 -0
  19. package/dist/card.d.ts +9 -0
  20. package/dist/card.d.ts.map +1 -0
  21. package/dist/card.js +17 -0
  22. package/dist/card.js.map +1 -0
  23. package/dist/checkbox.d.ts +5 -0
  24. package/dist/checkbox.d.ts.map +1 -0
  25. package/dist/checkbox.js +10 -0
  26. package/dist/checkbox.js.map +1 -0
  27. package/dist/confirm-dialog.d.ts +17 -0
  28. package/dist/confirm-dialog.d.ts.map +1 -0
  29. package/dist/confirm-dialog.js +18 -0
  30. package/dist/confirm-dialog.js.map +1 -0
  31. package/dist/data-table.d.ts +33 -0
  32. package/dist/data-table.d.ts.map +1 -0
  33. package/dist/data-table.js +40 -0
  34. package/dist/data-table.js.map +1 -0
  35. package/dist/detail-panel.d.ts +11 -0
  36. package/dist/detail-panel.d.ts.map +1 -0
  37. package/dist/detail-panel.js +6 -0
  38. package/dist/detail-panel.js.map +1 -0
  39. package/dist/dialog.d.ts +14 -0
  40. package/dist/dialog.d.ts.map +1 -0
  41. package/dist/dialog.js +26 -0
  42. package/dist/dialog.js.map +1 -0
  43. package/dist/field.d.ts +26 -0
  44. package/dist/field.d.ts.map +1 -0
  45. package/dist/field.js +74 -0
  46. package/dist/field.js.map +1 -0
  47. package/dist/form-submit.d.ts +16 -0
  48. package/dist/form-submit.d.ts.map +1 -0
  49. package/dist/form-submit.js +15 -0
  50. package/dist/form-submit.js.map +1 -0
  51. package/dist/form.d.ts +26 -0
  52. package/dist/form.d.ts.map +1 -0
  53. package/dist/form.js +65 -0
  54. package/dist/form.js.map +1 -0
  55. package/dist/index.d.ts +33 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +33 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/input.d.ts +4 -0
  60. package/dist/input.d.ts.map +1 -0
  61. package/dist/input.js +9 -0
  62. package/dist/input.js.map +1 -0
  63. package/dist/label.d.ts +6 -0
  64. package/dist/label.d.ts.map +1 -0
  65. package/dist/label.js +11 -0
  66. package/dist/label.js.map +1 -0
  67. package/dist/layout.d.ts +22 -0
  68. package/dist/layout.d.ts.map +1 -0
  69. package/dist/layout.js +36 -0
  70. package/dist/layout.js.map +1 -0
  71. package/dist/live-pulse.d.ts +7 -0
  72. package/dist/live-pulse.d.ts.map +1 -0
  73. package/dist/live-pulse.js +6 -0
  74. package/dist/live-pulse.js.map +1 -0
  75. package/dist/option-cards.d.ts +15 -0
  76. package/dist/option-cards.d.ts.map +1 -0
  77. package/dist/option-cards.js +9 -0
  78. package/dist/option-cards.js.map +1 -0
  79. package/dist/page-header.d.ts +9 -0
  80. package/dist/page-header.d.ts.map +1 -0
  81. package/dist/page-header.js +6 -0
  82. package/dist/page-header.js.map +1 -0
  83. package/dist/secret-field.d.ts +22 -0
  84. package/dist/secret-field.d.ts.map +1 -0
  85. package/dist/secret-field.js +26 -0
  86. package/dist/secret-field.js.map +1 -0
  87. package/dist/select.d.ts +14 -0
  88. package/dist/select.d.ts.map +1 -0
  89. package/dist/select.js +30 -0
  90. package/dist/select.js.map +1 -0
  91. package/dist/separator.d.ts +5 -0
  92. package/dist/separator.d.ts.map +1 -0
  93. package/dist/separator.js +9 -0
  94. package/dist/separator.js.map +1 -0
  95. package/dist/skeleton.d.ts +3 -0
  96. package/dist/skeleton.d.ts.map +1 -0
  97. package/dist/skeleton.js +7 -0
  98. package/dist/skeleton.js.map +1 -0
  99. package/dist/sonner.d.ts +8 -0
  100. package/dist/sonner.d.ts.map +1 -0
  101. package/dist/sonner.js +19 -0
  102. package/dist/sonner.js.map +1 -0
  103. package/dist/switch.d.ts +5 -0
  104. package/dist/switch.d.ts.map +1 -0
  105. package/dist/switch.js +9 -0
  106. package/dist/switch.js.map +1 -0
  107. package/dist/table.d.ts +11 -0
  108. package/dist/table.d.ts.map +1 -0
  109. package/dist/table.js +28 -0
  110. package/dist/table.js.map +1 -0
  111. package/dist/tabs.d.ts +8 -0
  112. package/dist/tabs.d.ts.map +1 -0
  113. package/dist/tabs.js +14 -0
  114. package/dist/tabs.js.map +1 -0
  115. package/dist/textarea.d.ts +4 -0
  116. package/dist/textarea.d.ts.map +1 -0
  117. package/dist/textarea.js +9 -0
  118. package/dist/textarea.js.map +1 -0
  119. package/dist/typography.d.ts +22 -0
  120. package/dist/typography.d.ts.map +1 -0
  121. package/dist/typography.js +39 -0
  122. package/dist/typography.js.map +1 -0
  123. package/dist/use-validated-form.d.ts +10 -0
  124. package/dist/use-validated-form.d.ts.map +1 -0
  125. package/dist/use-validated-form.js +20 -0
  126. package/dist/use-validated-form.js.map +1 -0
  127. package/dist/utils.d.ts +8 -0
  128. package/dist/utils.d.ts.map +1 -0
  129. package/dist/utils.js +11 -0
  130. package/dist/utils.js.map +1 -0
  131. package/dist/wizard-steps.d.ts +8 -0
  132. package/dist/wizard-steps.d.ts.map +1 -0
  133. package/dist/wizard-steps.js +7 -0
  134. package/dist/wizard-steps.js.map +1 -0
  135. package/package.json +83 -3
  136. package/src/alert.tsx +47 -0
  137. package/src/app-shell.tsx +24 -0
  138. package/src/badge.tsx +30 -0
  139. package/src/button.tsx +48 -0
  140. package/src/card.tsx +47 -0
  141. package/src/checkbox.tsx +28 -0
  142. package/src/confirm-dialog.tsx +65 -0
  143. package/src/data-table.tsx +176 -0
  144. package/src/detail-panel.tsx +26 -0
  145. package/src/dialog.tsx +102 -0
  146. package/src/field.tsx +230 -0
  147. package/src/form-submit.tsx +29 -0
  148. package/src/form.tsx +157 -0
  149. package/src/index.ts +95 -0
  150. package/src/input.tsx +21 -0
  151. package/src/label.tsx +20 -0
  152. package/src/layout.tsx +69 -0
  153. package/src/live-pulse.tsx +27 -0
  154. package/src/option-cards.tsx +47 -0
  155. package/src/page-header.tsx +22 -0
  156. package/src/secret-field.tsx +50 -0
  157. package/src/select.tsx +146 -0
  158. package/src/separator.tsx +30 -0
  159. package/src/skeleton.tsx +7 -0
  160. package/src/sonner.tsx +25 -0
  161. package/src/styles.css +117 -0
  162. package/src/switch.tsx +29 -0
  163. package/src/table.tsx +68 -0
  164. package/src/tabs.tsx +52 -0
  165. package/src/textarea.tsx +21 -0
  166. package/src/typography.tsx +59 -0
  167. package/src/use-validated-form.ts +30 -0
  168. package/src/utils.ts +11 -0
  169. package/src/wizard-steps.tsx +40 -0
package/src/field.tsx ADDED
@@ -0,0 +1,230 @@
1
+ "use client";
2
+
3
+ import type * as React from "react";
4
+ import { useMemo } from "react";
5
+ import { cva, type VariantProps } from "class-variance-authority";
6
+
7
+ import { cn } from "./utils.js";
8
+ import { Label } from "./label.js";
9
+ import { Separator } from "./separator.js";
10
+
11
+ function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
12
+ return (
13
+ <fieldset
14
+ data-slot="field-set"
15
+ className={cn(
16
+ "flex flex-col gap-6",
17
+ "has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
18
+ className,
19
+ )}
20
+ {...props}
21
+ />
22
+ );
23
+ }
24
+
25
+ function FieldLegend({
26
+ className,
27
+ variant = "legend",
28
+ ...props
29
+ }: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
30
+ return (
31
+ <legend
32
+ data-slot="field-legend"
33
+ data-variant={variant}
34
+ className={cn(
35
+ "mb-3 font-medium",
36
+ "data-[variant=legend]:text-base",
37
+ "data-[variant=label]:text-sm",
38
+ className,
39
+ )}
40
+ {...props}
41
+ />
42
+ );
43
+ }
44
+
45
+ function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
46
+ return (
47
+ <div
48
+ data-slot="field-group"
49
+ className={cn(
50
+ "group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
51
+ className,
52
+ )}
53
+ {...props}
54
+ />
55
+ );
56
+ }
57
+
58
+ const fieldVariants = cva("group/field flex w-full gap-3 data-[invalid=true]:text-destructive", {
59
+ variants: {
60
+ orientation: {
61
+ vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
62
+ horizontal: [
63
+ "flex-row items-center",
64
+ "[&>[data-slot=field-label]]:flex-auto",
65
+ "has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
66
+ ],
67
+ responsive: [
68
+ "flex-col @md/field-group:flex-row @md/field-group:items-center [&>*]:w-full @md/field-group:[&>*]:w-auto [&>.sr-only]:w-auto",
69
+ "@md/field-group:[&>[data-slot=field-label]]:flex-auto",
70
+ "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
71
+ ],
72
+ },
73
+ },
74
+ defaultVariants: {
75
+ orientation: "vertical",
76
+ },
77
+ });
78
+
79
+ function Field({
80
+ className,
81
+ orientation = "vertical",
82
+ ...props
83
+ }: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
84
+ return (
85
+ <div
86
+ role="group"
87
+ data-slot="field"
88
+ data-orientation={orientation}
89
+ className={cn(fieldVariants({ orientation }), className)}
90
+ {...props}
91
+ />
92
+ );
93
+ }
94
+
95
+ function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
96
+ return (
97
+ <div
98
+ data-slot="field-content"
99
+ className={cn("group/field-content flex flex-1 flex-col gap-1.5 leading-snug", className)}
100
+ {...props}
101
+ />
102
+ );
103
+ }
104
+
105
+ function FieldLabel({ className, ...props }: React.ComponentProps<typeof Label>) {
106
+ return (
107
+ <Label
108
+ data-slot="field-label"
109
+ className={cn(
110
+ "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
111
+ "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>[data-slot=field]]:p-4",
112
+ "has-data-[state=checked]:border-primary has-data-[state=checked]:bg-primary/5 dark:has-data-[state=checked]:bg-primary/10",
113
+ className,
114
+ )}
115
+ {...props}
116
+ />
117
+ );
118
+ }
119
+
120
+ function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
121
+ return (
122
+ <div
123
+ data-slot="field-label"
124
+ className={cn(
125
+ "flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
126
+ className,
127
+ )}
128
+ {...props}
129
+ />
130
+ );
131
+ }
132
+
133
+ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
134
+ return (
135
+ <p
136
+ data-slot="field-description"
137
+ className={cn(
138
+ "text-sm leading-normal font-normal text-muted-foreground group-has-[[data-orientation=horizontal]]/field:text-balance",
139
+ "last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
140
+ "[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
141
+ className,
142
+ )}
143
+ {...props}
144
+ />
145
+ );
146
+ }
147
+
148
+ function FieldSeparator({
149
+ children,
150
+ className,
151
+ ...props
152
+ }: React.ComponentProps<"div"> & {
153
+ children?: React.ReactNode;
154
+ }) {
155
+ return (
156
+ <div
157
+ data-slot="field-separator"
158
+ data-content={!!children}
159
+ className={cn("relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2", className)}
160
+ {...props}
161
+ >
162
+ <Separator className="absolute inset-0 top-1/2" />
163
+ {children && (
164
+ <span
165
+ className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
166
+ data-slot="field-separator-content"
167
+ >
168
+ {children}
169
+ </span>
170
+ )}
171
+ </div>
172
+ );
173
+ }
174
+
175
+ function FieldError({
176
+ className,
177
+ children,
178
+ errors,
179
+ ...props
180
+ }: React.ComponentProps<"div"> & {
181
+ errors?: Array<{ message?: string } | undefined>;
182
+ }) {
183
+ const content = useMemo(() => {
184
+ if (children) {
185
+ return children;
186
+ }
187
+
188
+ if (!errors) {
189
+ return null;
190
+ }
191
+
192
+ if (errors?.length === 1 && errors[0]?.message) {
193
+ return errors[0].message;
194
+ }
195
+
196
+ return (
197
+ <ul className="ml-4 flex list-disc flex-col gap-1">
198
+ {errors.map((error, index) => error?.message && <li key={index}>{error.message}</li>)}
199
+ </ul>
200
+ );
201
+ }, [children, errors]);
202
+
203
+ if (!content) {
204
+ return null;
205
+ }
206
+
207
+ return (
208
+ <div
209
+ role="alert"
210
+ data-slot="field-error"
211
+ className={cn("text-sm font-normal text-destructive", className)}
212
+ {...props}
213
+ >
214
+ {content}
215
+ </div>
216
+ );
217
+ }
218
+
219
+ export {
220
+ Field,
221
+ FieldLabel,
222
+ FieldDescription,
223
+ FieldError,
224
+ FieldGroup,
225
+ FieldLegend,
226
+ FieldSeparator,
227
+ FieldSet,
228
+ FieldContent,
229
+ FieldTitle,
230
+ };
@@ -0,0 +1,29 @@
1
+ "use client";
2
+
3
+ import type * as React from "react";
4
+ import { useFormContext } from "react-hook-form";
5
+
6
+ import { Button, type ButtonProps } from "./button.js";
7
+
8
+ export type FormSubmitProps = Omit<ButtonProps, "type"> & {
9
+ /**
10
+ * Label shown while the form submits. Defaults to `children`.
11
+ * Both come from the caller (message catalog), never hard-coded.
12
+ */
13
+ pendingLabel?: React.ReactNode;
14
+ };
15
+
16
+ /**
17
+ * Submit button wired to the enclosing form: disabled while the
18
+ * submission is in flight, with an optional pending label.
19
+ * Must render inside `<Form>` (uses the form context).
20
+ */
21
+ export function FormSubmit({ pendingLabel, children, disabled, ...props }: FormSubmitProps) {
22
+ const { formState } = useFormContext();
23
+ const pending = formState.isSubmitting;
24
+ return (
25
+ <Button type="submit" disabled={disabled === true || pending} {...props}>
26
+ {pending && pendingLabel !== undefined ? pendingLabel : children}
27
+ </Button>
28
+ );
29
+ }
package/src/form.tsx ADDED
@@ -0,0 +1,157 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as LabelPrimitive from "@radix-ui/react-label";
5
+ import { Slot } from "@radix-ui/react-slot";
6
+ import {
7
+ Controller,
8
+ FormProvider,
9
+ useFormContext,
10
+ type ControllerProps,
11
+ type FieldPath,
12
+ type FieldValues,
13
+ } from "react-hook-form";
14
+
15
+ import { cn } from "./utils.js";
16
+ import { Label } from "./label.js";
17
+
18
+ const Form = FormProvider;
19
+
20
+ type FormFieldContextValue<
21
+ TFieldValues extends FieldValues = FieldValues,
22
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
23
+ > = {
24
+ name: TName;
25
+ };
26
+
27
+ const FormFieldContext = React.createContext<FormFieldContextValue | null>(null);
28
+
29
+ const FormField = <
30
+ TFieldValues extends FieldValues = FieldValues,
31
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
32
+ >({
33
+ ...props
34
+ }: ControllerProps<TFieldValues, TName>) => {
35
+ return (
36
+ <FormFieldContext.Provider value={{ name: props.name }}>
37
+ <Controller {...props} />
38
+ </FormFieldContext.Provider>
39
+ );
40
+ };
41
+
42
+ const useFormField = () => {
43
+ const fieldContext = React.useContext(FormFieldContext);
44
+ const itemContext = React.useContext(FormItemContext);
45
+ const { getFieldState, formState } = useFormContext();
46
+
47
+ if (!fieldContext) {
48
+ throw new Error("useFormField should be used within <FormField>");
49
+ }
50
+
51
+ if (!itemContext) {
52
+ throw new Error("useFormField should be used within <FormItem>");
53
+ }
54
+
55
+ const fieldState = getFieldState(fieldContext.name, formState);
56
+
57
+ const { id } = itemContext;
58
+
59
+ return {
60
+ id,
61
+ name: fieldContext.name,
62
+ formItemId: `${id}-form-item`,
63
+ formDescriptionId: `${id}-form-item-description`,
64
+ formMessageId: `${id}-form-item-message`,
65
+ ...fieldState,
66
+ };
67
+ };
68
+
69
+ type FormItemContextValue = {
70
+ id: string;
71
+ };
72
+
73
+ const FormItemContext = React.createContext<FormItemContextValue | null>(null);
74
+
75
+ const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
76
+ ({ className, ...props }, ref) => {
77
+ const id = React.useId();
78
+
79
+ return (
80
+ <FormItemContext.Provider value={{ id }}>
81
+ <div ref={ref} className={cn("space-y-2", className)} {...props} />
82
+ </FormItemContext.Provider>
83
+ );
84
+ },
85
+ );
86
+ FormItem.displayName = "FormItem";
87
+
88
+ const FormLabel = React.forwardRef<
89
+ React.ElementRef<typeof LabelPrimitive.Root>,
90
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
91
+ >(({ className, ...props }, ref) => {
92
+ const { error, formItemId } = useFormField();
93
+
94
+ return (
95
+ <Label
96
+ ref={ref}
97
+ className={cn(error && "text-destructive", className)}
98
+ htmlFor={formItemId}
99
+ {...props}
100
+ />
101
+ );
102
+ });
103
+ FormLabel.displayName = "FormLabel";
104
+
105
+ const FormControl = React.forwardRef<
106
+ React.ElementRef<typeof Slot>,
107
+ React.ComponentPropsWithoutRef<typeof Slot>
108
+ >(({ ...props }, ref) => {
109
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
110
+
111
+ return (
112
+ <Slot
113
+ ref={ref}
114
+ id={formItemId}
115
+ aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
116
+ aria-invalid={!!error}
117
+ {...props}
118
+ />
119
+ );
120
+ });
121
+ FormControl.displayName = "FormControl";
122
+
123
+ const FormDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
124
+ ({ className, ...props }, ref) => {
125
+ const { formDescriptionId } = useFormField();
126
+
127
+ return (
128
+ <p ref={ref} id={formDescriptionId} className={cn("text-[0.8rem] text-muted-foreground", className)} {...props} />
129
+ );
130
+ },
131
+ );
132
+ FormDescription.displayName = "FormDescription";
133
+
134
+ const FormMessage = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
135
+ ({ className, children, ...props }, ref) => {
136
+ const { error, formMessageId } = useFormField();
137
+ const body = error ? String(error?.message ?? "") : children;
138
+
139
+ if (!body) {
140
+ return null;
141
+ }
142
+
143
+ return (
144
+ <p
145
+ ref={ref}
146
+ id={formMessageId}
147
+ className={cn("text-[0.8rem] font-medium text-destructive", className)}
148
+ {...props}
149
+ >
150
+ {body}
151
+ </p>
152
+ );
153
+ },
154
+ );
155
+ FormMessage.displayName = "FormMessage";
156
+
157
+ export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField };
package/src/index.ts ADDED
@@ -0,0 +1,95 @@
1
+ export { Alert, AlertDescription, AlertTitle } from "./alert.js";
2
+ export { AppShell, type AppShellProps } from "./app-shell.js";
3
+ export { Badge, badgeVariants } from "./badge.js";
4
+ export { Button, buttonVariants, type ButtonProps } from "./button.js";
5
+ export { ConfirmDialog, type ConfirmDialogProps } from "./confirm-dialog.js";
6
+ export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./card.js";
7
+ export { Checkbox } from "./checkbox.js";
8
+ export {
9
+ DataTable,
10
+ createDataTableColumns,
11
+ type DataTableColumn,
12
+ type DataTableProps,
13
+ } from "./data-table.js";
14
+ export {
15
+ Dialog,
16
+ DialogClose,
17
+ DialogContent,
18
+ DialogDescription,
19
+ DialogFooter,
20
+ DialogHeader,
21
+ DialogOverlay,
22
+ DialogPortal,
23
+ DialogTitle,
24
+ DialogTrigger,
25
+ } from "./dialog.js";
26
+ export { DetailPanel, type DetailItem, type DetailPanelProps } from "./detail-panel.js";
27
+ export {
28
+ Field,
29
+ FieldContent,
30
+ FieldDescription,
31
+ FieldError,
32
+ FieldGroup,
33
+ FieldLabel,
34
+ FieldLegend,
35
+ FieldSeparator,
36
+ FieldSet,
37
+ FieldTitle,
38
+ } from "./field.js";
39
+ export {
40
+ Form,
41
+ FormControl,
42
+ FormDescription,
43
+ FormField,
44
+ FormItem,
45
+ FormLabel,
46
+ FormMessage,
47
+ useFormField,
48
+ } from "./form.js";
49
+ export { FormSubmit, type FormSubmitProps } from "./form-submit.js";
50
+ export { useValidatedForm } from "./use-validated-form.js";
51
+ export { Input } from "./input.js";
52
+ export { Label } from "./label.js";
53
+ export { Section, Stack, stackVariants, type SectionProps, type StackProps } from "./layout.js";
54
+ export { LivePulse, type LivePulseProps } from "./live-pulse.js";
55
+ export { OptionCards, type OptionCard, type OptionCardsProps } from "./option-cards.js";
56
+ export { PageHeader, type PageHeaderProps } from "./page-header.js";
57
+ export { SecretField, type SecretFieldProps } from "./secret-field.js";
58
+ export {
59
+ Select,
60
+ SelectContent,
61
+ SelectGroup,
62
+ SelectItem,
63
+ SelectLabel,
64
+ SelectScrollDownButton,
65
+ SelectScrollUpButton,
66
+ SelectSeparator,
67
+ SelectTrigger,
68
+ SelectValue,
69
+ } from "./select.js";
70
+ export { Separator } from "./separator.js";
71
+ export { Skeleton } from "./skeleton.js";
72
+ export {
73
+ Table,
74
+ TableBody,
75
+ TableCaption,
76
+ TableCell,
77
+ TableFooter,
78
+ TableHead,
79
+ TableHeader,
80
+ TableRow,
81
+ } from "./table.js";
82
+ export { WizardSteps, type WizardStepsProps } from "./wizard-steps.js";
83
+ export { Switch } from "./switch.js";
84
+ export { Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs.js";
85
+ export { Textarea } from "./textarea.js";
86
+ export { Toaster } from "./sonner.js";
87
+ export {
88
+ Heading,
89
+ headingVariants,
90
+ Text,
91
+ textVariants,
92
+ type HeadingProps,
93
+ type TextProps,
94
+ } from "./typography.js";
95
+ export { cn } from "./utils.js";
package/src/input.tsx ADDED
@@ -0,0 +1,21 @@
1
+ import * as React from "react";
2
+ import { cn } from "./utils.js";
3
+
4
+ const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
5
+ ({ className, type, ...props }, ref) => {
6
+ return (
7
+ <input
8
+ type={type}
9
+ className={cn(
10
+ "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
11
+ className,
12
+ )}
13
+ ref={ref}
14
+ {...props}
15
+ />
16
+ );
17
+ },
18
+ );
19
+ Input.displayName = "Input";
20
+
21
+ export { Input };
package/src/label.tsx ADDED
@@ -0,0 +1,20 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as LabelPrimitive from "@radix-ui/react-label";
5
+ import { cva, type VariantProps } from "class-variance-authority";
6
+ import { cn } from "./utils.js";
7
+
8
+ const labelVariants = cva(
9
+ "text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
10
+ );
11
+
12
+ const Label = React.forwardRef<
13
+ React.ElementRef<typeof LabelPrimitive.Root>,
14
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
15
+ >(({ className, ...props }, ref) => (
16
+ <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
17
+ ));
18
+ Label.displayName = LabelPrimitive.Root.displayName;
19
+
20
+ export { Label };
package/src/layout.tsx ADDED
@@ -0,0 +1,69 @@
1
+ import type * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "./utils.js";
5
+ import { Heading } from "./typography.js";
6
+ import { Text } from "./typography.js";
7
+
8
+ // No shadcn source exists for layout stacks/sections, so these two
9
+ // thin wrappers carry the documented spacing patterns. They exist so
10
+ // consumers can replace raw <section>/<div> stacks entirely.
11
+
12
+ const stackVariants = cva("flex", {
13
+ variants: {
14
+ direction: {
15
+ vertical: "flex-col",
16
+ horizontal: "flex-row items-center",
17
+ },
18
+ gap: {
19
+ 1: "gap-1",
20
+ 2: "gap-2",
21
+ 3: "gap-3",
22
+ 4: "gap-4",
23
+ 6: "gap-6",
24
+ 8: "gap-8",
25
+ },
26
+ },
27
+ defaultVariants: {
28
+ direction: "vertical",
29
+ gap: 4,
30
+ },
31
+ });
32
+
33
+ export type StackProps = React.HTMLAttributes<HTMLDivElement> &
34
+ VariantProps<typeof stackVariants> & {
35
+ direction?: "vertical" | "horizontal";
36
+ gap?: 1 | 2 | 3 | 4 | 6 | 8;
37
+ };
38
+
39
+ function Stack({ direction, gap, className, ...props }: StackProps) {
40
+ return <div className={cn(stackVariants({ direction, gap }), className)} {...props} />;
41
+ }
42
+
43
+ export type SectionProps = React.HTMLAttributes<HTMLElement> & {
44
+ /** Section title, rendered as a level-2 heading. */
45
+ title?: React.ReactNode;
46
+ /** Muted description under the title. */
47
+ description?: React.ReactNode;
48
+ /** Actions row, right-aligned next to the title. */
49
+ actions?: React.ReactNode;
50
+ };
51
+
52
+ function Section({ title, description, actions, children, className, ...props }: SectionProps) {
53
+ return (
54
+ <section className={cn("space-y-4", className)} {...props}>
55
+ {(title || actions) && (
56
+ <div className="flex items-start justify-between gap-4">
57
+ <div>
58
+ {title && <Heading level={2}>{title}</Heading>}
59
+ {description && <Text variant="muted">{description}</Text>}
60
+ </div>
61
+ {actions}
62
+ </div>
63
+ )}
64
+ {children}
65
+ </section>
66
+ );
67
+ }
68
+
69
+ export { Stack, stackVariants, Section };
@@ -0,0 +1,27 @@
1
+ import { cn } from "./utils.js";
2
+
3
+ export interface LivePulseProps {
4
+ active?: boolean;
5
+ label?: string;
6
+ className?: string;
7
+ }
8
+
9
+ export function LivePulse({ active = true, label = "Live", className }: LivePulseProps) {
10
+ return (
11
+ <span
12
+ role="status"
13
+ aria-label={active ? label : "Idle"}
14
+ className={cn("inline-flex items-center gap-1.5 text-xs text-muted-foreground", className)}
15
+ >
16
+ <span className="relative flex size-2">
17
+ {active ? (
18
+ <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-success opacity-60" />
19
+ ) : null}
20
+ <span
21
+ className={cn("relative inline-flex size-2 rounded-full", active ? "bg-success" : "bg-muted-foreground/40")}
22
+ />
23
+ </span>
24
+ {active ? label : "Idle"}
25
+ </span>
26
+ );
27
+ }
@@ -0,0 +1,47 @@
1
+ import type * as React from "react";
2
+
3
+ import { cn } from "./utils.js";
4
+
5
+ export interface OptionCard {
6
+ id: string;
7
+ title: React.ReactNode;
8
+ description?: React.ReactNode;
9
+ }
10
+
11
+ export interface OptionCardsProps {
12
+ options: OptionCard[];
13
+ selected: string[];
14
+ onChange: (ids: string[]) => void;
15
+ label?: string;
16
+ className?: string;
17
+ }
18
+
19
+ export function OptionCards({ options, selected, onChange, label = "Options", className }: OptionCardsProps) {
20
+ function toggle(id: string): void {
21
+ onChange(selected.includes(id) ? selected.filter((entry) => entry !== id) : [...selected, id]);
22
+ }
23
+
24
+ return (
25
+ <div role="group" aria-label={label} className={cn("grid gap-3 sm:grid-cols-2", className)}>
26
+ {options.map((option) => (
27
+ <label
28
+ key={option.id}
29
+ className="flex cursor-pointer gap-3 rounded-lg border p-4 transition-colors hover:border-muted-foreground/40 has-[:checked]:border-primary has-[:checked]:bg-primary/5 has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring"
30
+ >
31
+ <input
32
+ type="checkbox"
33
+ className="sr-only"
34
+ checked={selected.includes(option.id)}
35
+ onChange={() => toggle(option.id)}
36
+ />
37
+ <span>
38
+ <span className="block font-medium">{option.title}</span>
39
+ {option.description ? (
40
+ <span className="block text-sm text-muted-foreground">{option.description}</span>
41
+ ) : null}
42
+ </span>
43
+ </label>
44
+ ))}
45
+ </div>
46
+ );
47
+ }