diff --git a/apps/web/.source/server.ts b/apps/web/.source/server.ts index 8fe8900..4d96bd8 100644 --- a/apps/web/.source/server.ts +++ b/apps/web/.source/server.ts @@ -35,8 +35,8 @@ import * as __fd_glob_7 from "../content/docs/dynamic-values.mdx?collection=docs import * as __fd_glob_6 from "../content/docs/developer-guide.mdx?collection=docs" import * as __fd_glob_5 from "../content/docs/custom-fields.mdx?collection=docs" import * as __fd_glob_4 from "../content/docs/conditional-logic.mdx?collection=docs" -import { default as __fd_glob_3 } from "../content/docs/fields/data/meta.json?collection=docs" -import { default as __fd_glob_2 } from "../content/docs/fields/layout/meta.json?collection=docs" +import { default as __fd_glob_3 } from "../content/docs/fields/layout/meta.json?collection=docs" +import { default as __fd_glob_2 } from "../content/docs/fields/data/meta.json?collection=docs" import { default as __fd_glob_1 } from "../content/docs/fields/meta.json?collection=docs" import { default as __fd_glob_0 } from "../content/docs/meta.json?collection=docs" import { server } from 'fumadocs-mdx/runtime/server'; @@ -47,4 +47,4 @@ const create = server({"doc":{"passthroughs":["extractedReferences"]}}); -export const docs = await create.docs("docs", "content/docs", {"meta.json": __fd_glob_0, "fields/meta.json": __fd_glob_1, "fields/layout/meta.json": __fd_glob_2, "fields/data/meta.json": __fd_glob_3, }, {"conditional-logic.mdx": __fd_glob_4, "custom-fields.mdx": __fd_glob_5, "developer-guide.mdx": __fd_glob_6, "dynamic-values.mdx": __fd_glob_7, "field-hooks.mdx": __fd_glob_8, "field-wrappers.mdx": __fd_glob_9, "form-component.mdx": __fd_glob_10, "form-provider.mdx": __fd_glob_11, "index.mdx": __fd_glob_12, "installation.mdx": __fd_glob_13, "migration.mdx": __fd_glob_14, "option-resolvers.mdx": __fd_glob_15, "output-transformation.mdx": __fd_glob_16, "quick-start.mdx": __fd_glob_17, "render-fields.mdx": __fd_glob_18, "schema.mdx": __fd_glob_19, "use-form.mdx": __fd_glob_20, "validation.mdx": __fd_glob_21, "fields/types.mdx": __fd_glob_22, "fields/layout/array.mdx": __fd_glob_23, "fields/layout/collapsible.mdx": __fd_glob_24, "fields/layout/group.mdx": __fd_glob_25, "fields/layout/row.mdx": __fd_glob_26, "fields/layout/tabs.mdx": __fd_glob_27, "fields/data/checkbox.mdx": __fd_glob_28, "fields/data/date.mdx": __fd_glob_29, "fields/data/email.mdx": __fd_glob_30, "fields/data/number.mdx": __fd_glob_31, "fields/data/password.mdx": __fd_glob_32, "fields/data/radio.mdx": __fd_glob_33, "fields/data/select.mdx": __fd_glob_34, "fields/data/switch.mdx": __fd_glob_35, "fields/data/tags.mdx": __fd_glob_36, "fields/data/text.mdx": __fd_glob_37, "fields/data/textarea.mdx": __fd_glob_38, "fields/data/upload.mdx": __fd_glob_39, }); \ No newline at end of file +export const docs = await create.docs("docs", "content/docs", {"meta.json": __fd_glob_0, "fields/meta.json": __fd_glob_1, "fields/data/meta.json": __fd_glob_2, "fields/layout/meta.json": __fd_glob_3, }, {"conditional-logic.mdx": __fd_glob_4, "custom-fields.mdx": __fd_glob_5, "developer-guide.mdx": __fd_glob_6, "dynamic-values.mdx": __fd_glob_7, "field-hooks.mdx": __fd_glob_8, "field-wrappers.mdx": __fd_glob_9, "form-component.mdx": __fd_glob_10, "form-provider.mdx": __fd_glob_11, "index.mdx": __fd_glob_12, "installation.mdx": __fd_glob_13, "migration.mdx": __fd_glob_14, "option-resolvers.mdx": __fd_glob_15, "output-transformation.mdx": __fd_glob_16, "quick-start.mdx": __fd_glob_17, "render-fields.mdx": __fd_glob_18, "schema.mdx": __fd_glob_19, "use-form.mdx": __fd_glob_20, "validation.mdx": __fd_glob_21, "fields/types.mdx": __fd_glob_22, "fields/layout/array.mdx": __fd_glob_23, "fields/layout/collapsible.mdx": __fd_glob_24, "fields/layout/group.mdx": __fd_glob_25, "fields/layout/row.mdx": __fd_glob_26, "fields/layout/tabs.mdx": __fd_glob_27, "fields/data/checkbox.mdx": __fd_glob_28, "fields/data/date.mdx": __fd_glob_29, "fields/data/email.mdx": __fd_glob_30, "fields/data/number.mdx": __fd_glob_31, "fields/data/password.mdx": __fd_glob_32, "fields/data/radio.mdx": __fd_glob_33, "fields/data/select.mdx": __fd_glob_34, "fields/data/switch.mdx": __fd_glob_35, "fields/data/tags.mdx": __fd_glob_36, "fields/data/text.mdx": __fd_glob_37, "fields/data/textarea.mdx": __fd_glob_38, "fields/data/upload.mdx": __fd_glob_39, }); \ No newline at end of file diff --git a/apps/web/public/r/document-upload-form.json b/apps/web/public/r/document-upload-form.json index b205ceb..ad58686 100644 --- a/apps/web/public/r/document-upload-form.json +++ b/apps/web/public/r/document-upload-form.json @@ -12,7 +12,7 @@ "files": [ { "path": "registry/shadcn/examples/document-upload-form.tsx", - "content": "\"use client\";\n\nimport { toast } from \"sonner\";\nimport { defineSchema, type InferType } from \"@buildnbuzz/form-react\";\nimport {\n Form,\n FormContent,\n FormFields,\n FormSubmit,\n FormReset,\n FormActions,\n} from \"@/registry/shadcn/form\";\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\";\n\nconst documentSchema = defineSchema({\n fields: [\n {\n type: \"upload\",\n name: \"document\",\n label: \"Upload Document\",\n description: \"PDF, Word, or text files (max 10MB)\",\n maxSize: 10 * 1024 * 1024,\n ui: {\n variant: \"dropzone\",\n accept: \".pdf,.doc,.docx,.txt\",\n dropzoneText: \"Drop your document here or click to browse\",\n },\n },\n {\n type: \"textarea\",\n name: \"notes\",\n label: \"Additional Notes\",\n placeholder: \"Any notes about this document...\",\n ui: {\n rows: 3,\n },\n },\n ],\n});\n\ntype DocumentSchema = InferType;\n\nexport default function DocumentUploadForm() {\n const handleSubmit = async ({ value }: { value: unknown }) => {\n const data = value as DocumentSchema;\n await new Promise((r) => setTimeout(r, 1000));\n toast(\"Document uploaded successfully!\", {\n description: (\n
\n

\n Check the console for the full File objects.\n

\n
\n            \n              {JSON.stringify(\n                {\n                  ...data,\n                  document: data.document\n                    ? data.document instanceof File\n                      ? {\n                          name: data.document.name,\n                          size: data.document.size,\n                          type: data.document.type,\n                        }\n                      : data.document\n                    : null,\n                },\n                null,\n                2,\n              )}\n            \n          
\n
\n ),\n });\n };\n\n return (\n \n \n Document Upload\n Drag-and-drop file upload with validation\n \n \n
\n \n \n \n Clear\n Upload Document\n \n \n
\n
\n
\n );\n}\n", + "content": "\"use client\";\n\nimport { toast } from \"sonner\";\nimport { defineSchema, type InferType } from \"@buildnbuzz/form-react\";\nimport {\n Form,\n FormContent,\n FormFields,\n FormSubmit,\n FormReset,\n FormActions,\n} from \"@/registry/shadcn/form\";\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\";\n\nconst documentSchema = defineSchema({\n fields: [\n {\n type: \"upload\",\n name: \"document\",\n label: \"Upload Document\",\n description: \"PDF, Word, or text files (max 10MB)\",\n maxSize: 10 * 1024 * 1024,\n ui: {\n variant: \"dropzone\",\n accept: \".pdf,.doc,.docx,.txt\",\n dropzoneText: \"Drop your document here or click to browse\",\n },\n },\n {\n type: \"textarea\",\n name: \"notes\",\n label: \"Additional Notes\",\n placeholder: \"Any notes about this document...\",\n ui: {\n rows: 3,\n },\n },\n ],\n});\n\ntype DocumentSchema = InferType;\n\nexport default function DocumentUploadForm() {\n const handleSubmit = async ({ value }: { value: unknown }) => {\n const data = value as DocumentSchema;\n await new Promise((r) => setTimeout(r, 1000));\n toast(\"Document uploaded successfully!\", {\n description: (\n
\n

\n Check the console for the full File objects.\n

\n
\n            \n              {JSON.stringify(\n                {\n                  ...data,\n                  document: data.document\n                    ? (typeof window !== \"undefined\" && data.document instanceof window.File)\n                      ? {\n                          name: data.document.name,\n                          size: data.document.size,\n                          type: data.document.type,\n                        }\n                      : data.document\n                    : null,\n                },\n                null,\n                2,\n              )}\n            \n          
\n
\n ),\n });\n };\n\n return (\n \n \n Document Upload\n Drag-and-drop file upload with validation\n \n \n
\n \n \n \n Clear\n Upload Document\n \n \n
\n
\n
\n );\n}\n", "type": "registry:component", "target": "components/buzzform/examples/document-upload-form.tsx" } diff --git a/apps/web/public/r/gallery-upload-form.json b/apps/web/public/r/gallery-upload-form.json index 700cfd0..e5e15db 100644 --- a/apps/web/public/r/gallery-upload-form.json +++ b/apps/web/public/r/gallery-upload-form.json @@ -11,7 +11,7 @@ "files": [ { "path": "registry/shadcn/examples/gallery-upload-form.tsx", - "content": "\"use client\";\n\nimport { toast } from \"sonner\";\nimport { defineSchema, type InferType } from \"@buildnbuzz/form-react\";\nimport {\n Form,\n FormContent,\n FormFields,\n FormSubmit,\n FormReset,\n FormActions,\n} from \"@/registry/shadcn/form\";\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\";\n\nconst gallerySchema = defineSchema({\n fields: [\n {\n type: \"upload\",\n name: \"images\",\n label: \"Product Images\",\n description: \"Upload up to 6 product images\",\n hasMany: true,\n max: 6,\n ui: {\n variant: \"gallery\",\n accept: \"image/*\",\n shape: \"rounded\",\n },\n },\n ],\n});\n\ntype GallerySchema = InferType;\n\nexport default function GalleryUploadForm() {\n const handleSubmit = async ({ value }: { value: unknown }) => {\n const data = value as GallerySchema;\n await new Promise((r) => setTimeout(r, 1000));\n\n const filesInfo = Array.isArray(data.images)\n ? data.images.map((img) =>\n img instanceof File\n ? { name: img.name, size: img.size, type: img.type }\n : img,\n )\n : [];\n\n toast(\"Images saved!\", {\n description: (\n
\n

\n Check the console for the full File objects.\n

\n
\n            {JSON.stringify({ ...data, images: filesInfo }, null, 2)}\n          
\n
\n ),\n });\n };\n\n return (\n \n \n Product Gallery\n Multi-file upload with thumbnail grid preview\n \n \n
\n \n \n \n Clear\n Save Images\n \n \n
\n
\n
\n );\n}\n", + "content": "\"use client\";\n\nimport { toast } from \"sonner\";\nimport { defineSchema, type InferType } from \"@buildnbuzz/form-react\";\nimport {\n Form,\n FormContent,\n FormFields,\n FormSubmit,\n FormReset,\n FormActions,\n} from \"@/registry/shadcn/form\";\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\";\n\nconst gallerySchema = defineSchema({\n fields: [\n {\n type: \"upload\",\n name: \"images\",\n label: \"Product Images\",\n description: \"Upload up to 6 product images\",\n hasMany: true,\n max: 6,\n ui: {\n variant: \"gallery\",\n accept: \"image/*\",\n shape: \"rounded\",\n },\n },\n ],\n});\n\ntype GallerySchema = InferType;\n\nexport default function GalleryUploadForm() {\n const handleSubmit = async ({ value }: { value: unknown }) => {\n const data = value as GallerySchema;\n await new Promise((r) => setTimeout(r, 1000));\n\n const filesInfo = Array.isArray(data.images)\n ? data.images.map((img) =>\n typeof window !== \"undefined\" && img instanceof window.File\n ? { name: img.name, size: img.size, type: img.type }\n : img,\n )\n : [];\n\n toast(\"Images saved!\", {\n description: (\n
\n

\n Check the console for the full File objects.\n

\n
\n            {JSON.stringify({ ...data, images: filesInfo }, null, 2)}\n          
\n
\n ),\n });\n };\n\n return (\n \n \n Product Gallery\n Multi-file upload with thumbnail grid preview\n \n \n
\n \n \n \n Clear\n Save Images\n \n \n
\n
\n
\n );\n}\n", "type": "registry:component", "target": "components/buzzform/examples/gallery-upload-form.tsx" } diff --git a/apps/web/public/r/profile-upload-form.json b/apps/web/public/r/profile-upload-form.json index 54cb4ff..76e166a 100644 --- a/apps/web/public/r/profile-upload-form.json +++ b/apps/web/public/r/profile-upload-form.json @@ -13,7 +13,7 @@ "files": [ { "path": "registry/shadcn/examples/profile-upload-form.tsx", - "content": "\"use client\";\n\nimport { toast } from \"sonner\";\nimport { defineSchema, type InferType } from \"@buildnbuzz/form-react\";\nimport {\n Form,\n FormContent,\n FormFields,\n FormSubmit,\n FormReset,\n FormActions,\n} from \"@/registry/shadcn/form\";\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\";\n\nconst profileSchema = defineSchema({\n fields: [\n {\n type: \"upload\",\n name: \"avatar\",\n label: \"Profile Picture\",\n description: \"Upload a profile photo\",\n ui: {\n variant: \"avatar\",\n shape: \"circle\",\n size: \"lg\",\n accept: \"image/*\",\n },\n },\n {\n type: \"text\",\n name: \"name\",\n label: \"Display Name\",\n placeholder: \"Your name\",\n required: true,\n },\n {\n type: \"email\",\n name: \"email\",\n label: \"Email\",\n placeholder: \"you@example.com\",\n required: true,\n },\n ],\n});\n\ntype ProfileSchema = InferType;\n\nexport default function ProfileUploadForm() {\n const handleSubmit = async ({ value }: { value: unknown }) => {\n const data = value as ProfileSchema;\n await new Promise((r) => setTimeout(r, 1000));\n toast(\"Profile saved successfully!\", {\n description: (\n
\n

\n Check the console for the full File objects.\n

\n
\n            \n              {JSON.stringify(\n                {\n                  ...data,\n                  avatar: data.avatar\n                    ? data.avatar instanceof File\n                      ? {\n                          name: data.avatar.name,\n                          size: data.avatar.size,\n                          type: data.avatar.type,\n                        }\n                      : data.avatar\n                    : null,\n                },\n                null,\n                2,\n              )}\n            \n          
\n
\n ),\n });\n };\n\n return (\n \n \n Profile Settings\n \n Avatar upload with circle preview layout\n \n \n \n
\n \n \n \n Clear\n Save Profile\n \n \n
\n
\n
\n );\n}\n", + "content": "\"use client\";\n\nimport { toast } from \"sonner\";\nimport { defineSchema, type InferType } from \"@buildnbuzz/form-react\";\nimport {\n Form,\n FormContent,\n FormFields,\n FormSubmit,\n FormReset,\n FormActions,\n} from \"@/registry/shadcn/form\";\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\";\n\nconst profileSchema = defineSchema({\n fields: [\n {\n type: \"upload\",\n name: \"avatar\",\n label: \"Profile Picture\",\n description: \"Upload a profile photo\",\n ui: {\n variant: \"avatar\",\n shape: \"circle\",\n size: \"lg\",\n accept: \"image/*\",\n },\n },\n {\n type: \"text\",\n name: \"name\",\n label: \"Display Name\",\n placeholder: \"Your name\",\n required: true,\n },\n {\n type: \"email\",\n name: \"email\",\n label: \"Email\",\n placeholder: \"you@example.com\",\n required: true,\n },\n ],\n});\n\ntype ProfileSchema = InferType;\n\nexport default function ProfileUploadForm() {\n const handleSubmit = async ({ value }: { value: unknown }) => {\n const data = value as ProfileSchema;\n await new Promise((r) => setTimeout(r, 1000));\n toast(\"Profile saved successfully!\", {\n description: (\n
\n

\n Check the console for the full File objects.\n

\n
\n            \n              {JSON.stringify(\n                {\n                  ...data,\n                  avatar: data.avatar\n                    ? (typeof window !== \"undefined\" && data.avatar instanceof window.File)\n                      ? {\n                          name: data.avatar.name,\n                          size: data.avatar.size,\n                          type: data.avatar.type,\n                        }\n                      : data.avatar\n                    : null,\n                },\n                null,\n                2,\n              )}\n            \n          
\n
\n ),\n });\n };\n\n return (\n \n \n Profile Settings\n \n Avatar upload with circle preview layout\n \n \n \n
\n \n \n \n Clear\n Save Profile\n \n \n
\n
\n
\n );\n}\n", "type": "registry:component", "target": "components/buzzform/examples/profile-upload-form.tsx" } diff --git a/apps/web/public/r/upload.json b/apps/web/public/r/upload.json index 84af4ec..de40a15 100644 --- a/apps/web/public/r/upload.json +++ b/apps/web/public/r/upload.json @@ -11,7 +11,7 @@ "files": [ { "path": "registry/shadcn/fields/upload.tsx", - "content": "\"use client\";\n\nimport * as React from \"react\";\nimport type { UploadField as UploadFieldDef } from \"@buildnbuzz/form-react\";\nimport { useDataField } from \"@buildnbuzz/form-react\";\nimport { IconPlaceholder } from \"@/components/icon-placeholder\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Field,\n FieldContent,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/components/ui/field\";\n\n// ---------------------------------------------------------------------------\n// UI Options defined inside the registry component\n// ---------------------------------------------------------------------------\n\nexport interface UploadUi {\n /** Visual variant. Defaults to `\"dropzone\"`. */\n variant?: \"dropzone\" | \"avatar\" | \"inline\" | \"gallery\";\n /** Shape preset for avatar / dropzone preview. Defaults to `\"rounded\"`. */\n shape?: \"circle\" | \"square\" | \"rounded\";\n /** Size preset. Defaults to `\"md\"`. */\n size?: \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\";\n /** Accepted mime types, e.g. \"image/*,application/pdf\". */\n accept?: string;\n /** Overwrite standard placeholder / dropzone text. */\n dropzoneText?: string;\n /** className applied to ``. */\n className?: string;\n /** Inline width applied to ``. */\n width?: string | number;\n /** Controls required asterisk visibility. */\n asterisk?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction formatBytes(bytes: number, decimals = 1) {\n if (bytes === 0) return \"0 Bytes\";\n const k = 1024;\n const dm = decimals < 0 ? 0 : decimals;\n const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\"];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + \" \" + sizes[i];\n}\n\nfunction FileTypeIcon({ name }: { name: string }) {\n const ext = name.split(\".\").pop()?.toLowerCase();\n const isImage = [\"png\", \"jpg\", \"jpeg\", \"webp\", \"gif\", \"svg\"].includes(\n ext || \"\",\n );\n const isPdf = ext === \"pdf\";\n const isZip = [\"zip\", \"rar\", \"tar\", \"gz\", \"7z\"].includes(ext || \"\");\n\n if (isImage) {\n return (\n \n );\n }\n if (isPdf) {\n return (\n \n );\n }\n if (isZip) {\n return (\n \n );\n }\n return (\n \n );\n}\n\nexport function UploadField() {\n const {\n fieldApi,\n field,\n isDisabled,\n isRequired,\n label,\n placeholder,\n description,\n errors,\n isInvalid,\n descriptionId,\n errorId,\n ariaDescribedBy,\n handleChange,\n } = useDataField();\n\n const ui = field.ui as UploadUi | undefined;\n const hasMany = !!field.hasMany;\n const variant = ui?.variant || \"dropzone\";\n const shape = ui?.shape || \"rounded\";\n const size = ui?.size || \"md\";\n\n const fileInputRef = React.useRef(null);\n const [isDragActive, setIsDragActive] = React.useState(false);\n const [previews, setPreviews] = React.useState<\n Array<{ name: string; size?: number; url?: string }>\n >([]);\n\n const rawValue = fieldApi.state.value;\n\n // Sync value from state to internal previews array\n React.useEffect(() => {\n if (!rawValue) {\n setPreviews([]);\n return;\n }\n\n const items = Array.isArray(rawValue) ? rawValue : [rawValue];\n const newPreviews = items.map((item) => {\n if (item instanceof File) {\n return {\n name: item.name,\n size: item.size,\n url: item.type.startsWith(\"image/\")\n ? URL.createObjectURL(item)\n : undefined,\n };\n }\n if (typeof item === \"string\") {\n return {\n name: item.split(\"/\").pop() || item,\n url: item,\n };\n }\n return { name: \"Unknown File\" };\n });\n\n setPreviews(newPreviews);\n\n // Cleanup object URLs on unmount/re-sync\n return () => {\n newPreviews.forEach((p) => {\n if (p.url && p.url.startsWith(\"blob:\")) {\n URL.revokeObjectURL(p.url);\n }\n });\n };\n }, [rawValue]);\n\n const onFilesSelected = (fileList: FileList | null) => {\n if (!fileList || fileList.length === 0) return;\n const newFiles = Array.from(fileList);\n\n if (hasMany) {\n const currentFiles = Array.isArray(rawValue)\n ? rawValue\n : rawValue\n ? [rawValue]\n : [];\n handleChange([...currentFiles, ...newFiles]);\n } else {\n handleChange(newFiles[0]);\n }\n };\n\n const removeFile = (index: number) => {\n if (isDisabled) return;\n if (hasMany && Array.isArray(rawValue)) {\n const updated = rawValue.filter((_, i) => i !== index);\n handleChange(updated.length > 0 ? updated : []);\n } else {\n handleChange(null as unknown as File | File[] | string | string[]);\n }\n };\n\n const handleDrag = (e: React.DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n if (isDisabled) return;\n\n if (e.type === \"dragenter\" || e.type === \"dragover\") {\n setIsDragActive(true);\n } else if (e.type === \"dragleave\") {\n setIsDragActive(false);\n }\n };\n\n const handleDrop = (e: React.DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setIsDragActive(false);\n if (isDisabled) return;\n\n if (e.dataTransfer.files && e.dataTransfer.files[0]) {\n onFilesSelected(e.dataTransfer.files);\n }\n };\n\n const triggerSelect = () => {\n if (isDisabled) return;\n fileInputRef.current?.click();\n };\n\n const width = ui?.width;\n const widthStyle = width\n ? {\n width: typeof width === \"number\" ? `${width}px` : width,\n flex: \"0 1 auto\",\n }\n : undefined;\n\n // Inline styling calculations\n const dropzoneText =\n ui?.dropzoneText ||\n placeholder ||\n (hasMany\n ? \"Drop files here or click to upload\"\n : \"Drop file here or click to upload\");\n\n return (\n \n \n {label && (\n \n {label}\n {isRequired && ui?.asterisk !== false ? (\n *\n ) : null}\n \n )}\n\n \n
\n onFilesSelected(e.target.files)}\n className=\"hidden\"\n aria-describedby={ariaDescribedBy}\n aria-invalid={isInvalid}\n />\n\n {/* VARIANT: AVATAR */}\n {variant === \"avatar\" && (\n
\n \n {previews[0]?.url ? (\n // eslint-disable-next-line @next/next/no-img-element\n \n ) : (\n
\n \n
\n )}\n\n {/* Hover action overlay */}\n
\n \n Edit\n
\n
\n\n {previews.length > 0 && !isDisabled && (\n removeFile(0)}\n className=\"text-muted-foreground hover:text-destructive h-7 px-2\"\n >\n Remove\n \n )}\n
\n )}\n\n {/* VARIANT: DROPZONE (DEFAULT) */}\n {variant === \"dropzone\" && (\n
\n \n
\n \n
\n
\n

{dropzoneText}

\n

\n {ui?.accept\n ? `Supports: ${ui.accept}`\n : \"All formats supported\"}\n {field.maxSize\n ? ` up to ${formatBytes(field.maxSize)}`\n : \"\"}\n

\n
\n
\n\n {/* Render Selected Files List */}\n {previews.length > 0 && (\n
\n {previews.map((file, idx) => (\n \n \n
\n

{file.name}

\n {file.size && (\n

\n {formatBytes(file.size)}\n

\n )}\n
\n {!isDisabled && (\n removeFile(idx)}\n className=\"size-8 text-muted-foreground hover:text-destructive shrink-0\"\n >\n \n \n )}\n
\n ))}\n \n )}\n \n )}\n\n {/* VARIANT: INLINE */}\n {variant === \"inline\" && (\n
\n
\n \n \n Attach File\n \n \n {previews.length === 0\n ? \"No file selected\"\n : `${previews.length} file(s) attached`}\n \n
\n\n {previews.length > 0 && (\n
\n {previews.map((file, idx) => (\n \n \n {file.name}\n {!isDisabled && (\n removeFile(idx)}\n className=\"size-5 rounded-full hover:bg-muted-foreground/20 flex items-center justify-center text-muted-foreground hover:text-foreground shrink-0\"\n >\n \n \n )}\n
\n ))}\n
\n )}\n \n )}\n\n {/* VARIANT: GALLERY */}\n {variant === \"gallery\" && (\n
\n {previews.map((file, idx) => (\n \n {file.url ? (\n // eslint-disable-next-line @next/next/no-img-element\n \n ) : (\n
\n \n \n {file.name}\n \n
\n )}\n\n {/* Delete overlay */}\n {!isDisabled && (\n
\n removeFile(idx)}\n className=\"size-8\"\n >\n \n \n
\n )}\n
\n ))}\n\n {/* Add Box */}\n {(!hasMany && previews.length === 0) ||\n (hasMany && (!field.max || previews.length < field.max)) ? (\n \n \n \n Add file\n \n \n ) : null}\n \n )}\n \n
\n\n {description && !isInvalid && (\n {description}\n )}\n\n {isInvalid && }\n
\n
\n );\n}\n", + "content": "\"use client\";\n\nimport * as React from \"react\";\nimport type { UploadField as UploadFieldDef } from \"@buildnbuzz/form-react\";\nimport { useDataField } from \"@buildnbuzz/form-react\";\nimport { IconPlaceholder } from \"@/components/icon-placeholder\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Field,\n FieldContent,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLabel,\n} from \"@/components/ui/field\";\n\n// ---------------------------------------------------------------------------\n// UI Options defined inside the registry component\n// ---------------------------------------------------------------------------\n\nexport interface UploadUi {\n /** Visual variant. Defaults to `\"dropzone\"`. */\n variant?: \"dropzone\" | \"avatar\" | \"inline\" | \"gallery\";\n /** Shape preset for avatar / dropzone preview. Defaults to `\"rounded\"`. */\n shape?: \"circle\" | \"square\" | \"rounded\";\n /** Size preset. Defaults to `\"md\"`. */\n size?: \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\";\n /** Accepted mime types, e.g. \"image/*,application/pdf\". */\n accept?: string;\n /** Overwrite standard placeholder / dropzone text. */\n dropzoneText?: string;\n /** className applied to ``. */\n className?: string;\n /** Inline width applied to ``. */\n width?: string | number;\n /** Controls required asterisk visibility. */\n asterisk?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction formatBytes(bytes: number, decimals = 1) {\n if (bytes === 0) return \"0 Bytes\";\n const k = 1024;\n const dm = decimals < 0 ? 0 : decimals;\n const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\"];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + \" \" + sizes[i];\n}\n\nfunction FileTypeIcon({ name }: { name: string }) {\n const ext = name.split(\".\").pop()?.toLowerCase();\n const isImage = [\"png\", \"jpg\", \"jpeg\", \"webp\", \"gif\", \"svg\"].includes(\n ext || \"\",\n );\n const isPdf = ext === \"pdf\";\n const isZip = [\"zip\", \"rar\", \"tar\", \"gz\", \"7z\"].includes(ext || \"\");\n\n if (isImage) {\n return (\n \n );\n }\n if (isPdf) {\n return (\n \n );\n }\n if (isZip) {\n return (\n \n );\n }\n return (\n \n );\n}\n\nexport function UploadField() {\n const {\n fieldApi,\n field,\n isDisabled,\n isRequired,\n label,\n placeholder,\n description,\n errors,\n isInvalid,\n descriptionId,\n errorId,\n ariaDescribedBy,\n handleChange,\n } = useDataField();\n\n const ui = field.ui as UploadUi | undefined;\n const hasMany = !!field.hasMany;\n const variant = ui?.variant || \"dropzone\";\n const shape = ui?.shape || \"rounded\";\n const size = ui?.size || \"md\";\n\n const fileInputRef = React.useRef(null);\n const [isDragActive, setIsDragActive] = React.useState(false);\n const [previews, setPreviews] = React.useState<\n Array<{ name: string; size?: number; url?: string }>\n >([]);\n\n const rawValue = fieldApi.state.value;\n\n // Sync value from state to internal previews array\n React.useEffect(() => {\n if (!rawValue) {\n setPreviews([]);\n return;\n }\n\n const items = Array.isArray(rawValue) ? rawValue : [rawValue];\n const newPreviews = items.map((item) => {\n if (typeof window !== \"undefined\" && item instanceof window.File) {\n return {\n name: item.name,\n size: item.size,\n url: item.type.startsWith(\"image/\")\n ? URL.createObjectURL(item)\n : undefined,\n };\n }\n if (typeof item === \"string\") {\n return {\n name: item.split(\"/\").pop() || item,\n url: item,\n };\n }\n return { name: \"Unknown File\" };\n });\n\n setPreviews(newPreviews);\n\n // Cleanup object URLs on unmount/re-sync\n return () => {\n newPreviews.forEach((p) => {\n if (p.url && p.url.startsWith(\"blob:\")) {\n URL.revokeObjectURL(p.url);\n }\n });\n };\n }, [rawValue]);\n\n const onFilesSelected = (fileList: FileList | null) => {\n if (!fileList || fileList.length === 0) return;\n const newFiles = Array.from(fileList);\n\n if (hasMany) {\n const currentFiles = Array.isArray(rawValue)\n ? rawValue\n : rawValue\n ? [rawValue]\n : [];\n handleChange([...currentFiles, ...newFiles]);\n } else {\n handleChange(newFiles[0]);\n }\n };\n\n const removeFile = (index: number) => {\n if (isDisabled) return;\n if (hasMany && Array.isArray(rawValue)) {\n const updated = rawValue.filter((_, i) => i !== index);\n handleChange(updated.length > 0 ? updated : []);\n } else {\n handleChange(null as unknown as File | File[] | string | string[]);\n }\n };\n\n const handleDrag = (e: React.DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n if (isDisabled) return;\n\n if (e.type === \"dragenter\" || e.type === \"dragover\") {\n setIsDragActive(true);\n } else if (e.type === \"dragleave\") {\n setIsDragActive(false);\n }\n };\n\n const handleDrop = (e: React.DragEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setIsDragActive(false);\n if (isDisabled) return;\n\n if (e.dataTransfer.files && e.dataTransfer.files[0]) {\n onFilesSelected(e.dataTransfer.files);\n }\n };\n\n const triggerSelect = () => {\n if (isDisabled) return;\n fileInputRef.current?.click();\n };\n\n const width = ui?.width;\n const widthStyle = width\n ? {\n width: typeof width === \"number\" ? `${width}px` : width,\n flex: \"0 1 auto\",\n }\n : undefined;\n\n // Inline styling calculations\n const dropzoneText =\n ui?.dropzoneText ||\n placeholder ||\n (hasMany\n ? \"Drop files here or click to upload\"\n : \"Drop file here or click to upload\");\n\n return (\n \n \n {label && (\n \n {label}\n {isRequired && ui?.asterisk !== false ? (\n *\n ) : null}\n \n )}\n\n \n
\n onFilesSelected(e.target.files)}\n className=\"hidden\"\n aria-describedby={ariaDescribedBy}\n aria-invalid={isInvalid}\n />\n\n {/* VARIANT: AVATAR */}\n {variant === \"avatar\" && (\n
\n \n {previews[0]?.url ? (\n // eslint-disable-next-line @next/next/no-img-element\n \n ) : (\n
\n \n
\n )}\n\n {/* Hover action overlay */}\n
\n \n Edit\n
\n
\n\n {previews.length > 0 && !isDisabled && (\n removeFile(0)}\n className=\"text-muted-foreground hover:text-destructive h-7 px-2\"\n >\n Remove\n \n )}\n
\n )}\n\n {/* VARIANT: DROPZONE (DEFAULT) */}\n {variant === \"dropzone\" && (\n
\n \n
\n \n
\n
\n

{dropzoneText}

\n

\n {ui?.accept\n ? `Supports: ${ui.accept}`\n : \"All formats supported\"}\n {field.maxSize\n ? ` up to ${formatBytes(field.maxSize)}`\n : \"\"}\n

\n
\n
\n\n {/* Render Selected Files List */}\n {previews.length > 0 && (\n
\n {previews.map((file, idx) => (\n \n \n
\n

{file.name}

\n {file.size && (\n

\n {formatBytes(file.size)}\n

\n )}\n
\n {!isDisabled && (\n removeFile(idx)}\n className=\"size-8 text-muted-foreground hover:text-destructive shrink-0\"\n >\n \n \n )}\n
\n ))}\n \n )}\n \n )}\n\n {/* VARIANT: INLINE */}\n {variant === \"inline\" && (\n
\n
\n \n \n Attach File\n \n \n {previews.length === 0\n ? \"No file selected\"\n : `${previews.length} file(s) attached`}\n \n
\n\n {previews.length > 0 && (\n
\n {previews.map((file, idx) => (\n \n \n {file.name}\n {!isDisabled && (\n removeFile(idx)}\n className=\"size-5 rounded-full hover:bg-muted-foreground/20 flex items-center justify-center text-muted-foreground hover:text-foreground shrink-0\"\n >\n \n \n )}\n
\n ))}\n
\n )}\n \n )}\n\n {/* VARIANT: GALLERY */}\n {variant === \"gallery\" && (\n
\n {previews.map((file, idx) => (\n \n {file.url ? (\n // eslint-disable-next-line @next/next/no-img-element\n \n ) : (\n
\n \n \n {file.name}\n \n
\n )}\n\n {/* Delete overlay */}\n {!isDisabled && (\n
\n removeFile(idx)}\n className=\"size-8\"\n >\n \n \n
\n )}\n
\n ))}\n\n {/* Add Box */}\n {(!hasMany && previews.length === 0) ||\n (hasMany && (!field.max || previews.length < field.max)) ? (\n \n \n \n Add file\n \n \n ) : null}\n \n )}\n \n
\n\n {description && !isInvalid && (\n {description}\n )}\n\n {isInvalid && }\n
\n
\n );\n}\n", "type": "registry:component", "target": "components/buzzform/fields/upload.tsx" } diff --git a/apps/web/registry/shadcn/examples/document-upload-form.tsx b/apps/web/registry/shadcn/examples/document-upload-form.tsx index d096485..abfdecf 100644 --- a/apps/web/registry/shadcn/examples/document-upload-form.tsx +++ b/apps/web/registry/shadcn/examples/document-upload-form.tsx @@ -62,7 +62,7 @@ export default function DocumentUploadForm() { { ...data, document: data.document - ? data.document instanceof File + ? (typeof window !== "undefined" && data.document instanceof window.File) ? { name: data.document.name, size: data.document.size, diff --git a/apps/web/registry/shadcn/examples/gallery-upload-form.tsx b/apps/web/registry/shadcn/examples/gallery-upload-form.tsx index db58bea..87299dc 100644 --- a/apps/web/registry/shadcn/examples/gallery-upload-form.tsx +++ b/apps/web/registry/shadcn/examples/gallery-upload-form.tsx @@ -45,7 +45,7 @@ export default function GalleryUploadForm() { const filesInfo = Array.isArray(data.images) ? data.images.map((img) => - img instanceof File + typeof window !== "undefined" && img instanceof window.File ? { name: img.name, size: img.size, type: img.type } : img, ) diff --git a/apps/web/registry/shadcn/examples/profile-upload-form.tsx b/apps/web/registry/shadcn/examples/profile-upload-form.tsx index 3aaf034..2c7213a 100644 --- a/apps/web/registry/shadcn/examples/profile-upload-form.tsx +++ b/apps/web/registry/shadcn/examples/profile-upload-form.tsx @@ -67,7 +67,7 @@ export default function ProfileUploadForm() { { ...data, avatar: data.avatar - ? data.avatar instanceof File + ? (typeof window !== "undefined" && data.avatar instanceof window.File) ? { name: data.avatar.name, size: data.avatar.size, diff --git a/apps/web/registry/shadcn/fields/upload.tsx b/apps/web/registry/shadcn/fields/upload.tsx index 09541ac..660a244 100644 --- a/apps/web/registry/shadcn/fields/upload.tsx +++ b/apps/web/registry/shadcn/fields/upload.tsx @@ -147,7 +147,7 @@ export function UploadField() { const items = Array.isArray(rawValue) ? rawValue : [rawValue]; const newPreviews = items.map((item) => { - if (item instanceof File) { + if (typeof window !== "undefined" && item instanceof window.File) { return { name: item.name, size: item.size,