feat(web): add run inputs options on dashboard

This commit is contained in:
BennyKok 2023-12-22 16:09:36 +08:00
parent cdbf4f3dd5
commit 592cc2abcd
29 changed files with 1321 additions and 102 deletions

Binary file not shown.

View File

@ -25,15 +25,21 @@
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-select": "^2.0.0", "@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-table": "^8.10.7", "@tanstack/react-table": "^8.10.7",
"@types/jsonwebtoken": "^9.0.5", "@types/jsonwebtoken": "^9.0.5",
"@types/uuid": "^9.0.7", "@types/uuid": "^9.0.7",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"date-fns": "^3.0.5",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"drizzle-orm": "^0.29.1", "drizzle-orm": "^0.29.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
@ -43,6 +49,7 @@
"next-plausible": "^3.12.0", "next-plausible": "^3.12.0",
"next-usequerystate": "^1.13.2", "next-usequerystate": "^1.13.2",
"react": "^18", "react": "^18",
"react-day-picker": "^8.9.1",
"react-dom": "^18", "react-dom": "^18",
"react-hook-form": "^7.48.2", "react-hook-form": "^7.48.2",
"react-use-websocket": "^4.5.0", "react-use-websocket": "^4.5.0",

View File

@ -1,5 +1,3 @@
import { OutputRender } from "./OutputRender";
import { CodeBlock } from "@/components/CodeBlock";
import { import {
Table, Table,
TableBody, TableBody,
@ -8,8 +6,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { findAllRuns } from "@/server/findAllRuns"; import type { findAllRuns } from "@/server/findAllRuns";
import { getRunsOutput } from "@/server/getRunsOutput";
export async function RunInputs({ export async function RunInputs({
run, run,
@ -30,16 +27,28 @@ export async function RunInputs({
{Object.entries(run.workflow_inputs).map(([key, data]) => { {Object.entries(run.workflow_inputs).map(([key, data]) => {
let imageUrl; let imageUrl;
try { try {
const url = new URL(data); if (data.startsWith("data:image/")) {
if (url.pathname.endsWith('.png')) {
imageUrl = data; imageUrl = data;
} else {
const url = new URL(data);
if (url.pathname.endsWith(".png")) {
imageUrl = data;
}
} }
} catch (_) { } catch (_) {}
}
return ( return (
<TableRow key={key}> <TableRow key={key}>
<TableCell>{key}</TableCell> <TableCell>{key}</TableCell>
{imageUrl ? <TableCell><img className="w-[200px] aspect-square object-contain" src={imageUrl}></img></TableCell> : <TableCell>{data}</TableCell>} {imageUrl ? (
<TableCell>
<img
className="w-[200px] aspect-square object-contain"
src={imageUrl}
/>
</TableCell>
) : (
<TableCell>{data}</TableCell>
)}
</TableRow> </TableRow>
); );
})} })}

View File

@ -2,7 +2,16 @@
import { callServerPromise } from "./callServerPromise"; import { callServerPromise } from "./callServerPromise";
import { LoadingIcon } from "@/components/LoadingIcon"; import { LoadingIcon } from "@/components/LoadingIcon";
import AutoForm, { AutoFormSubmit } from "@/components/ui/auto-form";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -18,14 +27,16 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { getInputsFromWorkflow } from "@/lib/getInputsFromWorkflow";
import { createRun } from "@/server/createRun"; import { createRun } from "@/server/createRun";
import { createDeployments } from "@/server/curdDeploments"; import { createDeployments } from "@/server/curdDeploments";
import type { getMachines } from "@/server/curdMachine"; import type { getMachines } from "@/server/curdMachine";
import type { findFirstTableWithVersion } from "@/server/findFirstTableWithVersion"; import type { findFirstTableWithVersion } from "@/server/findFirstTableWithVersion";
import { Copy, MoreVertical, Play } from "lucide-react"; import { Copy, MoreVertical, Play } from "lucide-react";
import { parseAsInteger, useQueryState } from "next-usequerystate"; import { parseAsInteger, useQueryState } from "next-usequerystate";
import { useState } from "react"; import { useMemo, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod";
export function VersionSelect({ export function VersionSelect({
workflow, workflow,
@ -106,31 +117,95 @@ export function RunWorkflowButton({
defaultValue: machines[0].id ?? "", defaultValue: machines[0].id ?? "",
}); });
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
return (
<Button
className="gap-2"
disabled={isLoading}
onClick={async () => {
const workflow_version_id = workflow?.versions.find(
(x) => x.version === version
)?.id;
if (!workflow_version_id) return;
setIsLoading(true); const [values, setValues] = useState<Record<string, string>>({});
try { const [open, setOpen] = useState(false);
const origin = window.location.origin;
await callServerPromise( const schema = useMemo(() => {
createRun(origin, workflow_version_id, machine, undefined, true) const workflow_version = getWorkflowVersionFromVersionIndex(
); workflow,
// console.log(res.json()); version
setIsLoading(false); );
} catch (error) { const inputs = getInputsFromWorkflow(workflow_version);
setIsLoading(false);
} if (!inputs) return null;
}}
> return z.object({
Run {isLoading ? <LoadingIcon /> : <Play size={14} />} ...Object.fromEntries(
</Button> inputs?.map((x) => {
return [x?.input_id, z.string().optional()];
})
),
});
}, [version]);
const runWorkflow = async () => {
console.log(values);
const val = Object.keys(values).length > 0 ? values : undefined;
const workflow_version_id = workflow?.versions.find(
(x) => x.version === version
)?.id;
console.log(workflow_version_id);
if (!workflow_version_id) return;
setIsLoading(true);
try {
const origin = window.location.origin;
await callServerPromise(
createRun(origin, workflow_version_id, machine, val, true)
);
// console.log(res.json());
setIsLoading(false);
} catch (error) {
setIsLoading(false);
}
setOpen(false);
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild className="appearance-none hover:cursor-pointer">
<Button className="gap-2" disabled={isLoading}>
Run {isLoading ? <LoadingIcon /> : <Play size={14} />}
</Button>
</DialogTrigger>
<DialogContent className="max-w-xl">
<DialogHeader>
<DialogTitle>Run outputs</DialogTitle>
<DialogDescription>
You can view your run&apos;s outputs here
</DialogDescription>
</DialogHeader>
{/* <div className="max-h-96 overflow-y-scroll"> */}
{schema && (
<AutoForm
formSchema={schema}
values={values}
onValuesChange={setValues}
onSubmit={runWorkflow}
>
<div className="flex justify-end">
<AutoFormSubmit>
Run
<span className="ml-2">
{isLoading ? <LoadingIcon /> : <Play size={14} />}
</span>
</AutoFormSubmit>
</div>
</AutoForm>
)}
{!schema && (
<Button className="gap-2" disabled={isLoading} onClick={runWorkflow}>
Confirm {isLoading ? <LoadingIcon /> : <Play size={14} />}
</Button>
)}
{/* </div> */}
{/* <div className="max-h-96 overflow-y-scroll">{view}</div> */}
</DialogContent>
</Dialog>
); );
} }

View File

@ -1,4 +1,5 @@
export const customInputNodes: Record<string, string> = { export const customInputNodes: Record<string, string> = {
ComfyUIDeployExternalText: "string", ComfyUIDeployExternalText: "string",
ComfyUIDeployExternalImage: "string - (public image url)", ComfyUIDeployExternalImage: "string - (public image url)",
ComfyUIDeployExternalImageAlpha: "string - (public image url)",
}; };

View File

@ -1,12 +1,11 @@
"use client" "use client";
import * as React from "react" import { cn } from "@/lib/utils";
import * as AccordionPrimitive from "@radix-ui/react-accordion" import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDown } from "lucide-react" import { ChevronDown } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils" const Accordion = AccordionPrimitive.Root;
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef< const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>, React.ElementRef<typeof AccordionPrimitive.Item>,
@ -17,8 +16,8 @@ const AccordionItem = React.forwardRef<
className={cn("border-b", className)} className={cn("border-b", className)}
{...props} {...props}
/> />
)) ));
AccordionItem.displayName = "AccordionItem" AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef< const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>, React.ElementRef<typeof AccordionPrimitive.Trigger>,
@ -37,8 +36,8 @@ const AccordionTrigger = React.forwardRef<
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" /> <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger> </AccordionPrimitive.Trigger>
</AccordionPrimitive.Header> </AccordionPrimitive.Header>
)) ));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef< const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>, React.ElementRef<typeof AccordionPrimitive.Content>,
@ -51,8 +50,8 @@ const AccordionContent = React.forwardRef<
> >
<div className={cn("pb-4 pt-0", className)}>{children}</div> <div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content> </AccordionPrimitive.Content>
)) ));
AccordionContent.displayName = AccordionPrimitive.Content.displayName AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

View File

@ -0,0 +1,33 @@
import AutoFormCheckbox from "./fields/checkbox";
import AutoFormDate from "./fields/date";
import AutoFormEnum from "./fields/enum";
import AutoFormInput from "./fields/input";
import AutoFormNumber from "./fields/number";
import AutoFormRadioGroup from "./fields/radio-group";
import AutoFormSwitch from "./fields/switch";
import AutoFormTextarea from "./fields/textarea";
export const INPUT_COMPONENTS = {
checkbox: AutoFormCheckbox,
date: AutoFormDate,
select: AutoFormEnum,
radio: AutoFormRadioGroup,
switch: AutoFormSwitch,
textarea: AutoFormTextarea,
number: AutoFormNumber,
fallback: AutoFormInput,
};
/**
* Define handlers for specific Zod types.
* You can expand this object to support more types.
*/
export const DEFAULT_ZOD_HANDLERS: {
[key: string]: keyof typeof INPUT_COMPONENTS;
} = {
ZodBoolean: "checkbox",
ZodDate: "date",
ZodEnum: "select",
ZodNativeEnum: "select",
ZodNumber: "number",
};

View File

@ -0,0 +1,67 @@
import type { useForm , useForm } from "react-hook-form";
import { useFieldArray } from "react-hook-form";
import { Button } from "../../button";
import { Separator } from "../../separator";
import { beautifyObjectName } from "../utils";
import AutoFormObject from "./object";
import { Plus, Trash } from "lucide-react";
import { AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import type { z } from "zod";
export default function AutoFormArray({
name,
item,
form,
path = [],
fieldConfig,
}: {
name: string;
item: z.ZodArray<any>;
form: ReturnType<typeof useForm>;
path?: string[];
fieldConfig?: any;
}) {
const { fields, append, remove } = useFieldArray({
control: form.control,
name,
});
const title = item._def.description ?? beautifyObjectName(name);
return (
<AccordionItem value={name}>
<AccordionTrigger>{title}</AccordionTrigger>
<AccordionContent className="border-l p-3 pl-6">
{fields.map((_field, index) => {
const key = [...path, index.toString()].join(".");
return (
<div className="mb-4 grid gap-6" key={`${key}`}>
<AutoFormObject
schema={item._def.type as z.ZodObject<any, any>}
form={form}
fieldConfig={fieldConfig}
path={[...path, index.toString()]}
/>
<Button
variant="secondary"
size="icon"
type="button"
onClick={() => remove(index)}
>
<Trash className="h-4 w-4" />
</Button>
<Separator />
</div>
);
})}
<Button
type="button"
onClick={() => append({})}
className="flex items-center"
>
<Plus className="mr-2" size={16} />
Add
</Button>
</AccordionContent>
</AccordionItem>
);
}

View File

@ -0,0 +1,32 @@
import { Checkbox } from "../../checkbox";
import { FormControl, FormDescription, FormItem, FormLabel } from "../../form";
import { AutoFormInputComponentProps } from "../types";
export default function AutoFormCheckbox({
label,
isRequired,
field,
fieldConfigItem,
fieldProps,
}: AutoFormInputComponentProps) {
return (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
{...fieldProps}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>
{label}
{isRequired && <span className="text-destructive"> *</span>}
</FormLabel>
{fieldConfigItem.description && (
<FormDescription>{fieldConfigItem.description}</FormDescription>
)}
</div>
</FormItem>
);
}

View File

@ -0,0 +1,37 @@
import { DatePicker } from "../../date-picker";
import {
FormControl,
FormDescription,
FormItem,
FormLabel,
FormMessage,
} from "../../form";
import type { AutoFormInputComponentProps } from "../types";
export default function AutoFormDate({
label,
isRequired,
field,
fieldConfigItem,
fieldProps,
}: AutoFormInputComponentProps) {
return (
<FormItem>
<FormLabel>
{label}
{isRequired && <span className="text-destructive"> *</span>}
</FormLabel>
<FormControl>
<DatePicker
date={field.value}
setDate={field.onChange}
{...fieldProps}
/>
</FormControl>
{fieldConfigItem.description && (
<FormDescription>{fieldConfigItem.description}</FormDescription>
)}
<FormMessage />
</FormItem>
);
}

View File

@ -0,0 +1,71 @@
import {
FormControl,
FormDescription,
FormItem,
FormLabel,
FormMessage,
} from "../../form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../select";
import { AutoFormInputComponentProps } from "../types";
import { getBaseSchema } from "../utils";
import * as z from "zod";
export default function AutoFormEnum({
label,
isRequired,
field,
fieldConfigItem,
zodItem,
}: AutoFormInputComponentProps) {
const baseValues = (getBaseSchema(zodItem) as unknown as z.ZodEnum<any>)._def
.values;
let values: [string, string][] = [];
if (!Array.isArray(baseValues)) {
values = Object.entries(baseValues);
} else {
values = baseValues.map((value) => [value, value]);
}
function findItem(value: any) {
return values.find((item) => item[0] === value);
}
return (
<FormItem>
<FormLabel>
{label}
{isRequired && <span className="text-destructive"> *</span>}
</FormLabel>
<FormControl>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<SelectTrigger>
<SelectValue
className="w-full"
placeholder={fieldConfigItem.inputProps?.placeholder}
>
{field.value ? findItem(field.value)?.[1] : "Select an option"}
</SelectValue>
</SelectTrigger>
<SelectContent>
{values.map(([value, label]) => (
<SelectItem value={label} key={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
{fieldConfigItem.description && (
<FormDescription>{fieldConfigItem.description}</FormDescription>
)}
<FormMessage />
</FormItem>
);
}

View File

@ -0,0 +1,36 @@
import {
FormControl,
FormDescription,
FormItem,
FormLabel,
FormMessage,
} from "../../form";
import { Input } from "../../input";
import { AutoFormInputComponentProps } from "../types";
export default function AutoFormInput({
label,
isRequired,
fieldConfigItem,
fieldProps,
}: AutoFormInputComponentProps) {
const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = fieldProps;
const showLabel = _showLabel === undefined ? true : _showLabel;
return (
<FormItem>
{showLabel && (
<FormLabel>
{label}
{isRequired && <span className="text-destructive"> *</span>}
</FormLabel>
)}
<FormControl>
<Input type="text" {...fieldPropsWithoutShowLabel} />
</FormControl>
{fieldConfigItem.description && (
<FormDescription>{fieldConfigItem.description}</FormDescription>
)}
<FormMessage />
</FormItem>
);
}

View File

@ -0,0 +1,17 @@
import { AutoFormInputComponentProps } from "../types";
import AutoFormInput from "./input";
export default function AutoFormNumber({
fieldProps,
...props
}: AutoFormInputComponentProps) {
return (
<AutoFormInput
fieldProps={{
type: "number",
...fieldProps,
}}
{...props}
/>
);
}

View File

@ -0,0 +1,130 @@
import * as z from "zod";
import { useForm } from "react-hook-form";
import { FieldConfig, FieldConfigItem } from "../types";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "../../accordion";
import {
beautifyObjectName,
getBaseSchema,
getBaseType,
zodToHtmlInputProps,
} from "../utils";
import { FormField } from "../../form";
import { DEFAULT_ZOD_HANDLERS, INPUT_COMPONENTS } from "../config";
import AutoFormArray from "./array";
function DefaultParent({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
export default function AutoFormObject<
SchemaType extends z.ZodObject<any, any>,
>({
schema,
form,
fieldConfig,
path = [],
}: {
schema: SchemaType | z.ZodEffects<SchemaType>;
form: ReturnType<typeof useForm>;
fieldConfig?: FieldConfig<z.infer<SchemaType>>;
path?: string[];
}) {
const { shape } = getBaseSchema<SchemaType>(schema);
return (
<Accordion type="multiple" className="space-y-5">
{Object.keys(shape).map((name) => {
const item = shape[name] as z.ZodAny;
const zodBaseType = getBaseType(item);
const itemName = item._def.description ?? beautifyObjectName(name);
const key = [...path, name].join(".");
if (zodBaseType === "ZodObject") {
return (
<AccordionItem value={name} key={key}>
<AccordionTrigger>{itemName}</AccordionTrigger>
<AccordionContent className="p-2">
<AutoFormObject
schema={item as unknown as z.ZodObject<any, any>}
form={form}
fieldConfig={
(fieldConfig?.[name] ?? {}) as FieldConfig<
z.infer<typeof item>
>
}
path={[...path, name]}
/>
</AccordionContent>
</AccordionItem>
);
}
if (zodBaseType === "ZodArray") {
return (
<AutoFormArray
key={key}
name={name}
item={item as unknown as z.ZodArray<any>}
form={form}
fieldConfig={fieldConfig?.[name] ?? {}}
path={[...path, name]}
/>
);
}
const fieldConfigItem: FieldConfigItem = fieldConfig?.[name] ?? {};
const zodInputProps = zodToHtmlInputProps(item);
const isRequired =
zodInputProps.required ||
fieldConfigItem.inputProps?.required ||
false;
return (
<FormField
control={form.control}
name={key}
key={key}
render={({ field }) => {
const inputType =
fieldConfigItem.fieldType ??
DEFAULT_ZOD_HANDLERS[zodBaseType] ??
"fallback";
const InputComponent =
typeof inputType === "function"
? inputType
: INPUT_COMPONENTS[inputType];
const ParentElement =
fieldConfigItem.renderParent ?? DefaultParent;
return (
<ParentElement key={`${key}.parent`}>
<InputComponent
zodInputProps={zodInputProps}
field={field}
fieldConfigItem={fieldConfigItem}
label={itemName}
isRequired={isRequired}
zodItem={item}
fieldProps={{
...zodInputProps,
...field,
...fieldConfigItem.inputProps,
value: !fieldConfigItem.inputProps?.defaultValue
? field.value ?? ""
: undefined,
}}
/>
</ParentElement>
);
}}
/>
);
})}
</Accordion>
);
}

View File

@ -0,0 +1,44 @@
import * as z from "zod";
import { AutoFormInputComponentProps } from "../types";
import { FormControl, FormItem, FormLabel, FormMessage } from "../../form";
import { RadioGroup, RadioGroupItem } from "../../radio-group";
export default function AutoFormRadioGroup({
label,
isRequired,
field,
zodItem,
fieldProps,
}: AutoFormInputComponentProps) {
const values = (zodItem as unknown as z.ZodEnum<any>)._def.values;
return (
<FormItem className="space-y-3">
<FormLabel>
{label}
{isRequired && <span className="text-destructive"> *</span>}
</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="flex flex-col space-y-1"
{...fieldProps}
>
{values.map((value: any) => (
<FormItem
className="flex items-center space-x-3 space-y-0"
key={value}
>
<FormControl>
<RadioGroupItem value={value} />
</FormControl>
<FormLabel className="font-normal">{value}</FormLabel>
</FormItem>
))}
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
);
}

View File

@ -0,0 +1,32 @@
import { FormControl, FormDescription, FormItem, FormLabel } from "../../form";
import { Switch } from "../../switch";
import { AutoFormInputComponentProps } from "../types";
export default function AutoFormSwitch({
label,
isRequired,
field,
fieldConfigItem,
fieldProps,
}: AutoFormInputComponentProps) {
return (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
{...fieldProps}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>
{label}
{isRequired && <span className="text-destructive"> *</span>}
</FormLabel>
{fieldConfigItem.description && (
<FormDescription>{fieldConfigItem.description}</FormDescription>
)}
</div>
</FormItem>
);
}

View File

@ -0,0 +1,36 @@
import {
FormControl,
FormDescription,
FormItem,
FormLabel,
FormMessage,
} from "../../form";
import { Textarea } from "../../textarea";
import { AutoFormInputComponentProps } from "../types";
export default function AutoFormTextarea({
label,
isRequired,
fieldConfigItem,
fieldProps,
}: AutoFormInputComponentProps) {
const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = fieldProps;
const showLabel = _showLabel === undefined ? true : _showLabel;
return (
<FormItem>
{showLabel && (
<FormLabel>
{label}
{isRequired && <span className="text-destructive"> *</span>}
</FormLabel>
)}
<FormControl>
<Textarea {...fieldPropsWithoutShowLabel} />
</FormControl>
{fieldConfigItem.description && (
<FormDescription>{fieldConfigItem.description}</FormDescription>
)}
<FormMessage />
</FormItem>
);
}

View File

@ -0,0 +1,87 @@
"use client";
import React from "react";
import { z } from "zod";
import { Form } from "../form";
import { DefaultValues, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "../button";
import { cn } from "@/lib/utils";
import { FieldConfig } from "./types";
import {
ZodObjectOrWrapped,
getDefaultValues,
getObjectFormSchema,
} from "./utils";
import AutoFormObject from "./fields/object";
export function AutoFormSubmit({ children }: { children?: React.ReactNode }) {
return <Button type="submit">{children ?? "Submit"}</Button>;
}
function AutoForm<SchemaType extends ZodObjectOrWrapped>({
formSchema,
values: valuesProp,
onValuesChange: onValuesChangeProp,
onParsedValuesChange,
onSubmit: onSubmitProp,
fieldConfig,
children,
className,
}: {
formSchema: SchemaType;
values?: Partial<z.infer<SchemaType>>;
onValuesChange?: (values: Partial<z.infer<SchemaType>>) => void;
onParsedValuesChange?: (values: Partial<z.infer<SchemaType>>) => void;
onSubmit?: (values: z.infer<SchemaType>) => void;
fieldConfig?: FieldConfig<z.infer<SchemaType>>;
children?: React.ReactNode;
className?: string;
}) {
const objectFormSchema = getObjectFormSchema(formSchema);
const defaultValues: DefaultValues<z.infer<typeof objectFormSchema>> =
getDefaultValues(objectFormSchema);
const form = useForm<z.infer<typeof objectFormSchema>>({
resolver: zodResolver(formSchema),
defaultValues,
values: valuesProp,
});
function onSubmit(values: z.infer<typeof formSchema>) {
const parsedValues = formSchema.safeParse(values);
if (parsedValues.success) {
onSubmitProp?.(parsedValues.data);
}
}
return (
<Form {...form}>
<form
onSubmit={(e) => {
form.handleSubmit(onSubmit)(e);
}}
onChange={() => {
const values = form.getValues();
onValuesChangeProp?.(values);
const parsedValues = formSchema.safeParse(values);
if (parsedValues.success) {
onParsedValuesChange?.(parsedValues.data);
}
}}
className={cn("space-y-5", className)}
>
<AutoFormObject
schema={objectFormSchema}
form={form}
fieldConfig={fieldConfig}
/>
{children}
</form>
</Form>
);
}
export default AutoForm;

View File

@ -0,0 +1,37 @@
import { ControllerRenderProps, FieldValues } from "react-hook-form";
import * as z from "zod";
import { INPUT_COMPONENTS } from "./config";
export type FieldConfigItem = {
description?: React.ReactNode;
inputProps?: React.InputHTMLAttributes<HTMLInputElement> & {
showLabel?: boolean;
};
fieldType?:
| keyof typeof INPUT_COMPONENTS
| React.FC<AutoFormInputComponentProps>;
renderParent?: (props: {
children: React.ReactNode;
}) => React.ReactElement | null;
};
export type FieldConfig<SchemaType extends z.infer<z.ZodObject<any, any>>> = {
// If SchemaType.key is an object, create a nested FieldConfig, otherwise FieldConfigItem
[Key in keyof SchemaType]?: SchemaType[Key] extends object
? FieldConfig<z.infer<SchemaType[Key]>>
: FieldConfigItem;
};
/**
* A FormInput component can handle a specific Zod type (e.g. "ZodBoolean")
*/
export type AutoFormInputComponentProps = {
zodInputProps: React.InputHTMLAttributes<HTMLInputElement>;
field: ControllerRenderProps<FieldValues, any>;
fieldConfigItem: FieldConfigItem;
label: string;
isRequired: boolean;
fieldProps: any;
zodItem: z.ZodAny;
};

View File

@ -0,0 +1,159 @@
import { DefaultValues } from "react-hook-form";
import { z } from "zod";
// TODO: This should support recursive ZodEffects but TypeScript doesn't allow circular type definitions.
export type ZodObjectOrWrapped =
| z.ZodObject<any, any>
| z.ZodEffects<z.ZodObject<any, any>>;
/**
* Beautify a camelCase string.
* e.g. "myString" -> "My String"
*/
export function beautifyObjectName(string: string) {
let output = string.replace(/([A-Z])/g, " $1");
output = output.charAt(0).toUpperCase() + output.slice(1);
return output;
}
/**
* Get the lowest level Zod type.
* This will unpack optionals, refinements, etc.
*/
export function getBaseSchema<
ChildType extends z.ZodAny | z.AnyZodObject = z.ZodAny,
>(schema: ChildType | z.ZodEffects<ChildType>): ChildType {
if ("innerType" in schema._def) {
return getBaseSchema(schema._def.innerType as ChildType);
}
if ("schema" in schema._def) {
return getBaseSchema(schema._def.schema as ChildType);
}
return schema as ChildType;
}
/**
* Get the type name of the lowest level Zod type.
* This will unpack optionals, refinements, etc.
*/
export function getBaseType(schema: z.ZodAny): string {
return getBaseSchema(schema)._def.typeName;
}
/**
* Search for a "ZodDefult" in the Zod stack and return its value.
*/
export function getDefaultValueInZodStack(schema: z.ZodAny): any {
const typedSchema = schema as unknown as z.ZodDefault<
z.ZodNumber | z.ZodString
>;
if (typedSchema._def.typeName === "ZodDefault") {
return typedSchema._def.defaultValue();
}
if ("innerType" in typedSchema._def) {
return getDefaultValueInZodStack(
typedSchema._def.innerType as unknown as z.ZodAny,
);
}
if ("schema" in typedSchema._def) {
return getDefaultValueInZodStack(
(typedSchema._def as any).schema as z.ZodAny,
);
}
return undefined;
}
/**
* Get all default values from a Zod schema.
*/
export function getDefaultValues<Schema extends z.ZodObject<any, any>>(
schema: Schema,
) {
const { shape } = schema;
type DefaultValuesType = DefaultValues<Partial<z.infer<Schema>>>;
const defaultValues = {} as DefaultValuesType;
for (const key of Object.keys(shape)) {
const item = shape[key] as z.ZodAny;
if (getBaseType(item) === "ZodObject") {
const defaultItems = getDefaultValues(
getBaseSchema(item) as unknown as z.ZodObject<any, any>,
);
for (const defaultItemKey of Object.keys(defaultItems)) {
const pathKey = `${key}.${defaultItemKey}` as keyof DefaultValuesType;
defaultValues[pathKey] = defaultItems[defaultItemKey];
}
} else {
const defaultValue = getDefaultValueInZodStack(item);
if (defaultValue !== undefined) {
defaultValues[key as keyof DefaultValuesType] = defaultValue;
}
}
}
return defaultValues;
}
export function getObjectFormSchema(
schema: ZodObjectOrWrapped,
): z.ZodObject<any, any> {
if (schema._def.typeName === "ZodEffects") {
const typedSchema = schema as z.ZodEffects<z.ZodObject<any, any>>;
return getObjectFormSchema(typedSchema._def.schema);
}
return schema as z.ZodObject<any, any>;
}
/**
* Convert a Zod schema to HTML input props to give direct feedback to the user.
* Once submitted, the schema will be validated completely.
*/
export function zodToHtmlInputProps(
schema:
| z.ZodNumber
| z.ZodString
| z.ZodOptional<z.ZodNumber | z.ZodString>
| any,
): React.InputHTMLAttributes<HTMLInputElement> {
if (["ZodOptional", "ZodNullable"].includes(schema._def.typeName)) {
const typedSchema = schema as z.ZodOptional<z.ZodNumber | z.ZodString>;
return {
...zodToHtmlInputProps(typedSchema._def.innerType),
required: false,
};
}
const typedSchema = schema as z.ZodNumber | z.ZodString;
if (!("checks" in typedSchema._def)) return {
required: true
};
const { checks } = typedSchema._def;
const inputProps: React.InputHTMLAttributes<HTMLInputElement> = {
required: true,
};
const type = getBaseType(schema);
for (const check of checks) {
if (check.kind === "min") {
if (type === "ZodString") {
inputProps.minLength = check.value;
} else {
inputProps.min = check.value;
}
}
if (check.kind === "max") {
if (type === "ZodString") {
inputProps.maxLength = check.value;
} else {
inputProps.max = check.value;
}
}
}
return inputProps;
}

View File

@ -0,0 +1,66 @@
"use client"
import * as React from "react"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { DayPicker } from "react-day-picker"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
export type CalendarProps = React.ComponentProps<typeof DayPicker>
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(
buttonVariants({ variant: "ghost" }),
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
),
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
}}
{...props}
/>
)
}
Calendar.displayName = "Calendar"
export { Calendar }

View File

@ -0,0 +1,46 @@
"use client";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { format } from "date-fns";
import { Calendar as CalendarIcon } from "lucide-react";
import { forwardRef } from "react";
export const DatePicker = forwardRef<
HTMLDivElement,
{
date?: Date;
setDate: (date?: Date) => void;
}
>(function DatePickerCmp({ date, setDate }, ref) {
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
"w-full justify-start text-left font-normal",
!date && "text-muted-foreground"
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? format(date, "PPP") : <span>Pick a date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" ref={ref}>
<Calendar
mode="single"
selected={date}
onSelect={setDate}
initialFocus
/>
</PopoverContent>
</Popover>
);
});

View File

@ -1,30 +1,23 @@
import * as React from "react" import { Label } from "@/components/ui/label";
import * as LabelPrimitive from "@radix-ui/react-label" import { cn } from "@/lib/utils";
import { Slot } from "@radix-ui/react-slot" import type * as LabelPrimitive from "@radix-ui/react-label";
import { import { Slot } from "@radix-ui/react-slot";
Controller, import * as React from "react";
ControllerProps, import type { ControllerProps, FieldPath, FieldValues } from "react-hook-form";
FieldPath, import { Controller, FormProvider, useFormContext } from "react-hook-form";
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"
import { cn } from "@/lib/utils" const Form = FormProvider;
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue< type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = { > = {
name: TName name: TName;
} };
const FormFieldContext = React.createContext<FormFieldContextValue>( const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue {} as FormFieldContextValue
) );
const FormField = < const FormField = <
TFieldValues extends FieldValues = FieldValues, TFieldValues extends FieldValues = FieldValues,
@ -36,21 +29,21 @@ const FormField = <
<FormFieldContext.Provider value={{ name: props.name }}> <FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} /> <Controller {...props} />
</FormFieldContext.Provider> </FormFieldContext.Provider>
) );
} };
const useFormField = () => { const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext) const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext) const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext() const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState) const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) { if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>") throw new Error("useFormField should be used within <FormField>");
} }
const { id } = itemContext const { id } = itemContext;
return { return {
id, id,
@ -59,36 +52,36 @@ const useFormField = () => {
formDescriptionId: `${id}-form-item-description`, formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`, formMessageId: `${id}-form-item-message`,
...fieldState, ...fieldState,
} };
} };
type FormItemContextValue = { type FormItemContextValue = {
id: string id: string;
} };
const FormItemContext = React.createContext<FormItemContextValue>( const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue {} as FormItemContextValue
) );
const FormItem = React.forwardRef< const FormItem = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const id = React.useId() const id = React.useId();
return ( return (
<FormItemContext.Provider value={{ id }}> <FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} /> <div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider> </FormItemContext.Provider>
) );
}) });
FormItem.displayName = "FormItem" FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef< const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField() const { error, formItemId } = useFormField();
return ( return (
<Label <Label
@ -97,15 +90,16 @@ const FormLabel = React.forwardRef<
htmlFor={formItemId} htmlFor={formItemId}
{...props} {...props}
/> />
) );
}) });
FormLabel.displayName = "FormLabel" FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef< const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>, React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot> React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => { >(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField() const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return ( return (
<Slot <Slot
@ -119,15 +113,15 @@ const FormControl = React.forwardRef<
aria-invalid={!!error} aria-invalid={!!error}
{...props} {...props}
/> />
) );
}) });
FormControl.displayName = "FormControl" FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef< const FormDescription = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => { >(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField() const { formDescriptionId } = useFormField();
return ( return (
<p <p
@ -136,19 +130,19 @@ const FormDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)} className={cn("text-sm text-muted-foreground", className)}
{...props} {...props}
/> />
) );
}) });
FormDescription.displayName = "FormDescription" FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef< const FormMessage = React.forwardRef<
HTMLParagraphElement, HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement> React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => { >(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField() const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children const body = error ? String(error?.message) : children;
if (!body) { if (!body) {
return null return null;
} }
return ( return (
@ -160,9 +154,9 @@ const FormMessage = React.forwardRef<
> >
{body} {body}
</p> </p>
) );
}) });
FormMessage.displayName = "FormMessage" FormMessage.displayName = "FormMessage";
export { export {
useFormField, useFormField,
@ -173,4 +167,4 @@ export {
FormDescription, FormDescription,
FormMessage, FormMessage,
FormField, FormField,
} };

View File

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }

View File

@ -0,0 +1,44 @@
"use client"
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }

View File

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

View File

@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

View File

@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }

View File

@ -0,0 +1,45 @@
"use client"
import * as React from "react"
import * as TogglePrimitive from "@radix-ui/react-toggle"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-3",
sm: "h-9 px-2.5",
lg: "h-11 px-5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root
ref={ref}
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
))
Toggle.displayName = TogglePrimitive.Root.displayName
export { Toggle, toggleVariants }