@nikala-ui/core 0.10.1 → 0.11.0
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 +1 -1
- package/registry/bubble.json +18 -0
- package/registry/button-group.json +20 -0
- package/registry/create-chat-scroll.json +13 -0
- package/registry/create-drop-zone.json +13 -0
- package/registry/create-pagination.json +13 -0
- package/registry/dropzone.json +21 -0
- package/registry/footer.json +18 -0
- package/registry/forgot-password-01.json +28 -0
- package/registry/hero-01.json +22 -0
- package/registry/index.json +331 -0
- package/registry/login-01.json +28 -0
- package/registry/marker.json +17 -0
- package/registry/marquee.json +17 -0
- package/registry/message.json +17 -0
- package/registry/navbar.json +19 -0
- package/registry/navigation-menu.json +22 -0
- package/registry/otp-verification-01.json +26 -0
- package/registry/pagination.json +22 -0
- package/registry/rating.json +19 -0
- package/registry/register-01.json +31 -0
- package/registry/review-card.json +23 -0
- package/registry/scroll-area.json +1 -1
- package/registry/sidebar.json +24 -0
- package/registry/spinner.json +1 -1
- package/registry/stat.json +19 -0
- package/registry/table.json +17 -0
- package/registry/timeline.json +18 -0
- package/registry/toggle-group.json +21 -0
- package/src/registry/blocks/forgot-password-01.tsx +141 -0
- package/src/registry/blocks/hero-01.tsx +28 -0
- package/src/registry/blocks/login-01.tsx +231 -0
- package/src/registry/blocks/otp-verification-01.tsx +170 -0
- package/src/registry/blocks/register-01.tsx +318 -0
- package/src/registry/components/ui/bubble.tsx +142 -0
- package/src/registry/components/ui/button-group.tsx +42 -0
- package/src/registry/components/ui/dropzone.tsx +189 -0
- package/src/registry/components/ui/footer.tsx +247 -0
- package/src/registry/components/ui/marker.tsx +102 -0
- package/src/registry/components/ui/marquee.tsx +118 -0
- package/src/registry/components/ui/message.tsx +168 -0
- package/src/registry/components/ui/navbar.tsx +368 -0
- package/src/registry/components/ui/navigation-menu.tsx +358 -0
- package/src/registry/components/ui/pagination.tsx +258 -0
- package/src/registry/components/ui/rating.tsx +185 -0
- package/src/registry/components/ui/review-card.tsx +195 -0
- package/src/registry/components/ui/scroll-area.tsx +2 -2
- package/src/registry/components/ui/sidebar.tsx +692 -0
- package/src/registry/components/ui/spinner.tsx +1 -1
- package/src/registry/components/ui/stat.tsx +245 -0
- package/src/registry/components/ui/table.tsx +160 -0
- package/src/registry/components/ui/timeline.tsx +350 -0
- package/src/registry/components/ui/toggle-group.tsx +203 -0
- package/src/registry/index.ts +3 -3
- package/src/registry/metadata.ts +141 -0
package/package.json
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bubble",
|
|
3
|
+
"title": "Bubble",
|
|
4
|
+
"description": "Chat message bubble container supporting variants, grouped consecutive bubbles, and emoji reactions.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge",
|
|
9
|
+
"class-variance-authority"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "ui/bubble.tsx",
|
|
14
|
+
"content": "import {\n splitProps,\n type Component,\n type JSX,\n type ParentComponent,\n useContext,\n} from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\n/* --- 1. Bubble Variants --- */\nexport const bubbleVariants = cva(\n \"relative max-w-full rounded-lg transition-colors break-words text-left select-text\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground shadow-2xs\",\n muted: \"bg-muted text-foreground border border-border/50 shadow-2xs\",\n outline: \"border border-border bg-background text-foreground shadow-2xs\",\n ghost: \"bg-transparent text-foreground\",\n },\n size: {\n sm: \"px-3 py-1.5 text-xs\",\n default: \"px-4 py-2.5 text-sm\",\n lg: \"px-5 py-3 text-base\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n);\n\n/* --- 2. BubbleGroup --- */\nexport interface BubbleGroupProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const BubbleGroup: ParentComponent<BubbleGroupProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\"flex flex-col gap-1 w-full\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 3. Bubble --- */\nexport interface BubbleProps\n extends JSX.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof bubbleVariants> {\n class?: string;\n}\n\nexport const Bubble: ParentComponent<BubbleProps> = (props) => {\n const [local, rest] = splitProps(props, [\"variant\", \"size\", \"class\", \"children\"]);\n\n return (\n <div\n class={cn(\n bubbleVariants({\n variant: local.variant,\n size: local.size,\n }),\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 4. BubbleContent --- */\nexport interface BubbleContentProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const BubbleContent: ParentComponent<BubbleContentProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\"leading-relaxed\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 5. BubbleReactions --- */\nexport interface BubbleReactionsProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const BubbleReactions: ParentComponent<BubbleReactionsProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\"flex items-center gap-1.5 pt-2 mt-2 border-t border-border/30 z-10 flex-wrap\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 6. BubbleReaction --- */\nexport interface BubbleReactionProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {\n active?: boolean;\n count?: number;\n class?: string;\n}\n\nexport const BubbleReaction: ParentComponent<BubbleReactionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"active\", \"count\", \"class\", \"children\"]);\n\n return (\n <button\n type=\"button\"\n data-active={local.active ? \"true\" : \"false\"}\n class={cn(\n \"inline-flex items-center gap-1.5 rounded-md border border-border/70 bg-background/80 px-2 py-1 text-xs text-foreground shadow-2xs transition-all hover:bg-accent hover:border-primary/40 cursor-pointer select-none\",\n local.active && \"border-primary/40 bg-primary/10 text-primary font-medium\",\n local.class\n )}\n {...rest}\n >\n <span class=\"text-xs leading-none\">{local.children}</span>\n {local.count !== undefined && (\n <span class=\"text-[11px] font-mono leading-none text-muted-foreground\">{local.count}</span>\n )}\n </button>\n );\n};\n",
|
|
15
|
+
"type": "registry:ui"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -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-chat-scroll",
|
|
3
|
+
"title": "createChatScroll",
|
|
4
|
+
"description": "SolidJS reactive primitive for automated chat container scrolling with manual scroll-up detection",
|
|
5
|
+
"type": "registry:hook",
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"path": "hooks/create-chat-scroll.ts",
|
|
9
|
+
"content": "import { createSignal, createEffect, onCleanup, onMount, type Accessor } from \"solid-js\";\n\nexport interface CreateChatScrollOptions {\n /** Target scrollable container element or accessor */\n target: HTMLElement | Accessor<HTMLElement | undefined>;\n /** Dependency accessor (e.g. messages length or content signal) that triggers auto-scroll when changed */\n trigger?: Accessor<any>;\n /** Threshold in pixels from bottom to consider the container \"at bottom\". Defaults to 40. */\n threshold?: number;\n /** Whether auto-scroll is enabled. Defaults to true. */\n enabled?: boolean | Accessor<boolean>;\n /** Scroll behavior: \"smooth\" or \"auto\". Defaults to \"smooth\". */\n behavior?: ScrollBehavior;\n}\n\nexport interface CreateChatScrollReturn {\n /** Accessor indicating whether the container is currently scrolled to the bottom */\n isAtBottom: Accessor<boolean>;\n /** Accessor indicating whether user has manually scrolled up away from bottom */\n isScrolledUp: Accessor<boolean>;\n /** Programmatically scroll container directly to the bottom */\n scrollToBottom: (options?: { smooth?: boolean }) => void;\n}\n\n/**\n * SolidJS reactive primitive for chat and streaming message auto-scrolling with user scroll detection.\n *\n * @param options Chat scroll configuration options.\n */\nexport function createChatScroll(options: CreateChatScrollOptions): CreateChatScrollReturn {\n const [isAtBottom, setIsAtBottom] = createSignal<boolean>(true);\n const isScrolledUp = () => !isAtBottom();\n\n const getElement = (): HTMLElement | undefined => {\n if (typeof options.target === \"function\") {\n return (options.target as Accessor<HTMLElement | undefined>)();\n }\n return options.target;\n };\n\n const isEnabled = () => {\n if (typeof options.enabled === \"function\") {\n return (options.enabled as Accessor<boolean>)();\n }\n return options.enabled ?? true;\n };\n\n const threshold = options.threshold ?? 40;\n\n const checkIfAtBottom = () => {\n const el = getElement();\n if (!el) return true;\n const distanceToBottom = el.scrollHeight - el.scrollTop - el.clientHeight;\n return distanceToBottom <= threshold;\n };\n\n const scrollToBottom = (opts?: { smooth?: boolean }) => {\n const el = getElement();\n if (!el) return;\n\n const useSmooth = opts?.smooth ?? (options.behavior === \"smooth\" || options.behavior === undefined);\n\n el.scrollTo({\n top: el.scrollHeight,\n behavior: useSmooth ? \"smooth\" : \"auto\",\n });\n setIsAtBottom(true);\n };\n\n const handleScroll = () => {\n const atBottom = checkIfAtBottom();\n setIsAtBottom(atBottom);\n };\n\n onMount(() => {\n if (typeof window === \"undefined\") return;\n\n const el = getElement();\n if (el) {\n el.addEventListener(\"scroll\", handleScroll, { passive: true });\n setIsAtBottom(checkIfAtBottom());\n }\n });\n\n onCleanup(() => {\n if (typeof window === \"undefined\") return;\n const el = getElement();\n if (el) {\n el.removeEventListener(\"scroll\", handleScroll);\n }\n });\n\n // Watch trigger dependencies (e.g. messages length or stream tokens)\n if (options.trigger) {\n createEffect(() => {\n // Track trigger dependency\n options.trigger!();\n\n if (isEnabled() && isAtBottom()) {\n // Run after microtask/DOM paint\n setTimeout(() => {\n scrollToBottom({ smooth: true });\n }, 10);\n }\n });\n }\n\n return {\n isAtBottom,\n isScrolledUp,\n scrollToBottom,\n };\n}\n",
|
|
10
|
+
"type": "registry:hook"
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -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,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-pagination",
|
|
3
|
+
"title": "createPagination",
|
|
4
|
+
"description": "SolidJS reactive primitive for computing pagination state, dynamic page ranges with ellipses, and navigation controls",
|
|
5
|
+
"type": "registry:hook",
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"path": "hooks/create-pagination.ts",
|
|
9
|
+
"content": "import { createSignal, createMemo, type Accessor } from \"solid-js\";\n\nexport interface CreatePaginationOptions {\n /** Total number of items across all pages, or total count. */\n count?: number | Accessor<number>;\n /** Explicit total number of pages. If provided, overrides count / pageSize calculation. */\n totalPages?: number | Accessor<number>;\n /** Controlled active page number (1-indexed). */\n page?: number | Accessor<number>;\n /** Default active page number for uncontrolled state. Defaults to 1. */\n defaultPage?: number;\n /** Number of items per page. Defaults to 10. */\n pageSize?: number | Accessor<number>;\n /** Number of sibling page buttons visible on each side of the current active page. Defaults to 1. */\n siblingCount?: number | Accessor<number>;\n /** Number of boundary pages visible at the beginning and end. Defaults to 1. */\n boundaries?: number | Accessor<number>;\n /** Callback fired whenever the active page changes. */\n onChange?: (page: number) => void;\n}\n\nexport interface CreatePaginationReturn {\n /** Accessor returning the current active page number (1-indexed). */\n page: Accessor<number>;\n /** Accessor returning the calculated total number of pages. */\n totalPages: Accessor<number>;\n /** Accessor returning the active page size. */\n pageSize: Accessor<number>;\n /** Accessor returning an array of page numbers and \"ellipsis\" strings. */\n range: Accessor<(number | \"ellipsis\")[]>;\n /** Function to programmatically change to a specific page number. */\n setPage: (page: number) => void;\n /** Function to navigate to the next page. */\n next: () => void;\n /** Function to navigate to the previous page. */\n previous: () => void;\n /** Function to navigate to the first page (1). */\n first: () => void;\n /** Function to navigate to the last page (totalPages). */\n last: () => void;\n /** Accessor indicating whether a next page exists. */\n hasNext: Accessor<boolean>;\n /** Accessor indicating whether a previous page exists. */\n hasPrevious: Accessor<boolean>;\n /** 1-based start index of items on the current page (e.g. 1 for page 1 with pageSize 10). */\n startIndex: Accessor<number>;\n /** 1-based end index of items on the current page (e.g. 10 for page 1 with pageSize 10). */\n endIndex: Accessor<number>;\n}\n\n/**\n * SolidJS reactive primitive for computing pagination state, dynamic page range with ellipses, and navigation helpers.\n *\n * @param options Pagination configuration options.\n */\nexport function createPagination(options: CreatePaginationOptions = {}): CreatePaginationReturn {\n const getCount = () => {\n const raw = typeof options.count === \"function\" ? options.count() : options.count ?? 0;\n return Math.max(0, raw);\n };\n\n const getPageSize = () => {\n const raw = typeof options.pageSize === \"function\" ? options.pageSize() : options.pageSize ?? 10;\n return Math.max(1, raw);\n };\n\n const getExplicitTotalPages = () => {\n const raw = typeof options.totalPages === \"function\" ? options.totalPages() : options.totalPages;\n return raw !== undefined ? Math.max(1, raw) : undefined;\n };\n\n const getSiblingCount = () => {\n const raw = typeof options.siblingCount === \"function\" ? options.siblingCount() : options.siblingCount ?? 1;\n return Math.max(0, raw);\n };\n\n const getBoundaries = () => {\n const raw = typeof options.boundaries === \"function\" ? options.boundaries() : options.boundaries ?? 1;\n return Math.max(0, raw);\n };\n\n const [internalPage, setInternalPage] = createSignal<number>(Math.max(1, options.defaultPage ?? 1));\n\n const totalPages = createMemo<number>(() => {\n const explicit = getExplicitTotalPages();\n if (explicit !== undefined) {\n return Math.max(1, explicit);\n }\n const count = getCount();\n const size = getPageSize();\n return Math.max(1, Math.ceil(count / size));\n });\n\n const rawPage = () => {\n if (typeof options.page === \"function\") {\n return options.page();\n }\n if (typeof options.page === \"number\") {\n return options.page;\n }\n return internalPage();\n };\n\n const page = createMemo<number>(() => {\n const p = rawPage();\n const max = totalPages();\n if (p < 1) return 1;\n if (p > max) return max;\n return p;\n });\n\n const setPage = (nextPage: number) => {\n const max = totalPages();\n const clamped = Math.max(1, Math.min(nextPage, max));\n if (typeof options.page !== \"function\" && typeof options.page !== \"number\") {\n setInternalPage(clamped);\n }\n options.onChange?.(clamped);\n };\n\n const next = () => setPage(page() + 1);\n const previous = () => setPage(page() - 1);\n const first = () => setPage(1);\n const last = () => setPage(totalPages());\n\n const hasNext = createMemo(() => page() < totalPages());\n const hasPrevious = createMemo(() => page() > 1);\n\n const startIndex = createMemo(() => {\n const count = getCount();\n if (count === 0 && getExplicitTotalPages() === undefined) return 0;\n return (page() - 1) * getPageSize() + 1;\n });\n\n const endIndex = createMemo(() => {\n const count = getCount();\n const calculated = page() * getPageSize();\n if (count > 0) {\n return Math.min(calculated, count);\n }\n return calculated;\n });\n\n const range = createMemo<(number | \"ellipsis\")[]>(() => {\n const total = totalPages();\n const current = page();\n const siblings = getSiblingCount();\n const boundaries = getBoundaries();\n\n if (total <= 1) {\n return [1];\n }\n\n const pagesSet = new Set<number>();\n\n // 1. Boundary pages at the start\n for (let i = 1; i <= Math.min(boundaries, total); i++) {\n pagesSet.add(i);\n }\n\n // 2. Sibling pages around current page\n const leftSibling = Math.max(1, current - siblings);\n const rightSibling = Math.min(total, current + siblings);\n for (let i = leftSibling; i <= rightSibling; i++) {\n pagesSet.add(i);\n }\n\n // 3. Boundary pages at the end\n for (let i = Math.max(1, total - boundaries + 1); i <= total; i++) {\n pagesSet.add(i);\n }\n\n const sortedPages = Array.from(pagesSet).sort((a, b) => a - b);\n const result: (number | \"ellipsis\")[] = [];\n\n for (let i = 0; i < sortedPages.length; i++) {\n const currentPageNum = sortedPages[i];\n if (i > 0) {\n const prevPageNum = sortedPages[i - 1];\n const gap = currentPageNum - prevPageNum;\n if (gap === 2) {\n result.push(prevPageNum + 1);\n } else if (gap > 2) {\n result.push(\"ellipsis\");\n }\n }\n result.push(currentPageNum);\n }\n\n return result;\n });\n\n return {\n page,\n totalPages,\n pageSize: getPageSize,\n range,\n setPage,\n next,\n previous,\n first,\n last,\n hasNext,\n hasPrevious,\n startIndex,\n endIndex,\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
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "footer",
|
|
3
|
+
"title": "Footer",
|
|
4
|
+
"description": "A responsive, accessible, and structured bottom navigation layout suite supporting multi-column link directories, brand sections, and newsletter inputs.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge",
|
|
9
|
+
"class-variance-authority"
|
|
10
|
+
],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "ui/footer.tsx",
|
|
14
|
+
"content": "import { splitProps, type JSX, type ParentComponent } from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\n/* --- Root Footer --- */\nexport const footerVariants = cva(\"w-full transition-colors\", {\n variants: {\n variant: {\n default: \"bg-background border-t border-border/60 text-foreground\",\n muted: \"bg-muted/40 border-t border-border text-foreground\",\n bordered: \"bg-background border-t-2 border-border text-foreground\",\n floating: \"my-8 rounded-lg border border-border/80 bg-card text-card-foreground shadow-xs\",\n transparent: \"bg-transparent border-transparent text-foreground\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n});\n\nexport interface FooterProps\n extends JSX.HTMLAttributes<HTMLElement>,\n VariantProps<typeof footerVariants> {\n maxWidth?: \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | \"full\";\n}\n\nexport const Footer: ParentComponent<FooterProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"variant\", \"maxWidth\", \"children\"]);\n\n const maxWidthClass = () => {\n switch (local.maxWidth) {\n case \"sm\":\n return \"max-w-screen-sm\";\n case \"md\":\n return \"max-w-screen-md\";\n case \"lg\":\n return \"max-w-screen-lg\";\n case \"xl\":\n return \"max-w-screen-xl\";\n case \"full\":\n return \"max-w-full\";\n case \"2xl\":\n default:\n return \"max-w-screen-2xl\";\n }\n };\n\n return (\n <footer\n class={cn(\n footerVariants({ variant: local.variant }),\n local.variant === \"floating\" && cn(\"mx-auto\", maxWidthClass()),\n local.class\n )}\n {...rest}\n >\n <div class={cn(\"w-full\", local.variant !== \"floating\" && cn(\"mx-auto\", maxWidthClass()))}>\n {local.children}\n </div>\n </footer>\n );\n};\n\n/* --- Footer Container --- */\nexport interface FooterContainerProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const FooterContainer: ParentComponent<FooterContainerProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\"px-4 sm:px-6 lg:px-8 py-10 sm:py-12 md:py-16 w-full\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- Footer Content (Columns Grid / Layout) --- */\nexport interface FooterContentProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const FooterContent: ParentComponent<FooterContentProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\n \"grid grid-cols-2 sm:grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-8 lg:gap-12\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- Footer Column --- */\nexport interface FooterColumnProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const FooterColumn: ParentComponent<FooterColumnProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div class={cn(\"flex flex-col space-y-3\", local.class)} {...rest}>\n {local.children}\n </div>\n );\n};\n\n/* --- Footer Column Title --- */\nexport interface FooterColumnTitleProps extends JSX.HTMLAttributes<HTMLHeadingElement> {\n class?: string;\n}\n\nexport const FooterColumnTitle: ParentComponent<FooterColumnTitleProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <h4\n class={cn(\n \"text-xs font-semibold uppercase tracking-wider text-foreground\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </h4>\n );\n};\n\n/* --- Footer Column List --- */\nexport interface FooterColumnListProps extends JSX.HTMLAttributes<HTMLUListElement> {\n class?: string;\n}\n\nexport const FooterColumnList: ParentComponent<FooterColumnListProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <ul class={cn(\"space-y-2 list-none p-0 m-0\", local.class)} {...rest}>\n {local.children}\n </ul>\n );\n};\n\n/* --- Footer Link --- */\nexport interface FooterLinkProps extends JSX.AnchorHTMLAttributes<HTMLAnchorElement> {\n class?: string;\n}\n\nexport const FooterLink: ParentComponent<FooterLinkProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <li>\n <a\n class={cn(\n \"inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors cursor-pointer\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </a>\n </li>\n );\n};\n\n/* --- Footer Brand / Info Section --- */\nexport interface FooterBrandProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const FooterBrand: ParentComponent<FooterBrandProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\n \"col-span-2 sm:col-span-2 md:col-span-4 lg:col-span-2 flex flex-col space-y-3 mb-4 lg:mb-0\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- Footer Bottom (Copyright and Secondary Links Bar) --- */\nexport interface FooterBottomProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const FooterBottom: ParentComponent<FooterBottomProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\n \"mt-12 sm:mt-16 pt-8 border-t border-border/50 flex flex-col sm:flex-row items-center justify-between gap-4 text-xs text-muted-foreground\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- Footer Copyright Text --- */\nexport interface FooterCopyrightProps extends JSX.HTMLAttributes<HTMLParagraphElement> {\n class?: string;\n}\n\nexport const FooterCopyright: ParentComponent<FooterCopyrightProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <p class={cn(\"text-xs text-muted-foreground leading-relaxed\", local.class)} {...rest}>\n {local.children}\n </p>\n );\n};\n\n/* --- Footer Social Links Container --- */\nexport interface FooterSocialsProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const FooterSocials: ParentComponent<FooterSocialsProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div class={cn(\"flex items-center gap-3 text-muted-foreground\", local.class)} {...rest}>\n {local.children}\n </div>\n );\n};\n",
|
|
15
|
+
"type": "registry:ui"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "forgot-password-01",
|
|
3
|
+
"title": "Forgot Password 01 — Account Recovery Flow",
|
|
4
|
+
"description": "A sleek password recovery block with email instructions submission and success confirmation states.",
|
|
5
|
+
"type": "registry:block",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge",
|
|
9
|
+
"lucide-solid"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"card",
|
|
13
|
+
"input",
|
|
14
|
+
"label",
|
|
15
|
+
"button",
|
|
16
|
+
"alert",
|
|
17
|
+
"form",
|
|
18
|
+
"field",
|
|
19
|
+
"form-message"
|
|
20
|
+
],
|
|
21
|
+
"files": [
|
|
22
|
+
{
|
|
23
|
+
"path": "blocks/forgot-password-01.tsx",
|
|
24
|
+
"content": "import { createSignal, Show, type Component } from \"solid-js\";\nimport { createForm } from \"@nikala-ui/hooks\";\nimport {\n Card,\n CardHeader,\n CardTitle,\n CardDescription,\n CardContent,\n CardFooter,\n} from \"../components/ui/card\";\nimport { Form } from \"../components/ui/form\";\nimport { Field, FieldLabel } from \"../components/ui/field\";\nimport { FormMessage } from \"../components/ui/form-message\";\nimport { Button } from \"../components/ui/button\";\nimport { Input } from \"../components/ui/input\";\nimport { Alert, AlertTitle, AlertDescription } from \"../components/ui/alert\";\nimport { Lock, Mail, ArrowLeft, ArrowRight, CheckCircle2, RotateCcw } from \"lucide-solid\";\n\nexport const ForgotPassword01: Component = () => {\n const [sent, setSent] = createSignal(false);\n const [submittedEmail, setSubmittedEmail] = createSignal(\"\");\n\n const form = createForm({\n initialValues: {\n email: \"\",\n },\n validate: (values) => {\n const errors: Record<string, string> = {};\n if (!values.email.trim()) {\n errors.email = \"Email is required\";\n } else if (!values.email.includes(\"@\")) {\n errors.email = \"Please enter a valid email address\";\n }\n return errors;\n },\n onSubmit: async (values) => {\n await new Promise((resolve) => setTimeout(resolve, 1000));\n setSubmittedEmail(values.email);\n setSent(true);\n },\n });\n\n const isFormValid = () =>\n form.values().email.trim() !== \"\" && form.values().email.includes(\"@\");\n\n const handleReset = () => {\n setSent(false);\n form.resetForm();\n };\n\n return (\n <div class=\"@container w-full min-h-[520px] flex items-center justify-center p-4 sm:p-6 md:p-10\">\n <Card class=\"w-full max-w-md border-border shadow-md bg-card\">\n {/* Card Header */}\n <CardHeader class=\"space-y-2 text-center pb-4\">\n <div class=\"mx-auto size-12 rounded-lg bg-primary/10 text-primary flex items-center justify-center mb-1 shadow-2xs\">\n <Show when={sent()} fallback={<Lock class=\"size-6\" />}>\n <Mail class=\"size-6\" />\n </Show>\n </div>\n <CardTitle class=\"text-2xl font-bold tracking-tight\">\n {sent() ? \"Check your email\" : \"Reset your password\"}\n </CardTitle>\n <CardDescription class=\"text-xs sm:text-sm\">\n {sent()\n ? `We have sent a secure password reset link to ${submittedEmail()}`\n : \"Enter the email associated with your account and we will send you a reset link\"}\n </CardDescription>\n </CardHeader>\n\n <CardContent class=\"space-y-4\">\n <Show\n when={sent()}\n fallback={\n <Form onSubmit={form.handleSubmit} loading={form.isSubmitting()} class=\"space-y-4\">\n <Field>\n <FieldLabel for=\"fp-email\" class=\"text-xs sm:text-sm\">Email address</FieldLabel>\n <Input\n id=\"fp-email\"\n type=\"email\"\n placeholder=\"name@company.com\"\n value={form.values().email}\n onInput={form.handleChange(\"email\")}\n onBlur={form.handleBlur(\"email\")}\n autocomplete=\"email\"\n />\n <FormMessage form={form} name=\"email\" />\n </Field>\n\n <Button\n type=\"submit\"\n class=\"w-full font-medium\"\n disabled={form.isSubmitting() || !isFormValid()}\n >\n <Show when={form.isSubmitting()} fallback={<>Send Reset Link <ArrowRight class=\"ml-2 size-4\" /></>}>\n Sending instructions...\n </Show>\n </Button>\n </Form>\n }\n >\n <div class=\"space-y-4\">\n <Alert class=\"bg-emerald-500/10 border-emerald-500/30 text-emerald-500 py-3\">\n <CheckCircle2 class=\"size-4 text-emerald-500 shrink-0\" />\n <AlertTitle class=\"text-xs font-semibold text-foreground\">Instructions Sent</AlertTitle>\n <AlertDescription class=\"text-[11px] text-muted-foreground\">\n If an account exists for <span class=\"font-medium text-foreground font-mono\">{submittedEmail()}</span>, you will receive an email shortly.\n </AlertDescription>\n </Alert>\n\n <div class=\"flex items-center justify-between text-xs text-muted-foreground pt-1\">\n <span>Didn't get the email?</span>\n <Button\n variant=\"link\"\n size=\"sm\"\n type=\"button\"\n onClick={handleReset}\n class=\"h-auto p-0 text-xs text-primary font-semibold gap-1 cursor-pointer\"\n >\n <RotateCcw class=\"size-3\" /> Try another email\n </Button>\n </div>\n </div>\n </Show>\n </CardContent>\n\n {/* Card Footer */}\n <CardFooter class=\"justify-center border-t border-border/50 py-4 text-xs text-muted-foreground\">\n <a\n href=\"/blocks/login-01\"\n class=\"inline-flex items-center gap-1.5 text-foreground hover:text-primary transition-colors font-medium\"\n >\n <ArrowLeft class=\"size-3.5\" /> Back to sign in\n </a>\n </CardFooter>\n </Card>\n </div>\n );\n};\n\nexport default ForgotPassword01;\n",
|
|
25
|
+
"type": "registry:block"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hero-01",
|
|
3
|
+
"title": "Hero 01 — Simple Centered with Actions",
|
|
4
|
+
"description": "A clean centered hero section with badge pill, high-contrast headline, and dual CTA buttons.",
|
|
5
|
+
"type": "registry:block",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge",
|
|
9
|
+
"lucide-solid"
|
|
10
|
+
],
|
|
11
|
+
"registryDependencies": [
|
|
12
|
+
"button",
|
|
13
|
+
"badge"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
{
|
|
17
|
+
"path": "blocks/hero-01.tsx",
|
|
18
|
+
"content": "import { Component } from \"solid-js\";\nimport { Button } from \"../components/ui/button\";\nimport { Badge } from \"../components/ui/badge\";\nimport { ArrowRight } from \"lucide-solid\";\n\nexport default function Hero01() {\n return (\n <section class=\"w-full py-12 md:py-24 lg:py-32 flex flex-col items-center justify-center text-center\">\n <Badge variant=\"secondary\" class=\"mb-4\">\n Nikala UI Blocks\n </Badge>\n <h1 class=\"text-4xl font-bold tracking-tight sm:text-6xl text-foreground max-w-3xl\">\n Build faster with copy-paste SolidJS blocks\n </h1>\n <p class=\"mt-6 text-lg text-muted-foreground max-w-2xl\">\n Pre-designed, fully responsive marketing and application blocks built natively for Tailwind CSS v4.\n </p>\n <div class=\"mt-8 flex items-center gap-4\">\n <Button size=\"lg\">\n Get Started <ArrowRight class=\"ml-2 size-4\" />\n </Button>\n <Button variant=\"outline\" size=\"lg\">\n Documentation\n </Button>\n </div>\n </section>\n );\n}\n",
|
|
19
|
+
"type": "registry:block"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|