@nikala-ui/core 0.10.1-nightly.3fd7ba3 → 0.10.1-nightly.4e09f2a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nikala-ui/core",
3
- "version": "0.10.1-nightly.3fd7ba3",
3
+ "version": "0.10.1-nightly.4e09f2a",
4
4
  "description": "Core component definitions, design tokens, and registry for Nikala UI",
5
5
  "type": "module",
6
6
  "private": false,
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "create-drop-zone",
3
+ "title": "createDropZone",
4
+ "description": "SolidJS reactive primitive for file drag & drop operations, validation, and file chooser dialogs",
5
+ "type": "registry:hook",
6
+ "files": [
7
+ {
8
+ "path": "hooks/create-drop-zone.ts",
9
+ "content": "import { createSignal, onCleanup, onMount, type Accessor } from \"solid-js\";\n\nexport interface FileRejection {\n file: File;\n errors: Array<{\n code: \"file-invalid-type\" | \"file-too-large\" | \"file-too-small\" | \"too-many-files\";\n message: string;\n }>;\n}\n\nexport interface CreateDropZoneOptions {\n /** Accepted file types: MIME types (e.g. \"image/*\", \"application/pdf\") or extensions (e.g. \".png\", \".jpg\") */\n accept?: string | string[];\n /** Maximum number of files allowed */\n maxFiles?: number;\n /** Maximum file size in bytes */\n maxSize?: number;\n /** Minimum file size in bytes */\n minSize?: number;\n /** Whether multiple files are allowed. Defaults to true */\n multiple?: boolean;\n /** Whether the drop zone is disabled */\n disabled?: boolean | Accessor<boolean>;\n /** Prevents browser default behavior of opening files dropped outside dropzone. Defaults to true */\n preventDropOnDocument?: boolean;\n /** Callback fired when valid files are dropped */\n onDrop?: (files: File[], event: DragEvent) => void;\n /** Callback fired when some or all files fail validation */\n onDropRejected?: (rejectedFiles: FileRejection[], event: DragEvent) => void;\n /** Callback fired when drag enters the dropzone */\n onDragEnter?: (event: DragEvent) => void;\n /** Callback fired when drag leaves the dropzone */\n onDragLeave?: (event: DragEvent) => void;\n /** Callback fired when dragging over the dropzone */\n onDragOver?: (event: DragEvent) => void;\n /** Callback fired whenever accepted files list changes */\n onFilesChanged?: (files: File[]) => void;\n}\n\nexport interface CreateDropZoneReturn {\n /** Whether drag operation is currently active over the target drop zone */\n isOver: Accessor<boolean>;\n /** Whether files are currently being dragged anywhere on the window */\n isDragging: Accessor<boolean>;\n /** Currently accepted dropped files */\n files: Accessor<File[]>;\n /** Currently rejected files with error details */\n rejectedFiles: Accessor<FileRejection[]>;\n /** Clear all accepted and rejected files */\n clear: () => void;\n /** Programmatically set files (e.g. from an <input type=\"file\" /> change event) */\n setFiles: (files: File[]) => void;\n /** Programmatically open the native browser file selector dialog */\n openFileDialog: () => void;\n /** Ref callback to attach to target DOM element */\n ref: (el: HTMLElement) => void;\n /** Event handler props to spread directly onto target JSX element */\n props: {\n onDragEnter: (e: DragEvent) => void;\n onDragLeave: (e: DragEvent) => void;\n onDragOver: (e: DragEvent) => void;\n onDrop: (e: DragEvent) => void;\n };\n}\n\nfunction matchesAccept(file: File, acceptList: string[]): boolean {\n if (acceptList.length === 0) return true;\n const fileName = file.name.toLowerCase();\n const fileType = file.type.toLowerCase();\n\n return acceptList.some((pattern) => {\n const p = pattern.trim().toLowerCase();\n if (p.startsWith(\".\")) {\n return fileName.endsWith(p);\n }\n if (p.endsWith(\"/*\")) {\n const typePrefix = p.slice(0, -2);\n return fileType.startsWith(typePrefix + \"/\");\n }\n return fileType === p;\n });\n}\n\nfunction validateFiles(\n incomingFiles: File[],\n options: CreateDropZoneOptions\n): { accepted: File[]; rejected: FileRejection[] } {\n const acceptList = options.accept\n ? (Array.isArray(options.accept) ? options.accept : options.accept.split(\",\"))\n .map((s) => s.trim())\n .filter(Boolean)\n : [];\n\n const maxFiles = options.maxFiles ?? (options.multiple === false ? 1 : Infinity);\n const maxSize = options.maxSize;\n const minSize = options.minSize;\n\n const accepted: File[] = [];\n const rejected: FileRejection[] = [];\n\n incomingFiles.forEach((file, index) => {\n const errors: FileRejection[\"errors\"] = [];\n\n if (index >= maxFiles) {\n errors.push({\n code: \"too-many-files\",\n message: `Maximum allowed files is ${maxFiles}.`,\n });\n }\n\n if (acceptList.length > 0 && !matchesAccept(file, acceptList)) {\n errors.push({\n code: \"file-invalid-type\",\n message: `File type \"${file.type || file.name.split(\".\").pop()}\" is not allowed.`,\n });\n }\n\n if (maxSize !== undefined && file.size > maxSize) {\n errors.push({\n code: \"file-too-large\",\n message: `File size exceeds ${(maxSize / (1024 * 1024)).toFixed(1)}MB limit.`,\n });\n }\n\n if (minSize !== undefined && file.size < minSize) {\n errors.push({\n code: \"file-too-small\",\n message: `File size is below ${(minSize / 1024).toFixed(1)}KB limit.`,\n });\n }\n\n if (errors.length > 0) {\n rejected.push({ file, errors });\n } else {\n accepted.push(file);\n }\n });\n\n return { accepted, rejected };\n}\n\n/**\n * SolidJS reactive primitive for managing file drag & drop zones with validation and file dialog support.\n *\n * @param options Configuration options for file acceptance, size limits, and callbacks.\n */\nexport function createDropZone(options: CreateDropZoneOptions = {}): CreateDropZoneReturn {\n const [isOver, setIsOver] = createSignal(false);\n const [isDragging, setIsDragging] = createSignal(false);\n const [files, setFilesInternal] = createSignal<File[]>([]);\n const [rejectedFiles, setRejectedFilesInternal] = createSignal<FileRejection[]>([]);\n\n let dragCounter = 0;\n let windowDragCounter = 0;\n let targetElement: HTMLElement | null = null;\n\n const isDisabled = () => {\n if (typeof options.disabled === \"function\") {\n return (options.disabled as Accessor<boolean>)();\n }\n return options.disabled ?? false;\n };\n\n const processFiles = (incomingFiles: File[], event: DragEvent) => {\n const { accepted, rejected } = validateFiles(incomingFiles, options);\n\n setFilesInternal(accepted);\n setRejectedFilesInternal(rejected);\n\n if (accepted.length > 0) {\n options.onDrop?.(accepted, event);\n options.onFilesChanged?.(accepted);\n }\n\n if (rejected.length > 0) {\n options.onDropRejected?.(rejected, event);\n }\n };\n\n const onDragEnter = (e: DragEvent) => {\n if (isDisabled()) return;\n e.preventDefault();\n dragCounter++;\n if (dragCounter === 1) {\n setIsOver(true);\n options.onDragEnter?.(e);\n }\n };\n\n const onDragOver = (e: DragEvent) => {\n if (isDisabled()) return;\n e.preventDefault();\n if (e.dataTransfer) {\n e.dataTransfer.dropEffect = \"copy\";\n }\n options.onDragOver?.(e);\n };\n\n const onDragLeave = (e: DragEvent) => {\n if (isDisabled()) return;\n e.preventDefault();\n dragCounter--;\n if (dragCounter <= 0) {\n dragCounter = 0;\n setIsOver(false);\n options.onDragLeave?.(e);\n }\n };\n\n const onDrop = (e: DragEvent) => {\n if (isDisabled()) return;\n e.preventDefault();\n dragCounter = 0;\n setIsOver(false);\n\n if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {\n processFiles(Array.from(e.dataTransfer.files), e);\n }\n };\n\n const clear = () => {\n setFilesInternal([]);\n setRejectedFilesInternal([]);\n options.onFilesChanged?.([]);\n };\n\n const setFiles = (newFiles: File[]) => {\n const dummyEvent = new Event(\"drop\") as unknown as DragEvent;\n processFiles(newFiles, dummyEvent);\n };\n\n const openFileDialog = () => {\n if (typeof document === \"undefined\" || isDisabled()) return;\n const input = document.createElement(\"input\");\n input.type = \"file\";\n if (options.multiple !== false && (options.maxFiles === undefined || options.maxFiles > 1)) {\n input.multiple = true;\n }\n if (options.accept) {\n input.accept = Array.isArray(options.accept) ? options.accept.join(\",\") : options.accept;\n }\n input.onchange = (e) => {\n const target = e.target as HTMLInputElement;\n if (target.files && target.files.length > 0) {\n processFiles(Array.from(target.files), e as unknown as DragEvent);\n }\n };\n input.click();\n };\n\n const ref = (el: HTMLElement) => {\n targetElement = el;\n };\n\n // Window-level drag detection and document drop prevention\n onMount(() => {\n if (typeof window === \"undefined\") return;\n\n const handleWindowDragEnter = (e: DragEvent) => {\n windowDragCounter++;\n if (windowDragCounter === 1) {\n setIsDragging(true);\n }\n };\n\n const handleWindowDragLeave = (e: DragEvent) => {\n windowDragCounter--;\n if (windowDragCounter <= 0) {\n windowDragCounter = 0;\n setIsDragging(false);\n }\n };\n\n const handleWindowDrop = (e: DragEvent) => {\n windowDragCounter = 0;\n setIsDragging(false);\n if (options.preventDropOnDocument !== false) {\n e.preventDefault();\n }\n };\n\n const handleWindowDragOver = (e: DragEvent) => {\n if (options.preventDropOnDocument !== false) {\n e.preventDefault();\n }\n };\n\n window.addEventListener(\"dragenter\", handleWindowDragEnter);\n window.addEventListener(\"dragleave\", handleWindowDragLeave);\n window.addEventListener(\"dragover\", handleWindowDragOver);\n window.addEventListener(\"drop\", handleWindowDrop);\n\n onCleanup(() => {\n window.removeEventListener(\"dragenter\", handleWindowDragEnter);\n window.removeEventListener(\"dragleave\", handleWindowDragLeave);\n window.removeEventListener(\"dragover\", handleWindowDragOver);\n window.removeEventListener(\"drop\", handleWindowDrop);\n });\n });\n\n return {\n isOver,\n isDragging,\n files,\n rejectedFiles,\n clear,\n setFiles,\n openFileDialog,\n ref,\n props: {\n onDragEnter,\n onDragLeave,\n onDragOver,\n onDrop,\n },\n };\n}\n",
10
+ "type": "registry:hook"
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "dropzone",
3
+ "title": "Dropzone",
4
+ "description": "A compound drag-and-drop file upload container with file list previews and validation feedback.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "lucide-solid"
10
+ ],
11
+ "registryDependencies": [
12
+ "create-drop-zone"
13
+ ],
14
+ "files": [
15
+ {
16
+ "path": "ui/dropzone.tsx",
17
+ "content": "import { splitProps, type Component, type JSX } from \"solid-js\";\nimport { cn } from \"@/lib/cn\";\nimport { CloudUpload, FileText, X, AlertCircle } from \"lucide-solid\";\n\nexport interface DropzoneProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n isOver?: boolean;\n disabled?: boolean;\n}\n\n/**\n * Root container for the Dropzone file upload component.\n */\nexport const Dropzone: Component<DropzoneProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"isOver\", \"disabled\"]);\n\n return (\n <div\n class={cn(\n \"group relative flex w-full flex-col items-center justify-center rounded-lg border-2 border-dashed border-border bg-card/50 p-8 text-center transition-all\",\n \"hover:border-primary/50 hover:bg-card/80\",\n local.isOver && \"border-primary bg-primary/5 ring-2 ring-primary/20\",\n local.disabled && \"pointer-events-none opacity-50\",\n local.class\n )}\n {...rest}\n />\n );\n};\n\nexport interface DropzoneIconProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\n/**\n * Centered icon placeholder for Dropzone.\n */\nexport const DropzoneIcon: Component<DropzoneIconProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\n \"mb-3 flex size-12 items-center justify-center rounded-lg bg-primary/10 text-primary transition-transform group-hover:scale-105\",\n local.class\n )}\n {...rest}\n >\n {local.children || <CloudUpload class=\"size-6\" />}\n </div>\n );\n};\n\nexport interface DropzoneTitleProps extends JSX.HTMLAttributes<HTMLHeadingElement> {\n class?: string;\n}\n\n/**\n * Primary title text for the dropzone prompt.\n */\nexport const DropzoneTitle: Component<DropzoneTitleProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <h4\n class={cn(\"text-sm font-semibold tracking-tight text-foreground\", local.class)}\n {...rest}\n />\n );\n};\n\nexport interface DropzoneDescriptionProps extends JSX.HTMLAttributes<HTMLParagraphElement> {\n class?: string;\n}\n\n/**\n * Subtitle description text for dropzone file specifications.\n */\nexport const DropzoneDescription: Component<DropzoneDescriptionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <p\n class={cn(\"mt-1 text-xs text-muted-foreground\", local.class)}\n {...rest}\n />\n );\n};\n\nexport interface DropzoneFileListProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\n/**\n * Container list for uploaded files.\n */\nexport const DropzoneFileList: Component<DropzoneFileListProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <div\n class={cn(\"mt-4 flex w-full flex-col gap-2\", local.class)}\n {...rest}\n />\n );\n};\n\nexport interface DropzoneFileItemProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n name: string;\n size?: string;\n onRemove?: () => void;\n}\n\n/**\n * Individual uploaded file card with name, formatted size, and remove button.\n */\nexport const DropzoneFileItem: Component<DropzoneFileItemProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"name\", \"size\", \"onRemove\", \"children\"]);\n\n return (\n <div\n class={cn(\n \"flex items-center justify-between gap-3 rounded-md border border-border bg-card p-2.5 text-xs transition-colors\",\n local.class\n )}\n {...rest}\n >\n <div class=\"flex min-w-0 items-center gap-2.5\">\n <div class=\"flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground\">\n {local.children || <FileText class=\"size-4\" />}\n </div>\n <div class=\"flex min-w-0 flex-col text-left\">\n <span class=\"truncate font-medium text-foreground\">{local.name}</span>\n {local.size && (\n <span class=\"font-mono text-[11px] text-muted-foreground\">{local.size}</span>\n )}\n </div>\n </div>\n\n {local.onRemove && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n local.onRemove?.();\n }}\n class=\"flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive\"\n aria-label=\"Remove file\"\n >\n <X class=\"size-3.5\" />\n </button>\n )}\n </div>\n );\n};\n\nexport interface DropzoneRejectedItemProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n name: string;\n error?: string;\n size?: string;\n}\n\n/**\n * Card for displaying rejected files and validation errors.\n */\nexport const DropzoneRejectedItem: Component<DropzoneRejectedItemProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"name\", \"error\", \"size\"]);\n\n return (\n <div\n class={cn(\n \"flex items-center justify-between gap-3 rounded-md border border-destructive/30 bg-destructive/10 p-2.5 text-xs text-destructive\",\n local.class\n )}\n {...rest}\n >\n <div class=\"flex min-w-0 items-center gap-2\">\n <AlertCircle class=\"size-4 shrink-0\" />\n <span class=\"truncate font-medium\">{local.name}</span>\n {local.size && <span class=\"font-mono text-[11px] opacity-80\">({local.size})</span>}\n </div>\n {local.error && (\n <span class=\"shrink-0 text-[11px] font-medium\">{local.error}</span>\n )}\n </div>\n );\n};\n",
18
+ "type": "registry:ui"
19
+ }
20
+ ]
21
+ }
@@ -210,6 +210,20 @@
210
210
  "scroll-area"
211
211
  ]
212
212
  },
213
+ {
214
+ "name": "dropzone",
215
+ "title": "Dropzone",
216
+ "description": "A compound drag-and-drop file upload container with file list previews and validation feedback.",
217
+ "type": "registry:ui",
218
+ "dependencies": [
219
+ "clsx",
220
+ "tailwind-merge",
221
+ "lucide-solid"
222
+ ],
223
+ "registryDependencies": [
224
+ "create-drop-zone"
225
+ ]
226
+ },
213
227
  {
214
228
  "name": "empty",
215
229
  "title": "Empty",
@@ -526,6 +540,16 @@
526
540
  "tailwind-merge"
527
541
  ]
528
542
  },
543
+ {
544
+ "name": "table",
545
+ "title": "Table",
546
+ "description": "A responsive and accessible data table component with headers, rows, cells, and footer summaries.",
547
+ "type": "registry:ui",
548
+ "dependencies": [
549
+ "clsx",
550
+ "tailwind-merge"
551
+ ]
552
+ },
529
553
  {
530
554
  "name": "tabs",
531
555
  "title": "Tabs",
@@ -836,5 +860,11 @@
836
860
  "title": "createScrollIntoView",
837
861
  "description": "SolidJS reactive primitive for scrolling a target element into view smooth or auto behavior",
838
862
  "type": "registry:hook"
863
+ },
864
+ {
865
+ "name": "create-drop-zone",
866
+ "title": "createDropZone",
867
+ "description": "SolidJS reactive primitive for file drag & drop operations, validation, and file chooser dialogs",
868
+ "type": "registry:hook"
839
869
  }
840
870
  ]
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "table",
3
+ "title": "Table",
4
+ "description": "A responsive and accessible data table component with headers, rows, cells, and footer summaries.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "ui/table.tsx",
13
+ "content": "import { splitProps, type Component, type JSX } from \"solid-js\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface TableProps extends JSX.HTMLAttributes<HTMLTableElement> {\n class?: string;\n}\n\n/**\n * Root Table container component wrapped in a responsive scroll container.\n */\nexport const Table: Component<TableProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <div class=\"relative w-full overflow-auto\">\n <table\n class={cn(\"w-full caption-bottom text-sm\", local.class)}\n {...rest}\n />\n </div>\n );\n};\n\nexport interface TableHeaderProps extends JSX.HTMLAttributes<HTMLTableSectionElement> {\n class?: string;\n}\n\n/**\n * Header section wrapper for the Table component.\n */\nexport const TableHeader: Component<TableHeaderProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <thead\n class={cn(\"[&_tr]:border-b border-border\", local.class)}\n {...rest}\n />\n );\n};\n\nexport interface TableBodyProps extends JSX.HTMLAttributes<HTMLTableSectionElement> {\n class?: string;\n}\n\n/**\n * Main body section wrapper for the Table component.\n */\nexport const TableBody: Component<TableBodyProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <tbody\n class={cn(\"[&_tr:last-child]:border-0\", local.class)}\n {...rest}\n />\n );\n};\n\nexport interface TableFooterProps extends JSX.HTMLAttributes<HTMLTableSectionElement> {\n class?: string;\n}\n\n/**\n * Footer section wrapper for the Table component.\n */\nexport const TableFooter: Component<TableFooterProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <tfoot\n class={cn(\n \"border-t border-border bg-muted/50 font-medium [&>tr]:last:border-b-0\",\n local.class\n )}\n {...rest}\n />\n );\n};\n\nexport interface TableRowProps extends JSX.HTMLAttributes<HTMLTableRowElement> {\n class?: string;\n}\n\n/**\n * Table row component with hover highlight states.\n */\nexport const TableRow: Component<TableRowProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <tr\n class={cn(\n \"border-b border-border transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted\",\n local.class\n )}\n {...rest}\n />\n );\n};\n\nexport interface TableHeadProps extends JSX.ThHTMLAttributes<HTMLTableCellElement> {\n class?: string;\n}\n\n/**\n * Header cell component for table columns.\n */\nexport const TableHead: Component<TableHeadProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <th\n class={cn(\n \"h-10 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n local.class\n )}\n {...rest}\n />\n );\n};\n\nexport interface TableCellProps extends JSX.TdHTMLAttributes<HTMLTableCellElement> {\n class?: string;\n}\n\n/**\n * Standard data cell component for table rows.\n */\nexport const TableCell: Component<TableCellProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <td\n class={cn(\n \"p-4 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n local.class\n )}\n {...rest}\n />\n );\n};\n\nexport interface TableCaptionProps extends JSX.HTMLAttributes<HTMLTableCaptionElement> {\n class?: string;\n}\n\n/**\n * Accessible table caption for describing table contents.\n */\nexport const TableCaption: Component<TableCaptionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <caption\n class={cn(\"mt-4 text-xs text-muted-foreground pb-2\", local.class)}\n {...rest}\n />\n );\n};\n",
14
+ "type": "registry:ui"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,189 @@
1
+ import { splitProps, type Component, type JSX } from "solid-js";
2
+ import { cn } from "@/lib/cn";
3
+ import { CloudUpload, FileText, X, AlertCircle } from "lucide-solid";
4
+
5
+ export interface DropzoneProps extends JSX.HTMLAttributes<HTMLDivElement> {
6
+ class?: string;
7
+ isOver?: boolean;
8
+ disabled?: boolean;
9
+ }
10
+
11
+ /**
12
+ * Root container for the Dropzone file upload component.
13
+ */
14
+ export const Dropzone: Component<DropzoneProps> = (props) => {
15
+ const [local, rest] = splitProps(props, ["class", "isOver", "disabled"]);
16
+
17
+ return (
18
+ <div
19
+ class={cn(
20
+ "group relative flex w-full flex-col items-center justify-center rounded-lg border-2 border-dashed border-border bg-card/50 p-8 text-center transition-all",
21
+ "hover:border-primary/50 hover:bg-card/80",
22
+ local.isOver && "border-primary bg-primary/5 ring-2 ring-primary/20",
23
+ local.disabled && "pointer-events-none opacity-50",
24
+ local.class
25
+ )}
26
+ {...rest}
27
+ />
28
+ );
29
+ };
30
+
31
+ export interface DropzoneIconProps extends JSX.HTMLAttributes<HTMLDivElement> {
32
+ class?: string;
33
+ }
34
+
35
+ /**
36
+ * Centered icon placeholder for Dropzone.
37
+ */
38
+ export const DropzoneIcon: Component<DropzoneIconProps> = (props) => {
39
+ const [local, rest] = splitProps(props, ["class", "children"]);
40
+
41
+ return (
42
+ <div
43
+ class={cn(
44
+ "mb-3 flex size-12 items-center justify-center rounded-lg bg-primary/10 text-primary transition-transform group-hover:scale-105",
45
+ local.class
46
+ )}
47
+ {...rest}
48
+ >
49
+ {local.children || <CloudUpload class="size-6" />}
50
+ </div>
51
+ );
52
+ };
53
+
54
+ export interface DropzoneTitleProps extends JSX.HTMLAttributes<HTMLHeadingElement> {
55
+ class?: string;
56
+ }
57
+
58
+ /**
59
+ * Primary title text for the dropzone prompt.
60
+ */
61
+ export const DropzoneTitle: Component<DropzoneTitleProps> = (props) => {
62
+ const [local, rest] = splitProps(props, ["class"]);
63
+
64
+ return (
65
+ <h4
66
+ class={cn("text-sm font-semibold tracking-tight text-foreground", local.class)}
67
+ {...rest}
68
+ />
69
+ );
70
+ };
71
+
72
+ export interface DropzoneDescriptionProps extends JSX.HTMLAttributes<HTMLParagraphElement> {
73
+ class?: string;
74
+ }
75
+
76
+ /**
77
+ * Subtitle description text for dropzone file specifications.
78
+ */
79
+ export const DropzoneDescription: Component<DropzoneDescriptionProps> = (props) => {
80
+ const [local, rest] = splitProps(props, ["class"]);
81
+
82
+ return (
83
+ <p
84
+ class={cn("mt-1 text-xs text-muted-foreground", local.class)}
85
+ {...rest}
86
+ />
87
+ );
88
+ };
89
+
90
+ export interface DropzoneFileListProps extends JSX.HTMLAttributes<HTMLDivElement> {
91
+ class?: string;
92
+ }
93
+
94
+ /**
95
+ * Container list for uploaded files.
96
+ */
97
+ export const DropzoneFileList: Component<DropzoneFileListProps> = (props) => {
98
+ const [local, rest] = splitProps(props, ["class"]);
99
+
100
+ return (
101
+ <div
102
+ class={cn("mt-4 flex w-full flex-col gap-2", local.class)}
103
+ {...rest}
104
+ />
105
+ );
106
+ };
107
+
108
+ export interface DropzoneFileItemProps extends JSX.HTMLAttributes<HTMLDivElement> {
109
+ class?: string;
110
+ name: string;
111
+ size?: string;
112
+ onRemove?: () => void;
113
+ }
114
+
115
+ /**
116
+ * Individual uploaded file card with name, formatted size, and remove button.
117
+ */
118
+ export const DropzoneFileItem: Component<DropzoneFileItemProps> = (props) => {
119
+ const [local, rest] = splitProps(props, ["class", "name", "size", "onRemove", "children"]);
120
+
121
+ return (
122
+ <div
123
+ class={cn(
124
+ "flex items-center justify-between gap-3 rounded-md border border-border bg-card p-2.5 text-xs transition-colors",
125
+ local.class
126
+ )}
127
+ {...rest}
128
+ >
129
+ <div class="flex min-w-0 items-center gap-2.5">
130
+ <div class="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
131
+ {local.children || <FileText class="size-4" />}
132
+ </div>
133
+ <div class="flex min-w-0 flex-col text-left">
134
+ <span class="truncate font-medium text-foreground">{local.name}</span>
135
+ {local.size && (
136
+ <span class="font-mono text-[11px] text-muted-foreground">{local.size}</span>
137
+ )}
138
+ </div>
139
+ </div>
140
+
141
+ {local.onRemove && (
142
+ <button
143
+ type="button"
144
+ onClick={(e) => {
145
+ e.stopPropagation();
146
+ local.onRemove?.();
147
+ }}
148
+ class="flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
149
+ aria-label="Remove file"
150
+ >
151
+ <X class="size-3.5" />
152
+ </button>
153
+ )}
154
+ </div>
155
+ );
156
+ };
157
+
158
+ export interface DropzoneRejectedItemProps extends JSX.HTMLAttributes<HTMLDivElement> {
159
+ class?: string;
160
+ name: string;
161
+ error?: string;
162
+ size?: string;
163
+ }
164
+
165
+ /**
166
+ * Card for displaying rejected files and validation errors.
167
+ */
168
+ export const DropzoneRejectedItem: Component<DropzoneRejectedItemProps> = (props) => {
169
+ const [local, rest] = splitProps(props, ["class", "name", "error", "size"]);
170
+
171
+ return (
172
+ <div
173
+ class={cn(
174
+ "flex items-center justify-between gap-3 rounded-md border border-destructive/30 bg-destructive/10 p-2.5 text-xs text-destructive",
175
+ local.class
176
+ )}
177
+ {...rest}
178
+ >
179
+ <div class="flex min-w-0 items-center gap-2">
180
+ <AlertCircle class="size-4 shrink-0" />
181
+ <span class="truncate font-medium">{local.name}</span>
182
+ {local.size && <span class="font-mono text-[11px] opacity-80">({local.size})</span>}
183
+ </div>
184
+ {local.error && (
185
+ <span class="shrink-0 text-[11px] font-medium">{local.error}</span>
186
+ )}
187
+ </div>
188
+ );
189
+ };
@@ -0,0 +1,160 @@
1
+ import { splitProps, type Component, type JSX } from "solid-js";
2
+ import { cn } from "@/lib/cn";
3
+
4
+ export interface TableProps extends JSX.HTMLAttributes<HTMLTableElement> {
5
+ class?: string;
6
+ }
7
+
8
+ /**
9
+ * Root Table container component wrapped in a responsive scroll container.
10
+ */
11
+ export const Table: Component<TableProps> = (props) => {
12
+ const [local, rest] = splitProps(props, ["class"]);
13
+
14
+ return (
15
+ <div class="relative w-full overflow-auto">
16
+ <table
17
+ class={cn("w-full caption-bottom text-sm", local.class)}
18
+ {...rest}
19
+ />
20
+ </div>
21
+ );
22
+ };
23
+
24
+ export interface TableHeaderProps extends JSX.HTMLAttributes<HTMLTableSectionElement> {
25
+ class?: string;
26
+ }
27
+
28
+ /**
29
+ * Header section wrapper for the Table component.
30
+ */
31
+ export const TableHeader: Component<TableHeaderProps> = (props) => {
32
+ const [local, rest] = splitProps(props, ["class"]);
33
+
34
+ return (
35
+ <thead
36
+ class={cn("[&_tr]:border-b border-border", local.class)}
37
+ {...rest}
38
+ />
39
+ );
40
+ };
41
+
42
+ export interface TableBodyProps extends JSX.HTMLAttributes<HTMLTableSectionElement> {
43
+ class?: string;
44
+ }
45
+
46
+ /**
47
+ * Main body section wrapper for the Table component.
48
+ */
49
+ export const TableBody: Component<TableBodyProps> = (props) => {
50
+ const [local, rest] = splitProps(props, ["class"]);
51
+
52
+ return (
53
+ <tbody
54
+ class={cn("[&_tr:last-child]:border-0", local.class)}
55
+ {...rest}
56
+ />
57
+ );
58
+ };
59
+
60
+ export interface TableFooterProps extends JSX.HTMLAttributes<HTMLTableSectionElement> {
61
+ class?: string;
62
+ }
63
+
64
+ /**
65
+ * Footer section wrapper for the Table component.
66
+ */
67
+ export const TableFooter: Component<TableFooterProps> = (props) => {
68
+ const [local, rest] = splitProps(props, ["class"]);
69
+
70
+ return (
71
+ <tfoot
72
+ class={cn(
73
+ "border-t border-border bg-muted/50 font-medium [&>tr]:last:border-b-0",
74
+ local.class
75
+ )}
76
+ {...rest}
77
+ />
78
+ );
79
+ };
80
+
81
+ export interface TableRowProps extends JSX.HTMLAttributes<HTMLTableRowElement> {
82
+ class?: string;
83
+ }
84
+
85
+ /**
86
+ * Table row component with hover highlight states.
87
+ */
88
+ export const TableRow: Component<TableRowProps> = (props) => {
89
+ const [local, rest] = splitProps(props, ["class"]);
90
+
91
+ return (
92
+ <tr
93
+ class={cn(
94
+ "border-b border-border transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
95
+ local.class
96
+ )}
97
+ {...rest}
98
+ />
99
+ );
100
+ };
101
+
102
+ export interface TableHeadProps extends JSX.ThHTMLAttributes<HTMLTableCellElement> {
103
+ class?: string;
104
+ }
105
+
106
+ /**
107
+ * Header cell component for table columns.
108
+ */
109
+ export const TableHead: Component<TableHeadProps> = (props) => {
110
+ const [local, rest] = splitProps(props, ["class"]);
111
+
112
+ return (
113
+ <th
114
+ class={cn(
115
+ "h-10 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
116
+ local.class
117
+ )}
118
+ {...rest}
119
+ />
120
+ );
121
+ };
122
+
123
+ export interface TableCellProps extends JSX.TdHTMLAttributes<HTMLTableCellElement> {
124
+ class?: string;
125
+ }
126
+
127
+ /**
128
+ * Standard data cell component for table rows.
129
+ */
130
+ export const TableCell: Component<TableCellProps> = (props) => {
131
+ const [local, rest] = splitProps(props, ["class"]);
132
+
133
+ return (
134
+ <td
135
+ class={cn(
136
+ "p-4 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
137
+ local.class
138
+ )}
139
+ {...rest}
140
+ />
141
+ );
142
+ };
143
+
144
+ export interface TableCaptionProps extends JSX.HTMLAttributes<HTMLTableCaptionElement> {
145
+ class?: string;
146
+ }
147
+
148
+ /**
149
+ * Accessible table caption for describing table contents.
150
+ */
151
+ export const TableCaption: Component<TableCaptionProps> = (props) => {
152
+ const [local, rest] = splitProps(props, ["class"]);
153
+
154
+ return (
155
+ <caption
156
+ class={cn("mt-4 text-xs text-muted-foreground pb-2", local.class)}
157
+ {...rest}
158
+ />
159
+ );
160
+ };
@@ -277,6 +277,17 @@ export const COMPONENT_METADATA: Record<string, ComponentMeta> = {
277
277
  description: "Profile and link preview popover triggered on hover, built on Kobalte primitives.",
278
278
  dependencies: ["clsx", "tailwind-merge", "@kobalte/core"],
279
279
  },
280
+ table: {
281
+ title: "Table",
282
+ description: "A responsive and accessible data table component with headers, rows, cells, and footer summaries.",
283
+ dependencies: ["clsx", "tailwind-merge"],
284
+ },
285
+ dropzone: {
286
+ title: "Dropzone",
287
+ description: "A compound drag-and-drop file upload container with file list previews and validation feedback.",
288
+ dependencies: ["clsx", "tailwind-merge", "lucide-solid"],
289
+ registryDependencies: ["create-drop-zone"],
290
+ },
280
291
  };
281
292
 
282
293
  /**
@@ -443,4 +454,8 @@ export const HOOK_METADATA: Record<string, ComponentMeta> = {
443
454
  title: "createScrollIntoView",
444
455
  description: "SolidJS reactive primitive for scrolling a target element into view smooth or auto behavior",
445
456
  },
457
+ "create-drop-zone": {
458
+ title: "createDropZone",
459
+ description: "SolidJS reactive primitive for file drag & drop operations, validation, and file chooser dialogs",
460
+ },
446
461
  };