diff --git a/apps/sandbox/app/page.tsx b/apps/sandbox/app/page.tsx index 6d64475..63aa77c 100644 --- a/apps/sandbox/app/page.tsx +++ b/apps/sandbox/app/page.tsx @@ -1,5 +1,10 @@ "use client"; -import { BlockWrapper, type FieldProps, quackFields } from "@duck-form/fields"; +import { + BlockWrapper, + type FieldProps, + BlockType, + quackFields, +} from "@duck-form/fields"; import { DevTool } from "@hookform/devtools"; import { Button } from "@rafty/ui"; import { Blueprint, DuckField, DuckForm } from "duck-form"; @@ -7,24 +12,24 @@ import { FormProvider, useForm } from "react-hook-form"; const schema: Record = { array: { - type: "array", + type: BlockType.ARRAY, label: "Array", of: { - type: "string", + type: BlockType.STRING, label: "String", }, }, calendar: { - type: "calendar", + type: BlockType.CALENDAR, label: "Calendar", }, checkbox: { - type: "boolean", + type: BlockType.BOOLEAN, label: "Checkbox", orientation: "row-reverse", }, checkbox_group: { - type: "checkboxgroup", + type: BlockType.CHECKBOX_GROUP, label: "Checkbox Group", options: [ { value: "1", label: "1" }, @@ -36,35 +41,35 @@ const schema: Record = { ], }, color_picker: { - type: "colorPicker", + type: BlockType.COLOR_PICKER, label: "Color Picker", }, currency: { - type: "currencyInput", + type: BlockType.CURRENCY_INPUT, label: "Currency", }, date: { - type: "date", + type: BlockType.DATE, label: "Date", }, date_range: { - type: "dateRange", + type: BlockType.DATE_RANGE, label: "Date Range", }, editable_number: { - type: "editableNumber", + type: BlockType.EDITABLE_NUMBER, label: "Editable Number", }, editable_text: { - type: "editableText", + type: BlockType.EDITABLE_TEXT, label: "Editable Text", }, editable_textarea: { - type: "editableTextarea", + type: BlockType.EDITABLE_TEXTAREA, label: "Editable Textarea", }, list_box: { - type: "listbox", + type: BlockType.LISTBOX, label: "List Box", options: [ { value: "1", label: "1" }, @@ -76,7 +81,7 @@ const schema: Record = { ], }, multi_list_box: { - type: "multiListbox", + type: BlockType.MULTI_LISTBOX, label: "Multi Listbox", options: [ { value: "1", label: "1" }, @@ -88,41 +93,41 @@ const schema: Record = { ], }, number: { - type: "number", + type: BlockType.NUMBER, label: "Number", }, object: { - type: "object", + type: BlockType.OBJECT, fields: { string: { - type: "string", + type: BlockType.STRING, label: "String", }, string1: { - type: "string", + type: BlockType.STRING, label: "String", }, string2: { - type: "string", + type: BlockType.STRING, label: "String", }, }, }, password: { - type: "password", + type: BlockType.PASSWORD, label: "Password", }, percentage: { - type: "percentageInput", + type: BlockType.PERCENTAGE_INPUT, label: "Percentage", }, pin: { - type: "pin", + type: BlockType.PIN, label: "Pin", length: 4, }, radio_group: { - type: "radio", + type: BlockType.RADIO, label: "Radio", options: [ { value: 1, label: "1" }, @@ -130,17 +135,17 @@ const schema: Record = { ], }, range_slider: { - type: "rangeSlider", + type: BlockType.RANGE_SLIDER, label: "Range Slider", }, rating: { - type: "rating", + type: BlockType.RATING, label: "Rating", count: 5, allowHalf: true, }, segmented_control: { - type: "segmentedControl", + type: BlockType.SEGMENTED_CONTROL, label: "Segmented Control", options: [ { value: "1", label: "1" }, @@ -152,7 +157,7 @@ const schema: Record = { ], }, select: { - type: "select", + type: BlockType.SELECT, label: "Select", options: [ { value: "1", label: "1" }, @@ -164,20 +169,20 @@ const schema: Record = { ], }, slider: { - type: "slider", + type: BlockType.SLIDER, label: "Slider", }, string: { - type: "string", + type: BlockType.STRING, label: "String", }, switch: { - type: "switch", + type: BlockType.SWTICH, label: "Switch", orientation: "row-reverse", }, switch_group: { - type: "switchGroup", + type: BlockType.SWITCH_GROUP, label: "Switch Group", options: [ { value: 1, label: "1" }, @@ -185,11 +190,11 @@ const schema: Record = { ], }, tag: { - type: "tag", + type: BlockType.TAG, label: "Tag", }, textarea: { - type: "textarea", + type: BlockType.TEXTAREA, label: "Textarea", }, }; @@ -205,15 +210,12 @@ export default function HomePage() { return (
- (props.id ? String(props.id) : undefined)} - > +
console.log(value), - console.error, + console.error )} className="space-y-3" > diff --git a/package.json b/package.json index db7c393..c977563 100644 --- a/package.json +++ b/package.json @@ -18,13 +18,15 @@ "@rollup/plugin-terser": "^0.4.4", "astro": "^4.15.4", "dayjs": "^1.11.13", + "lodash": "^4.17.21", "nanoid": "^5.0.7", "next": "14.2.3", "react": "18.3.1", "react-dom": "18.3.1", "react-hook-form": "^7.53.0", "sharp": "^0.33.5", - "tslib": "^2.3.0" + "tslib": "^2.3.0", + "zod": "^3.24.1" }, "devDependencies": { "@biomejs/biome": "^1.8.3", @@ -40,6 +42,7 @@ "@swc/cli": "~0.3.12", "@swc/core": "~1.5.7", "@swc/helpers": "~0.5.11", + "@types/lodash": "^4.17.14", "@types/node": "18.16.9", "@types/react": "18.3.1", "@types/react-dom": "18.3.0", diff --git a/packages/core/package.json b/packages/core/package.json index 88efd9f..c9daeb6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -23,6 +23,7 @@ "url": "https://github.com/rhinobase/duck-form/issues" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^18.3.1", + "lodash": "^4.17.21" } } diff --git a/packages/core/src/components/DuckField.tsx b/packages/core/src/components/DuckField.tsx index c890b61..638358e 100644 --- a/packages/core/src/components/DuckField.tsx +++ b/packages/core/src/components/DuckField.tsx @@ -1,22 +1,24 @@ import { Fragment } from "react"; import { FieldProvider, useBlueprint, useDuckForm } from "../providers"; +import _ from "lodash"; export type DuckField< - T extends Record = Record, + T extends Record = Record > = { type: string; } & T; export function DuckField>(props: T) { const { wrapper: Wrapper = Fragment, schema } = useBlueprint() ?? {}; - const { components, resolver } = useDuckForm(); + const { components, resolverKey } = useDuckForm(); + + const options = _.get(schema, String(props[resolverKey])) as DuckField; - const options = resolver(schema, props) as DuckField; let Component = options?.type ? components[options.type] : undefined; Component ??= components.default; return ( - + diff --git a/packages/core/src/providers/duckform.tsx b/packages/core/src/providers/duckform.tsx index 6fb501e..dff4f60 100644 --- a/packages/core/src/providers/duckform.tsx +++ b/packages/core/src/providers/duckform.tsx @@ -6,33 +6,23 @@ import { } from "react"; import { ComponentNotFound } from "../components/ComponentNotFound"; -type DuckFormContextType = { +type DuckFormContextType = { readonly components: Record ReactNode>; - readonly generateId?: ( - schema: Record, - props: Record, - ) => string | undefined; - readonly resolver: ( - schema: Record, - props: Record, - ) => T | undefined; + resolverKey: string; }; -// biome-ignore lint/suspicious/noExplicitAny: Generic context -const DuckFormContext = createContext | null>(null); +const DuckFormContext = createContext(null); -export type DuckForm = PropsWithChildren>>; +export type DuckForm = PropsWithChildren>; -export function DuckForm({ +export function DuckForm({ children, components = {}, - resolver = defaultResolver, - generateId, -}: DuckForm) { + resolverKey = "id", +}: DuckForm) { const value = { components: { default: ComponentNotFound, ...components }, - resolver, - generateId, + resolverKey, }; return ( @@ -42,20 +32,10 @@ export function DuckForm({ ); } -export function useDuckForm() { - const context = useContext | null>(DuckFormContext); +export function useDuckForm() { + const context = useContext(DuckFormContext); if (!context) throw new Error("Missing DuckFormContext.Provider in the tree"); return context; } - -function defaultResolver( - schema: Record, - props: Record, -): T { - return { - ...schema[String(props.id)], - ...props, - }; -} diff --git a/packages/core/src/providers/field.tsx b/packages/core/src/providers/field.tsx index 6c42a3d..6e6a8f0 100644 --- a/packages/core/src/providers/field.tsx +++ b/packages/core/src/providers/field.tsx @@ -4,10 +4,11 @@ import type { DuckField } from "../components"; // biome-ignore lint/suspicious/noExplicitAny: Context is generic const FieldContext = createContext | null>(null); -export type FieldProvider> = - PropsWithChildren>; +export type FieldProvider = PropsWithChildren< + DuckField +>; -export function FieldProvider>({ +export function FieldProvider({ children, ...values }: FieldProvider) { @@ -16,8 +17,8 @@ export function FieldProvider>({ ); } -export function useField>() { - const context = useContext | null>(FieldContext); +export function useField() { + const context = useContext(FieldContext); if (!context) throw new Error("Missing FieldContext.Provider in the tree"); diff --git a/packages/fields/package.json b/packages/fields/package.json index 488fdc2..d9d8233 100644 --- a/packages/fields/package.json +++ b/packages/fields/package.json @@ -30,6 +30,8 @@ "duck-form": "^0.1.0", "react": "^18.2.0", "react-hook-form": "^7.53.0", - "@hookform/error-message": "^2.0.1" + "@hookform/devtools": "^4.3.3", + "@hookform/error-message": "^2.0.1", + "zod": "^3.24.1" } } diff --git a/packages/fields/src/Calendar.tsx b/packages/fields/src/Calendar.tsx deleted file mode 100644 index b442dbd..0000000 --- a/packages/fields/src/Calendar.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Calendar as RaftyCalendar } from "@rafty/ui"; -import type { FieldType } from "./constants"; - -export type CalendarProps = { - name?: string; - type: FieldType.CALENDAR; - placeholder?: string; - defaultValue?: string; - value?: string; - onChange?: (value?: string) => void; -}; - -export function CalendarField({ type, onChange, ...props }: CalendarProps) { - return ; -} diff --git a/packages/fields/src/Currency.tsx b/packages/fields/src/Currency.tsx deleted file mode 100644 index e9b3d01..0000000 --- a/packages/fields/src/Currency.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { CurrencyInput as RaftyCurrencyInput } from "@rafty/ui"; -import type { FieldType } from "./constants"; - -export type CurrencyInputProps = { - name?: string; - type: FieldType.CURRENCY_INPUT; - defaultValue?: string; - currencyCode?: string; - value?: string; - onChange?: (value?: string) => void; -}; - -export function CurrencyField({ type, ...props }: CurrencyInputProps) { - return ; -} diff --git a/packages/fields/src/Date.tsx b/packages/fields/src/Date.tsx deleted file mode 100644 index a1dfc3d..0000000 --- a/packages/fields/src/Date.tsx +++ /dev/null @@ -1,28 +0,0 @@ -"use client"; -import { InputField } from "@rafty/ui"; -import dayjs from "dayjs"; -import type { FieldType } from "./constants"; - -export type DateFieldProps = { - name?: string; - type: FieldType.DATE; - placeholder?: string; - defaultValue?: string; - value?: string; - onChange?: (value?: string) => void; -}; - -export function DateField({ type, value, onChange, ...props }: DateFieldProps) { - const formattedValue = value ? dayjs(value).format("YYYY-MM-DD") : undefined; - - return ( - onChange?.(dayjs(e.target.value).toISOString())} - /> - ); -} diff --git a/packages/fields/src/Default.tsx b/packages/fields/src/Default.tsx index b7e0897..9e4854b 100644 --- a/packages/fields/src/Default.tsx +++ b/packages/fields/src/Default.tsx @@ -1,27 +1,23 @@ "use client"; import { JSONExplorer } from "@rafty/corp"; import { Kbd } from "@rafty/ui"; -import { type DuckField, useBlueprint, useDuckForm } from "duck-form"; -import { useId, useMemo } from "react"; +import { type DuckField, useDuckForm } from "duck-form"; +import { useId } from "react"; export function DefaultField(props: DuckField>) { - const { generateId } = useDuckForm(); - const { schema } = useBlueprint(); + const { resolverKey } = useDuckForm(); const autoId = useId(); - const customId = useMemo( - () => generateId?.(schema, props), - [generateId, schema, props] - ); + const componentId = String(props[resolverKey]) ?? autoId; - const componentId = customId ?? autoId; + const fieldProps = { ...props, [resolverKey]: componentId }; return (

Field type {props.type} is not available!

- +
); } diff --git a/packages/fields/src/Div.tsx b/packages/fields/src/Div.tsx new file mode 100644 index 0000000..fb2d67e --- /dev/null +++ b/packages/fields/src/Div.tsx @@ -0,0 +1,30 @@ +import { DuckField, useDuckForm, useField } from "duck-form"; +import { useId } from "react"; +import type z from "zod"; +import type { divSchema } from "./validations"; + +export type DivProps = z.infer; + +export function Div() { + const props = useField(); + const { resolverKey } = useDuckForm(); + const { className, blocks } = props; + + const fieldProps = className ? { className } : {}; + + const autoId = useId(); + const componentId = String(props[resolverKey as keyof DivProps]) ?? autoId; + + return ( +
+ {blocks && + Object.keys(blocks).map((key) => { + const nestedProps = { + [resolverKey]: `${componentId}.${key}`, + }; + + return ; + })} +
+ ); +} diff --git a/packages/fields/src/EditableNumber.tsx b/packages/fields/src/EditableNumber.tsx deleted file mode 100644 index 9d577b0..0000000 --- a/packages/fields/src/EditableNumber.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { EditableNumber as RaftyEditableNumber } from "@rafty/ui"; -import type { FieldType } from "./constants"; - -export type EditableNumberProps = { - name?: string; - type: FieldType.EDITABLE_NUMBER; - placeholder?: string; - defaultValue?: number; - value?: number; - onChange?: (value?: number) => void; -}; - -export function EditableNumberField({ - type, - onChange, - ...props -}: EditableNumberProps) { - return ( - - ); -} diff --git a/packages/fields/src/EditableText.tsx b/packages/fields/src/EditableText.tsx deleted file mode 100644 index a7bd9c8..0000000 --- a/packages/fields/src/EditableText.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { EditableText as RaftyEditableText } from "@rafty/ui"; -import type { FieldType } from "./constants"; - -export type EditableTextProps = { - name?: string; - type: FieldType.EDITABLE_TEXT; - placeholder?: string; - defaultValue?: string; - value?: string; - onChange?: (value?: string) => void; -}; - -export function EditableTextField({ - type, - onChange, - ...props -}: EditableTextProps) { - return ( - - ); -} diff --git a/packages/fields/src/EditableTextarea.tsx b/packages/fields/src/EditableTextarea.tsx deleted file mode 100644 index 3945a2a..0000000 --- a/packages/fields/src/EditableTextarea.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { EditableTextarea as RaftyEditableTextarea } from "@rafty/ui"; -import type { FieldType } from "./constants"; - -export type EditableTextareaProps = { - name?: string; - type: FieldType.EDITABLE_TEXTAREA; - placeholder?: string; - defaultValue?: string; - value?: string; - onChange?: (value?: string) => void; -}; - -export function EditableTextareaField({ - type, - onChange, - ...props -}: EditableTextareaProps) { - return ( - - ); -} diff --git a/packages/fields/src/Link.tsx b/packages/fields/src/Link.tsx new file mode 100644 index 0000000..ac0337d --- /dev/null +++ b/packages/fields/src/Link.tsx @@ -0,0 +1,30 @@ +import { DuckField, useDuckForm, useField } from "duck-form"; +import { useId } from "react"; +import type z from "zod"; +import type { linkSchema } from "./validations"; + +export type LinkProps = z.infer; + +export function Link() { + const props = useField(); + const { resolverKey } = useDuckForm(); + const { className, blocks, link } = props; + + const fieldProps = className ? { className } : {}; + + const autoId = useId(); + const componentId = String(props[resolverKey as keyof LinkProps]) ?? autoId; + + return ( + + {blocks && + Object.keys(blocks).map((key) => { + const nestedProps = { + [resolverKey]: `${componentId}.${key}`, + }; + + return ; + })} + + ); +} diff --git a/packages/fields/src/Listbox.tsx b/packages/fields/src/Listbox.tsx deleted file mode 100644 index adac1bf..0000000 --- a/packages/fields/src/Listbox.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Listbox as RaftyListbox } from "@rafty/corp"; -import type { FieldType } from "./constants"; - -export type ListboxProps = { - name?: string; - type: FieldType.LISTBOX; - options: { - value: string; - label?: string; - }[]; - defaultValue?: string; - value?: string; - onChange?: (value?: string) => void; -}; - -export function ListboxField({ type, onChange, ...props }: ListboxProps) { - return ( - - ); -} diff --git a/packages/fields/src/MultiListbox.tsx b/packages/fields/src/MultiListbox.tsx deleted file mode 100644 index 9638997..0000000 --- a/packages/fields/src/MultiListbox.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Listbox as RaftyListbox } from "@rafty/corp"; -import type { FieldType } from "./constants"; - -export type MultiListboxProps = { - name?: string; - type: FieldType.MULTI_LISTBOX; - options: { - value: string; - label?: string; - }[]; - defaultValue?: string[]; - value?: string[]; - onChange?: (value?: string[]) => void; -}; - -export function MultiListboxField({ - type, - options, - onChange, - ...props -}: MultiListboxProps) { - return ( - - ); -} diff --git a/packages/fields/src/Number.tsx b/packages/fields/src/Number.tsx deleted file mode 100644 index 5e190b2..0000000 --- a/packages/fields/src/Number.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import type { InputField } from "@rafty/ui"; -import { InputField as RaftyInputField } from "@rafty/ui"; -import type { FieldType } from "./constants"; - -export type NumberProps = { - name?: string; - type: FieldType.NUMBER; - placeholder?: string; - inputMode?: "none" | "numeric" | "decimal"; - min?: InputField["min"]; - max?: InputField["max"]; - defaultValue?: number; - step?: number | "any"; - value?: number; - onChange?: (value?: number) => void; -}; - -export function NumberField({ type, onChange, step, ...props }: NumberProps) { - return ( - { - const value = event.target.value; - - onChange?.(value !== "" ? Number(value) : undefined); - }} - /> - ); -} diff --git a/packages/fields/src/Paragraph.tsx b/packages/fields/src/Paragraph.tsx new file mode 100644 index 0000000..8cb2f2b --- /dev/null +++ b/packages/fields/src/Paragraph.tsx @@ -0,0 +1,31 @@ +import { DuckField, useDuckForm, useField } from "duck-form"; +import { useId } from "react"; +import type z from "zod"; +import type { paragraphSchema } from "./validations"; + +export type ParagraphProps = z.infer; + +export function Paragraph() { + const props = useField(); + const { resolverKey } = useDuckForm(); + const { className, blocks } = props; + + const fieldProps = className ? { className } : {}; + + const autoId = useId(); + const componentId = + String(props[resolverKey as keyof ParagraphProps]) ?? autoId; + + return ( +

+ {blocks && + Object.keys(blocks).map((key) => { + const nestedProps = { + [resolverKey]: `${componentId}.${key}`, + }; + + return ; + })} +

+ ); +} diff --git a/packages/fields/src/Percentage.tsx b/packages/fields/src/Percentage.tsx deleted file mode 100644 index 40145e9..0000000 --- a/packages/fields/src/Percentage.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { PercentageInput as RaftyPercentageInput } from "@rafty/ui"; -import type { FieldType } from "./constants"; - -export type PercentageInputProps = { - name?: string; - type: FieldType.PERCENTAGE_INPUT; - defaultValue?: string; - value?: string; - onChange?: (value?: string) => void; -}; - -export function PercentageField({ type, ...props }: PercentageInputProps) { - return ; -} diff --git a/packages/fields/src/Rating.tsx b/packages/fields/src/Rating.tsx deleted file mode 100644 index 671f813..0000000 --- a/packages/fields/src/Rating.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Rating as RaftyRating } from "@rafty/ui"; -import type { FieldType } from "./constants"; - -export type RatingProps = { - name?: string; - type: FieldType.RATING; - count: number; - allowHalf?: boolean; - defaultValue?: number; - value?: number; - onChange?: (value?: number) => void; -}; - -export function RatingField({ type, onChange, ...props }: RatingProps) { - return ; -} diff --git a/packages/fields/src/SegmentedControl.tsx b/packages/fields/src/SegmentedControl.tsx deleted file mode 100644 index ba4e41b..0000000 --- a/packages/fields/src/SegmentedControl.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { - SegmentedControl as RaftySegmentedControl, - SegmentedControlItem, -} from "@rafty/ui"; -import type { FieldType } from "./constants"; - -export type SegmentedControlProps = { - name?: string; - type: FieldType.SEGMENTED_CONTROL; - options: { - value: string; - label?: string; - }[]; - defaultValue?: string; - value?: string; - onChange?: (value?: string) => void; -}; - -export function SegmentedControlField({ - type, - onChange, - options, - ...props -}: SegmentedControlProps) { - return ( - - {options.map(({ value, label }, index) => ( - - {label ?? value} - - ))} - - ); -} diff --git a/packages/fields/src/Span.tsx b/packages/fields/src/Span.tsx new file mode 100644 index 0000000..7d33a8f --- /dev/null +++ b/packages/fields/src/Span.tsx @@ -0,0 +1,30 @@ +import { DuckField, useDuckForm, useField } from "duck-form"; +import { useId } from "react"; +import type z from "zod"; +import type { spanSchema } from "./validations"; + +export type SpanProps = z.infer; + +export function Span() { + const props = useField(); + const { resolverKey } = useDuckForm(); + const { className, blocks } = props; + + const fieldProps = className ? { className } : {}; + + const autoId = useId(); + const componentId = String(props[resolverKey as keyof SpanProps]) ?? autoId; + + return ( + + {blocks && + Object.keys(blocks).map((key) => { + const nestedProps = { + [resolverKey]: `${componentId}.${key}`, + }; + + return ; + })} + + ); +} diff --git a/packages/fields/src/String.tsx b/packages/fields/src/String.tsx deleted file mode 100644 index 130ab88..0000000 --- a/packages/fields/src/String.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { InputField as RaftyInputField } from "@rafty/ui"; -import type { FieldType } from "./constants"; - -export type StringProps = { - name?: string; - type: FieldType.STRING; - inputType?: RaftyInputField["type"]; - placeholder?: string; - inputMode?: RaftyInputField["inputMode"]; - maxLength?: number; - minLength?: number; - defaultValue?: string; - value?: string; - onChange?: (value?: string) => void; -}; - -export function StringField({ - type, - onChange, - inputType, - ...props -}: StringProps) { - return ( - onChange?.(event.target.value)} - /> - ); -} diff --git a/packages/fields/src/Tag.tsx b/packages/fields/src/Tag.tsx deleted file mode 100644 index e8cb7a3..0000000 --- a/packages/fields/src/Tag.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { TagField as RaftyTagField } from "@rafty/ui"; -import type { FieldType } from "./constants"; - -export type TagFieldProps = { - name?: string; - type: FieldType.TAG; - defaultValue?: string[]; - value?: string[]; - onChange?: (value?: string[]) => void; -}; - -export function TagField({ type, onChange, ...props }: TagFieldProps) { - return ( - onChange?.(value)} - /> - ); -} diff --git a/packages/fields/src/Text.tsx b/packages/fields/src/Text.tsx new file mode 100644 index 0000000..30c4a70 --- /dev/null +++ b/packages/fields/src/Text.tsx @@ -0,0 +1,23 @@ +import { useDuckForm, useField } from "duck-form"; +import { useId } from "react"; +import type z from "zod"; +import type { textSchema } from "./validations"; + +export type TextProps = z.infer; + +export function Text() { + const props = useField(); + const { resolverKey } = useDuckForm(); + const { className, content } = props; + + const fieldProps = className ? { className } : {}; + + const autoId = useId(); + const componentId = String(props[resolverKey as keyof TextProps]) ?? autoId; + + return ( +

+ {content} +

+ ); +} diff --git a/packages/fields/src/Textarea.tsx b/packages/fields/src/Textarea.tsx deleted file mode 100644 index 65a26c5..0000000 --- a/packages/fields/src/Textarea.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Textarea as RaftyTextarea } from "@rafty/ui"; -import type { FieldType } from "./constants"; - -export type TextareaProps = { - name?: string; - type: FieldType.TEXTAREA; - placeholder?: string; - defaultValue?: string; - value?: string; - onChange?: (value?: string) => void; -}; - -export function TextareaField({ type, onChange, ...props }: TextareaProps) { - return ( - onChange?.(event.target.value)} - /> - ); -} diff --git a/packages/fields/src/config.tsx b/packages/fields/src/config.tsx index 528e5b4..0ed13be 100644 --- a/packages/fields/src/config.tsx +++ b/packages/fields/src/config.tsx @@ -1,36 +1,45 @@ -import { ArrayField } from "./Array"; -import { CalendarField } from "./Calendar"; -import { CheckboxField } from "./Checkbox"; -import { CheckboxGroupField } from "./CheckboxGroup"; -import { ColorPickerField } from "./ColorPicker"; -import { FieldType } from "./constants"; -import { CurrencyField } from "./Currency"; -import { DateField } from "./Date"; -import { DateRangeField } from "./DateRange"; -import { DatetimeField } from "./Datetime"; +import { BlockType } from "./constants"; import { DefaultField } from "./Default"; -import { EditableNumberField } from "./EditableNumber"; -import { EditableTextField } from "./EditableText"; -import { EditableTextareaField } from "./EditableTextarea"; -import { ListboxField } from "./Listbox"; -import { MultiListboxField } from "./MultiListbox"; -import { NumberField } from "./Number"; -import { ObjectField } from "./Object"; -import { PasswordField } from "./Password"; -import { PercentageField } from "./Percentage"; -import { PinField } from "./Pin"; -import { RadioGroupField } from "./RadioGroup"; -import { RangeSliderField } from "./RangeSlider"; -import { RatingField } from "./Rating"; -import { SegmentedControlField } from "./SegmentedControl"; -import { SelectField } from "./Select"; -import { SliderField } from "./Slider"; -import { StringField } from "./String"; -import { SwitchField } from "./Switch"; -import { SwitchGroupField } from "./SwitchGroup"; -import { TagField } from "./Tag"; -import { TextareaField } from "./Textarea"; -import { DuckWrapper, ReactHookFormWrapper } from "./wrappers"; +import { Div } from "./Div"; +import { + ArrayField, + CalendarField, + CheckboxField, + CheckboxGroupField, + ColorPickerField, + CurrencyField, + DateField, + DateRangeField, + DatetimeField, + EditableNumberField, + EditableTextareaField, + EditableTextField, + Form, + ListboxField, + MultiListboxField, + NumberField, + ObjectField, + PasswordField, + PercentageField, + PinField, + RadioGroupField, + RangeSliderField, + RatingField, + ReactHookFormWrapper, + SegmentedControlField, + SelectField, + SliderField, + StringField, + SwitchField, + SwitchGroupField, + TagField, + TextareaField, +} from "./fields"; +import { Link } from "./Link"; +import { Paragraph } from "./Paragraph"; +import { Span } from "./Span"; +import { Text } from "./Text"; +import { DuckWrapper } from "./wrappers"; // biome-ignore lint/suspicious/noExplicitAny: const wrapper = (Component: (props: any) => JSX.Element) => () => @@ -42,36 +51,42 @@ const wrapper = (Component: (props: any) => JSX.Element) => () => ); -export const quackFields: Record JSX.Element> = { - [FieldType.ARRAY]: ArrayField, - [FieldType.CALENDAR]: wrapper(CalendarField), - [FieldType.BOOLEAN]: wrapper(CheckboxField), - [FieldType.CHECKBOX_GROUP]: wrapper(CheckboxGroupField), - [FieldType.COLOR_PICKER]: wrapper(ColorPickerField), - [FieldType.CURRENCY_INPUT]: wrapper(CurrencyField), - [FieldType.DATE]: wrapper(DateField), - [FieldType.DATE_RANGE]: wrapper(DateRangeField), - [FieldType.DATE_TIME]: wrapper(DatetimeField), - [FieldType.DEFAULT]: wrapper(DefaultField), - [FieldType.EDITABLE_NUMBER]: wrapper(EditableNumberField), - [FieldType.EDITABLE_TEXT]: wrapper(EditableTextField), - [FieldType.EDITABLE_TEXTAREA]: wrapper(EditableTextareaField), - [FieldType.LISTBOX]: wrapper(ListboxField), - [FieldType.MULTI_LISTBOX]: wrapper(MultiListboxField), - [FieldType.NUMBER]: wrapper(NumberField), - [FieldType.OBJECT]: ObjectField, - [FieldType.PASSWORD]: wrapper(PasswordField), - [FieldType.PERCENTAGE_INPUT]: wrapper(PercentageField), - [FieldType.PIN]: wrapper(PinField), - [FieldType.RADIO]: wrapper(RadioGroupField), - [FieldType.RANGE_SLIDER]: wrapper(RangeSliderField), - [FieldType.RATING]: wrapper(RatingField), - [FieldType.SEGMENTED_CONTROL]: wrapper(SegmentedControlField), - [FieldType.SELECT]: wrapper(SelectField), - [FieldType.SLIDER]: wrapper(SliderField), - [FieldType.STRING]: wrapper(StringField), - [FieldType.SWTICH]: wrapper(SwitchField), - [FieldType.SWITCH_GROUP]: wrapper(SwitchGroupField), - [FieldType.TAG]: wrapper(TagField), - [FieldType.TEXTAREA]: wrapper(TextareaField), +export const quackFields: Record JSX.Element> = { + [BlockType.ARRAY]: ArrayField, + [BlockType.CALENDAR]: wrapper(CalendarField), + [BlockType.BOOLEAN]: wrapper(CheckboxField), + [BlockType.CHECKBOX_GROUP]: wrapper(CheckboxGroupField), + [BlockType.COLOR_PICKER]: wrapper(ColorPickerField), + [BlockType.CURRENCY_INPUT]: wrapper(CurrencyField), + [BlockType.DATE]: wrapper(DateField), + [BlockType.DATE_RANGE]: wrapper(DateRangeField), + [BlockType.DATE_TIME]: wrapper(DatetimeField), + [BlockType.DEFAULT]: wrapper(DefaultField), + [BlockType.EDITABLE_NUMBER]: wrapper(EditableNumberField), + [BlockType.EDITABLE_TEXT]: wrapper(EditableTextField), + [BlockType.EDITABLE_TEXTAREA]: wrapper(EditableTextareaField), + [BlockType.LISTBOX]: wrapper(ListboxField), + [BlockType.MULTI_LISTBOX]: wrapper(MultiListboxField), + [BlockType.NUMBER]: wrapper(NumberField), + [BlockType.OBJECT]: ObjectField, + [BlockType.PASSWORD]: wrapper(PasswordField), + [BlockType.PERCENTAGE_INPUT]: wrapper(PercentageField), + [BlockType.PIN]: wrapper(PinField), + [BlockType.RADIO]: wrapper(RadioGroupField), + [BlockType.RANGE_SLIDER]: wrapper(RangeSliderField), + [BlockType.RATING]: wrapper(RatingField), + [BlockType.SEGMENTED_CONTROL]: wrapper(SegmentedControlField), + [BlockType.SELECT]: wrapper(SelectField), + [BlockType.SLIDER]: wrapper(SliderField), + [BlockType.STRING]: wrapper(StringField), + [BlockType.SWTICH]: wrapper(SwitchField), + [BlockType.SWITCH_GROUP]: wrapper(SwitchGroupField), + [BlockType.TAG]: wrapper(TagField), + [BlockType.TEXTAREA]: wrapper(TextareaField), + [BlockType.LINK]: Link, + [BlockType.DIV]: Div, + [BlockType.SPAN]: Span, + [BlockType.TEXT]: Text, + [BlockType.PARAGRAPH]: Paragraph, + [BlockType.FORM]: Form, }; diff --git a/packages/fields/src/constants.ts b/packages/fields/src/constants.ts index 085ed93..9ed78f0 100644 --- a/packages/fields/src/constants.ts +++ b/packages/fields/src/constants.ts @@ -1,4 +1,4 @@ -export enum FieldType { +export enum BlockType { ARRAY = "array", CALENDAR = "calendar", BOOLEAN = "boolean", @@ -9,13 +9,17 @@ export enum FieldType { DATE_RANGE = "dateRange", DATE_TIME = "datetime", DEFAULT = "default", + DIV = "div", EDITABLE_NUMBER = "editableNumber", EDITABLE_TEXT = "editableText", EDITABLE_TEXTAREA = "editableTextarea", + FORM = "form", + LINK = "link", LISTBOX = "listbox", MULTI_LISTBOX = "multiListbox", NUMBER = "number", OBJECT = "object", + PARAGRAPH = "paragraph", PASSWORD = "password", PERCENTAGE_INPUT = "percentageInput", PIN = "pin", @@ -25,9 +29,17 @@ export enum FieldType { SEGMENTED_CONTROL = "segmentedControl", SELECT = "select", SLIDER = "slider", + SPAN = "span", STRING = "string", SWTICH = "switch", SWITCH_GROUP = "switchGroup", TAG = "tag", + TEXT = "text", TEXTAREA = "textarea", } + +export enum ORIENTATION { + ROW = "row", + COL = "col", + ROW_REVERSE = "row-reverse", +} diff --git a/packages/fields/src/Array.tsx b/packages/fields/src/fields/Array.tsx similarity index 74% rename from packages/fields/src/Array.tsx rename to packages/fields/src/fields/Array.tsx index d1f9a12..feb9146 100644 --- a/packages/fields/src/Array.tsx +++ b/packages/fields/src/fields/Array.tsx @@ -6,37 +6,20 @@ import { TrashIcon, } from "@heroicons/react/24/outline"; import { Button, eventHandler } from "@rafty/ui"; -import { DuckField, useBlueprint, useDuckForm, useField } from "duck-form"; -import { useId, useMemo } from "react"; +import { DuckField, useDuckForm, useField } from "duck-form"; +import { useId } from "react"; import { useFieldArray, useFormContext } from "react-hook-form"; -import type { FieldProps } from "./types"; -import type { FieldType } from "./constants"; +import type z from "zod"; +import type { arraySchema } from "../validations"; -export type ArrayProps = { - type: FieldType.ARRAY; - of: FieldProps; - defaultValue?: unknown[] | (() => unknown[]); - options?: { - sortable?: boolean; - layout?: "tags" | "grid"; - // biome-ignore lint/suspicious/noExplicitAny: - list?: { title: string; value: any }[]; - editModal?: "dialog" | "fullscreen" | "popover"; - }; -}; +export type ArrayProps = z.infer; export function ArrayField() { + const { resolverKey } = useDuckForm(); const props = useField(); - const { generateId } = useDuckForm(); - const { schema } = useBlueprint(); const autoId = useId(); - const customId = useMemo( - () => generateId?.(schema, props), - [generateId, schema, props] - ); - - const componentId = customId ?? autoId; + const componentId = String(props[resolverKey as keyof ArrayProps]) ?? autoId; const { control } = useFormContext(); const { fields, append, swap, remove, insert } = useFieldArray({ @@ -54,9 +37,16 @@ export function ArrayField() { const handleInsertNew = eventHandler(() => insert(index + 1, {})); const handleDelete = eventHandler(() => remove(index)); + const name = `${componentId}.${index}`; + + const nestedFieldProps = { + name, + [resolverKey]: `${componentId}.of.${index}`, + }; + return (
@@ -79,7 +69,7 @@ export function ArrayField() {
- +
- - - ); -} +"use client"; +import { EyeIcon, EyeSlashIcon } from "@heroicons/react/24/outline"; +import { + Button, + InputField, + InputGroup, + Suffix, + eventHandler, + useBoolean, +} from "@rafty/ui"; +import type z from "zod"; +import type { passwordSchema } from "../validations"; + +export type PasswordProps = z.infer; + +export function PasswordField({ + onChange, + defaultValue, + name, + placeholder, + value, +}: PasswordProps) { + const [showPassword, toggle] = useBoolean(false); + + const Icon = showPassword ? EyeSlashIcon : EyeIcon; + + const handlerToggleShowPassword = eventHandler(() => toggle()); + + return ( + + onChange?.(event.target.value)} + /> + + + + + ); +} diff --git a/packages/fields/src/fields/Percentage.tsx b/packages/fields/src/fields/Percentage.tsx new file mode 100644 index 0000000..2c904c7 --- /dev/null +++ b/packages/fields/src/fields/Percentage.tsx @@ -0,0 +1,21 @@ +import { PercentageInput as RaftyPercentageInput } from "@rafty/ui"; +import type z from "zod"; +import type { percentageInputSchema } from "../validations"; + +export type PercentageInputProps = z.infer; + +export function PercentageField({ + defaultValue, + name, + onChange, + value, +}: PercentageInputProps) { + return ( + + ); +} diff --git a/packages/fields/src/Pin.tsx b/packages/fields/src/fields/Pin.tsx similarity index 61% rename from packages/fields/src/Pin.tsx rename to packages/fields/src/fields/Pin.tsx index 3a4f781..82e1adc 100644 --- a/packages/fields/src/Pin.tsx +++ b/packages/fields/src/fields/Pin.tsx @@ -1,22 +1,16 @@ import { PinInput as RaftyPinInput } from "@rafty/ui"; -import type { FieldType } from "./constants"; +import type z from "zod"; +import type { pinSchema } from "../validations"; -export type PinInputProps = { - name?: string; - type: FieldType.PIN; - length: number; - placeholder?: string; - defaultValue?: string; - value?: string; - onChange?: (value?: string[]) => void; -}; +export type PinInputProps = z.infer; export function PinField({ - type, defaultValue, value, onChange, - ...props + length, + name, + placeholder, }: PinInputProps) { const formattedValue = value ? Array.from(value) : undefined; const formattedDefaultValue = defaultValue @@ -25,8 +19,9 @@ export function PinField({ return ( onChange?.(value)} diff --git a/packages/fields/src/RadioGroup.tsx b/packages/fields/src/fields/RadioGroup.tsx similarity index 63% rename from packages/fields/src/RadioGroup.tsx rename to packages/fields/src/fields/RadioGroup.tsx index 112367d..10ab090 100644 --- a/packages/fields/src/RadioGroup.tsx +++ b/packages/fields/src/fields/RadioGroup.tsx @@ -3,45 +3,36 @@ import { RadioGroup as RaftyRadioGroup, classNames, } from "@rafty/ui"; -import type { ReactNode } from "react"; -import type { FieldType } from "./constants"; +import type z from "zod"; +import type { radioGroupSchema } from "../validations"; -export type RadioGroupProps = { - name?: string; - type: FieldType.RADIO; - options: { - value: string | number; - label?: ReactNode; - description?: string; - }[]; - orientaion?: "horizontal" | "vertical"; - defaultValue?: string; - value?: string; - onChange?: (value?: string) => void; -}; +export type RadioGroupProps = z.infer; export function RadioGroupField({ - type, options, - orientaion = "vertical", + orientation = "vertical", onChange, - ...props + defaultValue, + name, + value, }: RadioGroupProps) { return ( div]:w-full xl:[&>div]:w-max" )} > {options.map((option, index) => { - const _id = `${props.name}.${option.value}`; + const _id = `${name}.${option.value}`; if (option.description) return ( -
+