@nikala-ui/core 0.10.0 → 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.0",
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,20 @@
1
+ {
2
+ "name": "button-group",
3
+ "title": "Button Group",
4
+ "description": "Groups related buttons into a connected horizontal or vertical control.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge"
9
+ ],
10
+ "registryDependencies": [
11
+ "button"
12
+ ],
13
+ "files": [
14
+ {
15
+ "path": "ui/button-group.tsx",
16
+ "content": "import { splitProps, type Component, type JSX } from \"solid-js\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface ButtonGroupProps extends JSX.HTMLAttributes<HTMLDivElement> {\n /** Controls whether grouped buttons are arranged in a row or column. */\n orientation?: \"horizontal\" | \"vertical\";\n class?: string;\n}\n\n/**\n * Groups adjacent buttons into a connected control with shared borders and radii.\n *\n * ButtonGroup is intentionally presentational. Use Button for individual actions\n * and compose the group with the same reactive state as the surrounding feature.\n */\nexport const ButtonGroup: Component<ButtonGroupProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"orientation\",\n \"class\",\n \"children\",\n ]);\n\n const orientation = () => local.orientation ?? \"horizontal\";\n\n return (\n <div\n role=\"group\"\n data-orientation={orientation()}\n class={cn(\n \"isolate inline-flex\",\n orientation() === \"horizontal\"\n ? \"flex-row [&>button:not(:first-child)]:-ml-px [&>button:not(:first-child)]:rounded-l-none [&>button:not(:last-child)]:rounded-r-none\"\n : \"flex-col [&>button:not(:first-child)]:-mt-px [&>button:not(:first-child)]:rounded-t-none [&>button:not(:last-child)]:rounded-b-none\",\n \"[&>button:focus-visible]:z-10\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n",
17
+ "type": "registry:ui"
18
+ }
19
+ ]
20
+ }
@@ -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
+ }
@@ -74,6 +74,19 @@
74
74
  "tailwind-merge"
75
75
  ]
76
76
  },
77
+ {
78
+ "name": "button-group",
79
+ "title": "Button Group",
80
+ "description": "Groups related buttons into a connected horizontal or vertical control.",
81
+ "type": "registry:ui",
82
+ "dependencies": [
83
+ "clsx",
84
+ "tailwind-merge"
85
+ ],
86
+ "registryDependencies": [
87
+ "button"
88
+ ]
89
+ },
77
90
  {
78
91
  "name": "button",
79
92
  "title": "Button",
@@ -197,6 +210,20 @@
197
210
  "scroll-area"
198
211
  ]
199
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
+ },
200
227
  {
201
228
  "name": "empty",
202
229
  "title": "Empty",
@@ -513,6 +540,16 @@
513
540
  "tailwind-merge"
514
541
  ]
515
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
+ },
516
553
  {
517
554
  "name": "tabs",
518
555
  "title": "Tabs",
@@ -823,5 +860,11 @@
823
860
  "title": "createScrollIntoView",
824
861
  "description": "SolidJS reactive primitive for scrolling a target element into view smooth or auto behavior",
825
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"
826
869
  }
827
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,42 @@
1
+ import { splitProps, type Component, type JSX } from "solid-js";
2
+ import { cn } from "@/lib/cn";
3
+
4
+ export interface ButtonGroupProps extends JSX.HTMLAttributes<HTMLDivElement> {
5
+ /** Controls whether grouped buttons are arranged in a row or column. */
6
+ orientation?: "horizontal" | "vertical";
7
+ class?: string;
8
+ }
9
+
10
+ /**
11
+ * Groups adjacent buttons into a connected control with shared borders and radii.
12
+ *
13
+ * ButtonGroup is intentionally presentational. Use Button for individual actions
14
+ * and compose the group with the same reactive state as the surrounding feature.
15
+ */
16
+ export const ButtonGroup: Component<ButtonGroupProps> = (props) => {
17
+ const [local, rest] = splitProps(props, [
18
+ "orientation",
19
+ "class",
20
+ "children",
21
+ ]);
22
+
23
+ const orientation = () => local.orientation ?? "horizontal";
24
+
25
+ return (
26
+ <div
27
+ role="group"
28
+ data-orientation={orientation()}
29
+ class={cn(
30
+ "isolate inline-flex",
31
+ orientation() === "horizontal"
32
+ ? "flex-row [&>button:not(:first-child)]:-ml-px [&>button:not(:first-child)]:rounded-l-none [&>button:not(:last-child)]:rounded-r-none"
33
+ : "flex-col [&>button:not(:first-child)]:-mt-px [&>button:not(:first-child)]:rounded-t-none [&>button:not(:last-child)]:rounded-b-none",
34
+ "[&>button:focus-visible]:z-10",
35
+ local.class
36
+ )}
37
+ {...rest}
38
+ >
39
+ {local.children}
40
+ </div>
41
+ );
42
+ };
@@ -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
+ };
@@ -16,6 +16,12 @@ export const COMPONENT_METADATA: Record<string, ComponentMeta> = {
16
16
  dependencies: ["clsx", "tailwind-merge", "class-variance-authority"],
17
17
  registryDependencies: ["spinner"],
18
18
  },
19
+ "button-group": {
20
+ title: "Button Group",
21
+ description: "Groups related buttons into a connected horizontal or vertical control.",
22
+ dependencies: ["clsx", "tailwind-merge"],
23
+ registryDependencies: ["button"],
24
+ },
19
25
  input: {
20
26
  title: "Input",
21
27
  description: "A standard text input field with styling variants.",
@@ -271,6 +277,17 @@ export const COMPONENT_METADATA: Record<string, ComponentMeta> = {
271
277
  description: "Profile and link preview popover triggered on hover, built on Kobalte primitives.",
272
278
  dependencies: ["clsx", "tailwind-merge", "@kobalte/core"],
273
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
+ },
274
291
  };
275
292
 
276
293
  /**
@@ -437,4 +454,8 @@ export const HOOK_METADATA: Record<string, ComponentMeta> = {
437
454
  title: "createScrollIntoView",
438
455
  description: "SolidJS reactive primitive for scrolling a target element into view smooth or auto behavior",
439
456
  },
457
+ "create-drop-zone": {
458
+ title: "createDropZone",
459
+ description: "SolidJS reactive primitive for file drag & drop operations, validation, and file chooser dialogs",
460
+ },
440
461
  };