@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.
Files changed (55) hide show
  1. package/package.json +1 -1
  2. package/registry/bubble.json +18 -0
  3. package/registry/button-group.json +20 -0
  4. package/registry/create-chat-scroll.json +13 -0
  5. package/registry/create-drop-zone.json +13 -0
  6. package/registry/create-pagination.json +13 -0
  7. package/registry/dropzone.json +21 -0
  8. package/registry/footer.json +18 -0
  9. package/registry/forgot-password-01.json +28 -0
  10. package/registry/hero-01.json +22 -0
  11. package/registry/index.json +331 -0
  12. package/registry/login-01.json +28 -0
  13. package/registry/marker.json +17 -0
  14. package/registry/marquee.json +17 -0
  15. package/registry/message.json +17 -0
  16. package/registry/navbar.json +19 -0
  17. package/registry/navigation-menu.json +22 -0
  18. package/registry/otp-verification-01.json +26 -0
  19. package/registry/pagination.json +22 -0
  20. package/registry/rating.json +19 -0
  21. package/registry/register-01.json +31 -0
  22. package/registry/review-card.json +23 -0
  23. package/registry/scroll-area.json +1 -1
  24. package/registry/sidebar.json +24 -0
  25. package/registry/spinner.json +1 -1
  26. package/registry/stat.json +19 -0
  27. package/registry/table.json +17 -0
  28. package/registry/timeline.json +18 -0
  29. package/registry/toggle-group.json +21 -0
  30. package/src/registry/blocks/forgot-password-01.tsx +141 -0
  31. package/src/registry/blocks/hero-01.tsx +28 -0
  32. package/src/registry/blocks/login-01.tsx +231 -0
  33. package/src/registry/blocks/otp-verification-01.tsx +170 -0
  34. package/src/registry/blocks/register-01.tsx +318 -0
  35. package/src/registry/components/ui/bubble.tsx +142 -0
  36. package/src/registry/components/ui/button-group.tsx +42 -0
  37. package/src/registry/components/ui/dropzone.tsx +189 -0
  38. package/src/registry/components/ui/footer.tsx +247 -0
  39. package/src/registry/components/ui/marker.tsx +102 -0
  40. package/src/registry/components/ui/marquee.tsx +118 -0
  41. package/src/registry/components/ui/message.tsx +168 -0
  42. package/src/registry/components/ui/navbar.tsx +368 -0
  43. package/src/registry/components/ui/navigation-menu.tsx +358 -0
  44. package/src/registry/components/ui/pagination.tsx +258 -0
  45. package/src/registry/components/ui/rating.tsx +185 -0
  46. package/src/registry/components/ui/review-card.tsx +195 -0
  47. package/src/registry/components/ui/scroll-area.tsx +2 -2
  48. package/src/registry/components/ui/sidebar.tsx +692 -0
  49. package/src/registry/components/ui/spinner.tsx +1 -1
  50. package/src/registry/components/ui/stat.tsx +245 -0
  51. package/src/registry/components/ui/table.tsx +160 -0
  52. package/src/registry/components/ui/timeline.tsx +350 -0
  53. package/src/registry/components/ui/toggle-group.tsx +203 -0
  54. package/src/registry/index.ts +3 -3
  55. package/src/registry/metadata.ts +141 -0
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "otp-verification-01",
3
+ "title": "OTP Verification 01 — Two-Factor Security Code",
4
+ "description": "A clean 2-Factor Authentication block with a 6-digit PIN input, countdown resend timer, and security notifications.",
5
+ "type": "registry:block",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "lucide-solid"
10
+ ],
11
+ "registryDependencies": [
12
+ "card",
13
+ "pin-input",
14
+ "button",
15
+ "alert",
16
+ "badge",
17
+ "form"
18
+ ],
19
+ "files": [
20
+ {
21
+ "path": "blocks/otp-verification-01.tsx",
22
+ "content": "import { createSignal, onMount, onCleanup, Show, type Component } from \"solid-js\";\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 { Button } from \"../components/ui/button\";\nimport { PinInput, PinInputInput } from \"../components/ui/pin-input\";\nimport { Alert, AlertTitle, AlertDescription } from \"../components/ui/alert\";\nimport { Badge } from \"../components/ui/badge\";\nimport { KeyRound, Mail, CheckCircle2, RotateCcw, ArrowRight } from \"lucide-solid\";\n\nexport const OtpVerification01: Component = () => {\n const [code, setCode] = createSignal(\"\");\n const [loading, setLoading] = createSignal(false);\n const [verified, setVerified] = createSignal(false);\n const [countdown, setCountdown] = createSignal(59);\n const [canResend, setCanResend] = createSignal(false);\n\n let timerId: ReturnType<typeof setInterval> | undefined;\n\n const startCountdown = () => {\n setCountdown(59);\n setCanResend(false);\n clearInterval(timerId);\n timerId = setInterval(() => {\n setCountdown((prev) => {\n if (prev <= 1) {\n clearInterval(timerId);\n setCanResend(true);\n return 0;\n }\n return prev - 1;\n });\n }, 1000);\n };\n\n onMount(() => {\n startCountdown();\n });\n\n onCleanup(() => {\n clearInterval(timerId);\n });\n\n const handleVerify = (e: Event) => {\n e.preventDefault();\n if (code().length < 6) return;\n setLoading(true);\n setTimeout(() => {\n setLoading(false);\n setVerified(true);\n }, 1200);\n };\n\n const handleResend = () => {\n if (!canResend()) return;\n setCode(\"\");\n startCountdown();\n };\n\n return (\n <div class=\"@container w-full min-h-[500px] 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 <KeyRound class=\"size-6\" />\n </div>\n <div class=\"flex items-center justify-center gap-2\">\n <CardTitle class=\"text-2xl font-bold tracking-tight\">Two-Factor Auth</CardTitle>\n <Badge variant=\"secondary\" class=\"text-[10px] px-1.5 py-0 font-mono\">\n 2FA\n </Badge>\n </div>\n <CardDescription class=\"text-xs sm:text-sm\">\n Enter the 6-digit security code sent to your registered email address\n </CardDescription>\n </CardHeader>\n\n <CardContent class=\"space-y-6\">\n {/* Info Alert */}\n <Alert class=\"bg-muted/40 border-border/70 py-2.5\">\n <Mail class=\"size-4 text-primary shrink-0\" />\n <AlertTitle class=\"text-xs font-semibold\">Verification Code Sent</AlertTitle>\n <AlertDescription class=\"text-[11px] text-muted-foreground\">\n Sent to <span class=\"font-medium text-foreground font-mono\">dev***@nikala.dev</span>\n </AlertDescription>\n </Alert>\n\n {/* Form */}\n <Form onSubmit={handleVerify} loading={loading()} class=\"space-y-6\">\n {/* Responsive 6-Digit PinInput */}\n <div class=\"flex flex-col items-center justify-center space-y-2 w-full\">\n <PinInput\n value={code()}\n onValueChange={setCode}\n length={6}\n type=\"numeric\"\n class=\"justify-center items-center gap-1.5 @sm:gap-2.5 max-w-full\"\n >\n <PinInputInput index={0} class=\"h-9 w-9 @sm:h-11 @sm:w-11 text-sm @sm:text-base font-semibold\" />\n <PinInputInput index={1} class=\"h-9 w-9 @sm:h-11 @sm:w-11 text-sm @sm:text-base font-semibold\" />\n <PinInputInput index={2} class=\"h-9 w-9 @sm:h-11 @sm:w-11 text-sm @sm:text-base font-semibold\" />\n <PinInputInput index={3} class=\"h-9 w-9 @sm:h-11 @sm:w-11 text-sm @sm:text-base font-semibold\" />\n <PinInputInput index={4} class=\"h-9 w-9 @sm:h-11 @sm:w-11 text-sm @sm:text-base font-semibold\" />\n <PinInputInput index={5} class=\"h-9 w-9 @sm:h-11 @sm:w-11 text-sm @sm:text-base font-semibold\" />\n </PinInput>\n\n <Show when={verified()}>\n <div class=\"flex items-center gap-1.5 text-xs text-emerald-500 font-medium pt-1\">\n <CheckCircle2 class=\"size-3.5\" />\n <span>Code verified successfully!</span>\n </div>\n </Show>\n </div>\n\n {/* Countdown / Resend Action */}\n <div class=\"flex items-center justify-between text-xs text-muted-foreground px-1\">\n <span>Didn't receive code?</span>\n <Show\n when={canResend()}\n fallback={\n <span class=\"font-mono text-primary text-[11px]\">\n Resend in 00:{countdown() < 10 ? `0${countdown()}` : countdown()}s\n </span>\n }\n >\n <Button\n variant=\"link\"\n size=\"sm\"\n type=\"button\"\n onClick={handleResend}\n class=\"h-auto p-0 text-xs text-primary font-semibold gap-1\"\n >\n <RotateCcw class=\"size-3\" /> Resend Code\n </Button>\n </Show>\n </div>\n\n {/* Submit Button */}\n <Button\n type=\"submit\"\n class=\"w-full font-medium\"\n disabled={loading() || code().length < 6 || verified()}\n >\n <Show when={loading()} fallback={<>Verify Account <ArrowRight class=\"ml-2 size-4\" /></>}>\n Verifying...\n </Show>\n </Button>\n </Form>\n </CardContent>\n\n {/* Card Footer */}\n <CardFooter class=\"justify-center border-t border-border/50 py-4 text-xs text-muted-foreground\">\n Need help?{\" \"}\n <a href=\"#support\" class=\"text-primary font-semibold hover:underline ml-1\">\n Contact Security Team\n </a>\n </CardFooter>\n </Card>\n </div>\n );\n};\n\nexport default OtpVerification01;\n",
23
+ "type": "registry:block"
24
+ }
25
+ ]
26
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "pagination",
3
+ "title": "Pagination",
4
+ "description": "An accessible multi-page navigation bar with previous, next, page numbers, and ellipsis controls.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "class-variance-authority",
10
+ "lucide-solid"
11
+ ],
12
+ "registryDependencies": [
13
+ "create-pagination"
14
+ ],
15
+ "files": [
16
+ {
17
+ "path": "ui/pagination.tsx",
18
+ "content": "import { splitProps, type Component, type JSX, type ParentComponent, Show } from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\nimport { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, MoreHorizontal } from \"lucide-solid\";\n\n/* --- 1. Button Variants for Pagination Links --- */\nexport const paginationButtonVariants = cva(\n \"inline-flex items-center justify-center whitespace-nowrap rounded-md font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 cursor-pointer select-none\",\n {\n variants: {\n variant: {\n default: \"hover:bg-accent hover:text-accent-foreground data-[active=true]:bg-primary data-[active=true]:text-primary-foreground data-[active=true]:font-bold data-[active=true]:shadow-2xs\",\n outline: \"border border-border bg-background hover:bg-accent hover:text-accent-foreground data-[active=true]:border-primary data-[active=true]:bg-primary/10 data-[active=true]:text-primary data-[active=true]:font-bold\",\n ghost: \"hover:bg-accent/80 hover:text-accent-foreground data-[active=true]:bg-accent data-[active=true]:text-foreground data-[active=true]:font-bold\",\n flat: \"hover:bg-muted/80 data-[active=true]:bg-muted data-[active=true]:text-foreground data-[active=true]:font-bold\",\n },\n size: {\n default: \"h-9 min-w-9 px-3 text-sm\",\n sm: \"h-8 min-w-8 px-2 text-xs\",\n lg: \"h-10 min-w-10 px-4 text-base\",\n icon: \"h-9 w-9 p-0 text-sm\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n);\n\n/* --- 2. Pagination Root --- */\nexport interface PaginationProps extends JSX.HTMLAttributes<HTMLElement> {\n class?: string;\n}\n\nexport const Pagination: ParentComponent<PaginationProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <nav\n role=\"navigation\"\n aria-label=\"pagination\"\n class={cn(\"mx-auto flex w-full justify-center\", local.class)}\n {...rest}\n >\n {local.children}\n </nav>\n );\n};\n\n/* --- 3. PaginationContent --- */\nexport interface PaginationContentProps extends JSX.HTMLAttributes<HTMLUListElement> {\n class?: string;\n}\n\nexport const PaginationContent: ParentComponent<PaginationContentProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <ul\n class={cn(\"flex flex-row items-center gap-1 flex-wrap\", local.class)}\n {...rest}\n >\n {local.children}\n </ul>\n );\n};\n\n/* --- 4. PaginationItem --- */\nexport interface PaginationItemProps extends JSX.HTMLAttributes<HTMLLIElement> {\n class?: string;\n}\n\nexport const PaginationItem: ParentComponent<PaginationItemProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <li class={cn(\"\", local.class)} {...rest}>\n {local.children}\n </li>\n );\n};\n\n/* --- 5. PaginationLink --- */\nexport interface PaginationLinkProps\n extends JSX.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof paginationButtonVariants> {\n isActive?: boolean;\n class?: string;\n}\n\nexport const PaginationLink: ParentComponent<PaginationLinkProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"variant\", \"size\", \"isActive\", \"children\"]);\n\n return (\n <button\n type=\"button\"\n aria-current={local.isActive ? \"page\" : undefined}\n data-active={local.isActive ? \"true\" : \"false\"}\n class={cn(\n paginationButtonVariants({\n variant: local.variant,\n size: local.size,\n }),\n local.class\n )}\n {...rest}\n >\n {local.children}\n </button>\n );\n};\n\n/* --- 6. PaginationPrevious --- */\nexport interface PaginationPreviousProps\n extends JSX.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof paginationButtonVariants> {\n class?: string;\n hideText?: boolean;\n}\n\nexport const PaginationPrevious: Component<PaginationPreviousProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"variant\", \"size\", \"hideText\"]);\n\n return (\n <PaginationLink\n aria-label=\"Go to previous page\"\n variant={local.variant}\n size={local.size}\n class={cn(\"gap-1 pl-2.5\", local.hideText && \"p-0 min-w-8\", local.class)}\n {...rest}\n >\n <ChevronLeft class=\"size-4\" />\n <Show when={!local.hideText}>\n <span>Previous</span>\n </Show>\n </PaginationLink>\n );\n};\n\n/* --- 7. PaginationNext --- */\nexport interface PaginationNextProps\n extends JSX.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof paginationButtonVariants> {\n class?: string;\n hideText?: boolean;\n}\n\nexport const PaginationNext: Component<PaginationNextProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"variant\", \"size\", \"hideText\"]);\n\n return (\n <PaginationLink\n aria-label=\"Go to next page\"\n variant={local.variant}\n size={local.size}\n class={cn(\"gap-1 pr-2.5\", local.hideText && \"p-0 min-w-8\", local.class)}\n {...rest}\n >\n <Show when={!local.hideText}>\n <span>Next</span>\n </Show>\n <ChevronRight class=\"size-4\" />\n </PaginationLink>\n );\n};\n\n/* --- 8. PaginationFirst --- */\nexport interface PaginationFirstProps\n extends JSX.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof paginationButtonVariants> {\n class?: string;\n hideText?: boolean;\n}\n\nexport const PaginationFirst: Component<PaginationFirstProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"variant\", \"size\", \"hideText\"]);\n\n return (\n <PaginationLink\n aria-label=\"Go to first page\"\n variant={local.variant}\n size={local.size}\n class={cn(\"gap-1 p-0 min-w-8\", local.class)}\n {...rest}\n >\n <ChevronsLeft class=\"size-4\" />\n <Show when={!local.hideText}>\n <span class=\"sr-only\">First page</span>\n </Show>\n </PaginationLink>\n );\n};\n\n/* --- 9. PaginationLast --- */\nexport interface PaginationLastProps\n extends JSX.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof paginationButtonVariants> {\n class?: string;\n hideText?: boolean;\n}\n\nexport const PaginationLast: Component<PaginationLastProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"variant\", \"size\", \"hideText\"]);\n\n return (\n <PaginationLink\n aria-label=\"Go to last page\"\n variant={local.variant}\n size={local.size}\n class={cn(\"gap-1 p-0 min-w-8\", local.class)}\n {...rest}\n >\n <ChevronsRight class=\"size-4\" />\n <Show when={!local.hideText}>\n <span class=\"sr-only\">Last page</span>\n </Show>\n </PaginationLink>\n );\n};\n\n/* --- 10. PaginationEllipsis --- */\nexport interface PaginationEllipsisProps extends JSX.HTMLAttributes<HTMLSpanElement> {\n class?: string;\n}\n\nexport const PaginationEllipsis: Component<PaginationEllipsisProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <span\n aria-hidden=\"true\"\n class={cn(\"flex h-9 w-9 items-center justify-center text-muted-foreground\", local.class)}\n {...rest}\n >\n <MoreHorizontal class=\"size-4\" />\n <span class=\"sr-only\">More pages</span>\n </span>\n );\n};\n\n/* --- 11. PaginationSummary --- */\nexport interface PaginationSummaryProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const PaginationSummary: ParentComponent<PaginationSummaryProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\"text-xs text-muted-foreground font-mono select-none\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n",
19
+ "type": "registry:ui"
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "rating",
3
+ "title": "Rating",
4
+ "description": "An accessible star rating component supporting interactive inputs, hover preview states, and read-only score badges.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "class-variance-authority",
10
+ "lucide-solid"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "ui/rating.tsx",
15
+ "content": "import {\n createSignal,\n splitProps,\n For,\n type JSX,\n type Component,\n} from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Star } from \"lucide-solid\";\nimport { cn } from \"@/lib/cn\";\n\nexport const ratingVariants = cva(\n \"inline-flex items-center select-none transition-colors\",\n {\n variants: {\n size: {\n sm: \"gap-0.5\",\n default: \"gap-1\",\n lg: \"gap-1.5\",\n },\n variant: {\n yellow: \"text-amber-500 dark:text-amber-400\",\n primary: \"text-primary\",\n destructive: \"text-destructive\",\n },\n },\n defaultVariants: {\n size: \"default\",\n variant: \"yellow\",\n },\n }\n);\n\nexport const ratingStarVariants = cva(\n \"transition-all duration-150 shrink-0\",\n {\n variants: {\n size: {\n sm: \"size-3.5\",\n default: \"size-4.5\",\n lg: \"size-6\",\n },\n },\n defaultVariants: {\n size: \"default\",\n },\n }\n);\n\nexport interface RatingProps\n extends Omit<JSX.HTMLAttributes<HTMLDivElement>, \"onChange\">,\n VariantProps<typeof ratingVariants> {\n value?: number;\n defaultValue?: number;\n max?: number;\n readOnly?: boolean;\n disabled?: boolean;\n class?: string;\n starClass?: string;\n onChange?: (value: number) => void;\n onHover?: (value: number | null) => void;\n}\n\nexport const Rating: Component<RatingProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"value\",\n \"defaultValue\",\n \"max\",\n \"readOnly\",\n \"disabled\",\n \"size\",\n \"variant\",\n \"class\",\n \"starClass\",\n \"onChange\",\n \"onHover\",\n ]);\n\n const [internalValue, setInternalValue] = createSignal(\n local.defaultValue ?? 0\n );\n const [hoverValue, setHoverValue] = createSignal<number | null>(null);\n\n const currentValue = () =>\n local.value !== undefined ? local.value : internalValue();\n\n const maxStars = () => local.max ?? 5;\n const isInteractive = () => !local.readOnly && !local.disabled;\n\n const stars = () => Array.from({ length: maxStars() }, (_, i) => i + 1);\n\n const handleSelect = (starValue: number) => {\n if (!isInteractive()) return;\n if (local.value === undefined) {\n setInternalValue(starValue);\n }\n local.onChange?.(starValue);\n };\n\n const handleMouseEnter = (starValue: number) => {\n if (!isInteractive()) return;\n setHoverValue(starValue);\n local.onHover?.(starValue);\n };\n\n const handleMouseLeave = () => {\n if (!isInteractive()) return;\n setHoverValue(null);\n local.onHover?.(null);\n };\n\n const handleKeyDown = (e: KeyboardEvent) => {\n if (!isInteractive()) return;\n\n const val = currentValue();\n if (e.key === \"ArrowRight\" || e.key === \"ArrowUp\") {\n e.preventDefault();\n const next = Math.min(maxStars(), val + 1);\n handleSelect(next);\n } else if (e.key === \"ArrowLeft\" || e.key === \"ArrowDown\") {\n e.preventDefault();\n const prev = Math.max(1, val - 1);\n handleSelect(prev);\n } else if (e.key === \"Home\") {\n e.preventDefault();\n handleSelect(1);\n } else if (e.key === \"End\") {\n e.preventDefault();\n handleSelect(maxStars());\n }\n };\n\n return (\n <div\n role={isInteractive() ? \"radiogroup\" : \"img\"}\n aria-label={`Rating: ${currentValue()} of ${maxStars()} stars`}\n tabIndex={isInteractive() ? 0 : undefined}\n onKeyDown={handleKeyDown}\n onMouseLeave={handleMouseLeave}\n class={cn(\n ratingVariants({ size: local.size, variant: local.variant }),\n local.disabled && \"opacity-50 cursor-not-allowed\",\n isInteractive() && \"cursor-pointer focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded-md\",\n local.class\n )}\n {...rest}\n >\n <For each={stars()}>\n {(star) => {\n const isFilled = () => {\n const active = hoverValue() !== null ? hoverValue()! : currentValue();\n return star <= active;\n };\n\n return (\n <button\n type=\"button\"\n disabled={local.disabled || local.readOnly}\n tabIndex={-1}\n aria-label={`${star} star${star > 1 ? \"s\" : \"\"}`}\n onClick={() => handleSelect(star)}\n onMouseEnter={() => handleMouseEnter(star)}\n class={cn(\n \"p-0.5 border-0 bg-transparent transition-transform focus:outline-hidden\",\n isInteractive() && \"hover:scale-115 active:scale-95 cursor-pointer\",\n local.readOnly && \"cursor-default\",\n local.disabled && \"cursor-not-allowed\"\n )}\n >\n <Star\n class={cn(\n ratingStarVariants({ size: local.size }),\n isFilled()\n ? \"fill-current\"\n : \"text-muted-foreground/30 fill-transparent\",\n local.starClass\n )}\n />\n </button>\n );\n }}\n </For>\n </div>\n );\n};\n",
16
+ "type": "registry:ui"
17
+ }
18
+ ]
19
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "register-01",
3
+ "title": "Register 01 — Sign-Up Card with Password Strength",
4
+ "description": "A comprehensive sign-up card featuring social logins, live password strength meter with validation checklist, and terms agreement.",
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
+ "checkbox",
17
+ "progress",
18
+ "badge",
19
+ "separator",
20
+ "form",
21
+ "field",
22
+ "form-message"
23
+ ],
24
+ "files": [
25
+ {
26
+ "path": "blocks/register-01.tsx",
27
+ "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 { Checkbox } from \"../components/ui/checkbox\";\nimport { Label } from \"../components/ui/label\";\nimport { Progress } from \"../components/ui/progress\";\nimport { Badge } from \"../components/ui/badge\";\nimport { Separator } from \"../components/ui/separator\";\nimport { Eye, EyeOff, Check, X, ArrowRight, ShieldCheck } from \"lucide-solid\";\n\nexport const Register01: Component = () => {\n const [showPassword, setShowPassword] = createSignal(false);\n\n const form = createForm({\n initialValues: {\n fullName: \"\",\n email: \"\",\n password: \"\",\n agreeTerms: false,\n },\n validate: (values) => {\n const errors: Record<string, string> = {};\n if (!values.fullName.trim()) {\n errors.fullName = \"Full name is required\";\n }\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 if (!values.password) {\n errors.password = \"Password is required\";\n } else if (values.password.length < 8) {\n errors.password = \"Password must be at least 8 characters\";\n }\n if (!values.agreeTerms) {\n errors.agreeTerms = \"You must agree to the terms and privacy policy\";\n }\n return errors;\n },\n onSubmit: async (values) => {\n // Simulate API registration request\n await new Promise((resolve) => setTimeout(resolve, 1200));\n },\n });\n\n const password = () => form.values().password;\n\n // Reactive password criteria calculations\n const hasMinLength = () => password().length >= 8;\n const hasNumber = () => /\\d/.test(password());\n const hasSpecial = () => /[^A-Za-z0-9]/.test(password());\n const hasUpperLower = () => /[a-z]/.test(password()) && /[A-Z]/.test(password());\n\n const strengthScore = () => {\n let score = 0;\n if (password().length === 0) return 0;\n if (hasMinLength()) score += 25;\n if (hasNumber()) score += 25;\n if (hasSpecial()) score += 25;\n if (hasUpperLower()) score += 25;\n return score;\n };\n\n const strengthLabel = () => {\n const score = strengthScore();\n if (score === 0) return \"None\";\n if (score <= 25) return \"Weak\";\n if (score <= 50) return \"Fair\";\n if (score <= 75) return \"Good\";\n return \"Strong\";\n };\n\n const strengthColorClass = () => {\n const score = strengthScore();\n if (score <= 25) return \"bg-destructive\";\n if (score <= 50) return \"bg-amber-500\";\n if (score <= 75) return \"bg-blue-500\";\n return \"bg-emerald-500\";\n };\n\n const strengthBadgeVariant = () => {\n const score = strengthScore();\n if (score <= 25) return \"destructive\";\n if (score <= 50) return \"outline\";\n return \"secondary\";\n };\n\n const isFormValid = () =>\n form.values().fullName.trim() !== \"\" &&\n form.values().email.trim() !== \"\" &&\n form.values().agreeTerms &&\n strengthScore() >= 50;\n\n return (\n <div class=\"@container w-full min-h-[600px] flex items-center justify-center p-4 sm:p-6 md:p-10\">\n <Card class=\"w-full max-w-lg border-border shadow-md bg-card\">\n {/* Card Header */}\n <CardHeader class=\"space-y-2 text-center pb-6\">\n <div class=\"mx-auto size-10 rounded-lg bg-primary/10 text-primary flex items-center justify-center mb-1\">\n <ShieldCheck class=\"size-5\" />\n </div>\n <CardTitle class=\"text-2xl font-bold tracking-tight\">Create your account</CardTitle>\n <CardDescription class=\"text-xs sm:text-sm\">\n Join thousands of developers building fast SolidJS interfaces\n </CardDescription>\n </CardHeader>\n\n <CardContent class=\"space-y-6\">\n {/* Social OAuth Buttons */}\n <div class=\"grid grid-cols-1 @xs:grid-cols-2 gap-2.5\">\n <Button variant=\"outline\" class=\"w-full justify-center text-xs h-9\">\n <svg class=\"size-4 mr-2 shrink-0\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <path d=\"M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z\" />\n </svg>\n GitHub\n </Button>\n <Button variant=\"outline\" class=\"w-full justify-center text-xs h-9\">\n <svg class=\"size-4 mr-2 shrink-0\" viewBox=\"0 0 24 24\">\n <path\n fill=\"#4285F4\"\n d=\"M23.745 12.27c0-.7-.06-1.4-.19-2.07H12v4.51h6.6c-.29 1.52-1.14 2.82-2.4 3.68v3.05h3.88c2.27-2.09 3.66-5.17 3.66-9.17z\"\n />\n <path\n fill=\"#34A853\"\n d=\"M12 24c3.24 0 5.95-1.08 7.93-2.91l-3.88-3.05c-1.08.72-2.45 1.16-4.05 1.16-3.12 0-5.77-2.1-6.72-4.93H1.25v3.15C3.26 21.36 7.33 24 12 24z\"\n />\n <path\n fill=\"#FBBC05\"\n d=\"M5.28 14.27c-.25-.72-.38-1.49-.38-2.27s.13-1.55.38-2.27V6.58H1.25C.45 8.18 0 10.03 0 12s.45 3.82 1.25 5.42l4.03-3.15z\"\n />\n <path\n fill=\"#EA4335\"\n d=\"M12 4.75c1.77 0 3.35.61 4.6 1.8l3.42-3.42C17.95 1.19 15.24 0 12 0 7.33 0 3.26 2.64 1.25 6.58l4.03 3.15c.95-2.83 3.6-4.98 6.72-4.98z\"\n />\n </svg>\n Google\n </Button>\n </div>\n\n <div class=\"relative\">\n <div class=\"absolute inset-0 flex items-center\">\n <Separator />\n </div>\n <div class=\"relative flex justify-center text-xs uppercase\">\n <span class=\"bg-card px-3 text-muted-foreground font-medium text-[11px]\">\n Or register with email\n </span>\n </div>\n </div>\n\n {/* Registration Form with Nikala UI Form & Field ecosystem */}\n <Form onSubmit={form.handleSubmit} loading={form.isSubmitting()} class=\"space-y-4\">\n <Field>\n <FieldLabel for=\"reg-name\" class=\"text-xs sm:text-sm\">Full Name</FieldLabel>\n <Input\n id=\"reg-name\"\n type=\"text\"\n placeholder=\"Niko Pirosmani\"\n value={form.values().fullName}\n onInput={form.handleChange(\"fullName\")}\n onBlur={form.handleBlur(\"fullName\")}\n autocomplete=\"name\"\n />\n <FormMessage form={form} name=\"fullName\" />\n </Field>\n\n <Field>\n <FieldLabel for=\"reg-email\" class=\"text-xs sm:text-sm\">Email Address</FieldLabel>\n <Input\n id=\"reg-email\"\n type=\"email\"\n placeholder=\"niko@nikala.dev\"\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 {/* Password with Strength Meter */}\n <Field class=\"space-y-1.5\">\n <div class=\"flex items-center justify-between\">\n <FieldLabel for=\"reg-password\" class=\"text-xs sm:text-sm\">Password</FieldLabel>\n <Show when={password().length > 0}>\n <Badge variant={strengthBadgeVariant()} class=\"text-[10px] px-1.5 py-0 font-mono\">\n {strengthLabel()}\n </Badge>\n </Show>\n </div>\n\n <div class=\"relative\">\n <Input\n id=\"reg-password\"\n type={showPassword() ? \"text\" : \"password\"}\n placeholder=\"Create a strong password\"\n value={form.values().password}\n onInput={form.handleChange(\"password\")}\n onBlur={form.handleBlur(\"password\")}\n autocomplete=\"new-password\"\n class=\"pr-10\"\n />\n <button\n type=\"button\"\n onClick={() => setShowPassword(!showPassword())}\n class=\"absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors p-1 cursor-pointer\"\n aria-label={showPassword() ? \"Hide password\" : \"Show password\"}\n >\n <Show when={showPassword()} fallback={<Eye class=\"size-4\" />}>\n <EyeOff class=\"size-4\" />\n </Show>\n </button>\n </div>\n <FormMessage form={form} name=\"password\" />\n\n {/* Reactive Password Strength Progress Bar */}\n <Show when={password().length > 0}>\n <Progress\n value={strengthScore()}\n class=\"h-1.5 mt-2\"\n indicatorClass={strengthColorClass()}\n />\n\n {/* Validation Checklist */}\n <div class=\"grid grid-cols-2 gap-1.5 pt-1 text-[11px] text-muted-foreground\">\n <div class={`flex items-center gap-1.5 ${hasMinLength() ? \"text-emerald-500 font-medium\" : \"\"}`}>\n <Show when={hasMinLength()} fallback={<X class=\"size-3 text-muted-foreground/60\" />}>\n <Check class=\"size-3\" />\n </Show>\n <span>8+ characters</span>\n </div>\n <div class={`flex items-center gap-1.5 ${hasNumber() ? \"text-emerald-500 font-medium\" : \"\"}`}>\n <Show when={hasNumber()} fallback={<X class=\"size-3 text-muted-foreground/60\" />}>\n <Check class=\"size-3\" />\n </Show>\n <span>At least 1 number</span>\n </div>\n <div class={`flex items-center gap-1.5 ${hasUpperLower() ? \"text-emerald-500 font-medium\" : \"\"}`}>\n <Show when={hasUpperLower()} fallback={<X class=\"size-3 text-muted-foreground/60\" />}>\n <Check class=\"size-3\" />\n </Show>\n <span>Uppercase & lowercase</span>\n </div>\n <div class={`flex items-center gap-1.5 ${hasSpecial() ? \"text-emerald-500 font-medium\" : \"\"}`}>\n <Show when={hasSpecial()} fallback={<X class=\"size-3 text-muted-foreground/60\" />}>\n <Check class=\"size-3\" />\n </Show>\n <span>1 special symbol</span>\n </div>\n </div>\n </Show>\n </Field>\n\n {/* Terms of Service Agreement */}\n <div class=\"space-y-1 pt-1\">\n <div class=\"flex items-start space-x-2.5\">\n <Checkbox\n id=\"reg-terms\"\n checked={form.values().agreeTerms}\n onChange={(checked) => form.setFieldValue(\"agreeTerms\", checked)}\n class=\"mt-0.5\"\n />\n <Label\n for=\"reg-terms\"\n class=\"text-xs font-normal text-muted-foreground cursor-pointer select-none leading-tight\"\n >\n I agree to the{\" \"}\n <a href=\"#terms\" class=\"text-primary font-medium underline hover:text-primary/80\">\n Terms of Service\n </a>{\" \"}\n and{\" \"}\n <a href=\"#privacy\" class=\"text-primary font-medium underline hover:text-primary/80\">\n Privacy Policy\n </a>.\n </Label>\n </div>\n <FormMessage form={form} name=\"agreeTerms\" />\n </div>\n\n {/* Submit Button */}\n <Button\n type=\"submit\"\n class=\"w-full mt-3 font-medium\"\n disabled={form.isSubmitting() || !isFormValid()}\n >\n <Show when={form.isSubmitting()} fallback={<>Create Account <ArrowRight class=\"ml-2 size-4\" /></>}>\n Creating account...\n </Show>\n </Button>\n </Form>\n </CardContent>\n\n {/* Card Footer */}\n <CardFooter class=\"justify-center border-t border-border/50 py-4 text-xs text-muted-foreground\">\n Already have an account?{\" \"}\n <a href=\"/blocks/login-01\" class=\"text-primary font-semibold hover:underline ml-1\">\n Sign in\n </a>\n </CardFooter>\n </Card>\n </div>\n );\n};\n\nexport default Register01;\n",
28
+ "type": "registry:block"
29
+ }
30
+ ]
31
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "review-card",
3
+ "title": "Review Card",
4
+ "description": "A versatile, structured card component for customer testimonials, product ratings, verified buyer badges, and social proof.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "class-variance-authority",
10
+ "lucide-solid"
11
+ ],
12
+ "registryDependencies": [
13
+ "avatar",
14
+ "rating"
15
+ ],
16
+ "files": [
17
+ {
18
+ "path": "ui/review-card.tsx",
19
+ "content": "import {\n splitProps,\n Show,\n type JSX,\n type ParentComponent,\n type Component,\n} from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { CheckCircle2 } from \"lucide-solid\";\nimport { cn } from \"@/lib/cn\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"./avatar\";\nimport { Rating, type RatingProps } from \"./rating\";\n\n/* --- 1. ReviewCard Root --- */\nexport const reviewCardVariants = cva(\n \"relative flex flex-col justify-between rounded-lg transition-all duration-200 overflow-hidden\",\n {\n variants: {\n variant: {\n default: \"border border-border bg-card text-card-foreground p-4 sm:p-5 shadow-2xs hover:shadow-xs\",\n bordered: \"border-2 border-border bg-background text-foreground p-4 sm:p-5\",\n flat: \"bg-muted/40 text-foreground p-4 sm:p-5\",\n glass: \"backdrop-blur-md bg-card/70 border border-border/80 text-card-foreground p-4 sm:p-5 shadow-2xs\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n);\n\nexport interface ReviewCardProps\n extends JSX.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof reviewCardVariants> {\n class?: string;\n}\n\nexport const ReviewCard: ParentComponent<ReviewCardProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"variant\", \"children\"]);\n\n return (\n <div\n class={cn(reviewCardVariants({ variant: local.variant }), local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 2. ReviewHeader --- */\nexport interface ReviewHeaderProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const ReviewHeader: ParentComponent<ReviewHeaderProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\"flex items-start justify-between gap-3 mb-3 min-w-0 w-full\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 3. ReviewProfile (Avatar + Names container) --- */\nexport interface ReviewProfileProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const ReviewProfile: ParentComponent<ReviewProfileProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div class={cn(\"flex items-start gap-2.5 min-w-0 flex-1 overflow-hidden\", local.class)} {...rest}>\n {local.children}\n </div>\n );\n};\n\n/* --- 4. ReviewAvatar --- */\nexport interface ReviewAvatarProps {\n src?: string;\n alt?: string;\n fallback?: string;\n class?: string;\n}\n\nexport const ReviewAvatar: Component<ReviewAvatarProps> = (props) => {\n const [local] = splitProps(props, [\"src\", \"alt\", \"fallback\", \"class\"]);\n\n return (\n <Avatar class={cn(\"size-9 shrink-0 mt-0.5\", local.class)}>\n <Show when={local.src}>\n <AvatarImage src={local.src!} alt={local.alt || \"Reviewer Avatar\"} />\n </Show>\n <AvatarFallback>\n {local.fallback || (local.alt ? local.alt.slice(0, 2).toUpperCase() : \"U\")}\n </AvatarFallback>\n </Avatar>\n );\n};\n\n/* --- 5. ReviewAuthor (Name & Subtitle/Role/Handle) --- */\nexport interface ReviewAuthorProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>, \"role\"> {\n name: string;\n username?: string;\n role?: string;\n verified?: boolean;\n class?: string;\n}\n\nexport const ReviewAuthor: Component<ReviewAuthorProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"name\",\n \"username\",\n \"role\",\n \"verified\",\n \"class\",\n ]);\n\n return (\n <div class={cn(\"flex flex-col min-w-0 flex-1 overflow-hidden\", local.class)} {...rest}>\n <div class=\"flex items-center gap-1 font-semibold text-sm leading-tight text-foreground truncate\">\n <span class=\"truncate\">{local.name}</span>\n <Show when={local.verified}>\n <CheckCircle2 class=\"size-3.5 text-primary shrink-0\" />\n </Show>\n </div>\n <Show when={local.username || local.role}>\n <p class=\"text-xs text-muted-foreground truncate mt-0.5\">\n {local.username || local.role}\n </p>\n </Show>\n </div>\n );\n};\n\n/* --- 6. ReviewRating (Composing standalone Rating) --- */\nexport interface ReviewRatingProps extends RatingProps {}\n\nexport const ReviewRating: Component<ReviewRatingProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"size\", \"readOnly\"]);\n\n return (\n <Rating\n size={local.size || \"sm\"}\n readOnly={local.readOnly !== undefined ? local.readOnly : true}\n class={cn(\"shrink-0 select-none ml-auto pt-0.5\", local.class)}\n {...rest}\n />\n );\n};\n\n/* --- 7. ReviewBody (Quote / Content) --- */\nexport interface ReviewBodyProps extends JSX.HTMLAttributes<HTMLParagraphElement> {\n class?: string;\n}\n\nexport const ReviewBody: ParentComponent<ReviewBodyProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <p\n class={cn(\"text-xs sm:text-sm text-muted-foreground leading-relaxed\", local.class)}\n {...rest}\n >\n {local.children}\n </p>\n );\n};\n\n/* --- 8. ReviewFooter --- */\nexport interface ReviewFooterProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const ReviewFooter: ParentComponent<ReviewFooterProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\n \"mt-3 pt-3 border-t border-border/40 flex items-center justify-between text-xs text-muted-foreground\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n",
20
+ "type": "registry:ui"
21
+ }
22
+ ]
23
+ }
@@ -14,7 +14,7 @@
14
14
  "files": [
15
15
  {
16
16
  "path": "ui/scroll-area.tsx",
17
- "content": "import {\n createSignal,\n createEffect,\n onCleanup,\n splitProps,\n type Component,\n type JSX,\n type Accessor,\n} from \"solid-js\";\nimport { createScrollPosition, createElementSize } from \"@nikala-ui/hooks\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface ScrollAreaProps extends JSX.HTMLAttributes<HTMLDivElement> {\n orientation?: \"vertical\" | \"horizontal\" | \"both\";\n scrollHideDelay?: number;\n class?: string;\n children?: JSX.Element;\n}\n\nexport const ScrollArea: Component<ScrollAreaProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"orientation\",\n \"scrollHideDelay\",\n \"class\",\n \"children\",\n ]);\n\n const orientation = () => local.orientation || \"vertical\";\n let viewportRef: HTMLDivElement | undefined;\n let verticalTrackRef: HTMLDivElement | undefined;\n let horizontalTrackRef: HTMLDivElement | undefined;\n\n const scrollPos = createScrollPosition({\n target: () => viewportRef,\n });\n\n const viewportSize = createElementSize(() => viewportRef);\n\n const [thumbHeight, setThumbHeight] = createSignal(0);\n const [thumbTop, setThumbTop] = createSignal(0);\n const [thumbWidth, setThumbWidth] = createSignal(0);\n const [thumbLeft, setThumbLeft] = createSignal(0);\n const [isDragging, setIsDragging] = createSignal(false);\n\n const updateThumbMetrics = () => {\n if (!viewportRef) return;\n\n const scrollHeight = viewportRef.scrollHeight;\n const clientHeight = viewportRef.clientHeight;\n const scrollWidth = viewportRef.scrollWidth;\n const clientWidth = viewportRef.clientWidth;\n\n const trackHeight = verticalTrackRef ? verticalTrackRef.clientHeight - 4 : clientHeight - 4;\n const trackWidth = horizontalTrackRef ? horizontalTrackRef.clientWidth - 4 : clientWidth - 4;\n\n if (scrollHeight > clientHeight && clientHeight > 0) {\n const vRatio = clientHeight / scrollHeight;\n const calculatedHeight = Math.max(vRatio * trackHeight, 20);\n const maxTop = trackHeight - calculatedHeight;\n const topPct = scrollPos.y() / (scrollHeight - clientHeight);\n setThumbHeight(calculatedHeight);\n setThumbTop(topPct * maxTop);\n } else {\n setThumbHeight(0);\n }\n\n if (scrollWidth > clientWidth && clientWidth > 0) {\n const hRatio = clientWidth / scrollWidth;\n const calculatedWidth = Math.max(hRatio * trackWidth, 20);\n const maxLeft = trackWidth - calculatedWidth;\n const leftPct = scrollPos.x() / (scrollWidth - clientWidth);\n setThumbWidth(calculatedWidth);\n setThumbLeft(leftPct * maxLeft);\n } else {\n setThumbWidth(0);\n }\n };\n\n createEffect(() => {\n viewportSize.width();\n viewportSize.height();\n scrollPos.x();\n scrollPos.y();\n updateThumbMetrics();\n });\n\n const handleVerticalThumbPointerDown = (e: PointerEvent) => {\n if (!viewportRef || !verticalTrackRef) return;\n e.preventDefault();\n e.stopPropagation();\n\n setIsDragging(true);\n const startY = e.clientY;\n const startScrollTop = viewportRef.scrollTop;\n const scrollHeight = viewportRef.scrollHeight;\n const clientHeight = viewportRef.clientHeight;\n const trackHeight = verticalTrackRef.clientHeight - 4;\n\n const maxScrollTop = scrollHeight - clientHeight;\n const maxThumbTop = trackHeight - thumbHeight();\n const ratio = maxThumbTop > 0 ? maxScrollTop / maxThumbTop : 0;\n\n const onPointerMove = (moveEvent: PointerEvent) => {\n const deltaY = moveEvent.clientY - startY;\n viewportRef!.scrollTop = Math.max(0, Math.min(maxScrollTop, startScrollTop + deltaY * ratio));\n };\n\n const onPointerUp = () => {\n setIsDragging(false);\n window.removeEventListener(\"pointermove\", onPointerMove);\n window.removeEventListener(\"pointerup\", onPointerUp);\n };\n\n window.addEventListener(\"pointermove\", onPointerMove);\n window.addEventListener(\"pointerup\", onPointerUp);\n };\n\n const handleHorizontalThumbPointerDown = (e: PointerEvent) => {\n if (!viewportRef || !horizontalTrackRef) return;\n e.preventDefault();\n e.stopPropagation();\n\n setIsDragging(true);\n const startX = e.clientX;\n const startScrollLeft = viewportRef.scrollLeft;\n const scrollWidth = viewportRef.scrollWidth;\n const clientWidth = viewportRef.clientWidth;\n const trackWidth = horizontalTrackRef.clientWidth - 4;\n\n const maxScrollLeft = scrollWidth - clientWidth;\n const maxThumbLeft = trackWidth - thumbWidth();\n const ratio = maxThumbLeft > 0 ? maxScrollLeft / maxThumbLeft : 0;\n\n const onPointerMove = (moveEvent: PointerEvent) => {\n const deltaX = moveEvent.clientX - startX;\n viewportRef!.scrollLeft = Math.max(0, Math.min(maxScrollLeft, startScrollLeft + deltaX * ratio));\n };\n\n const onPointerUp = () => {\n setIsDragging(false);\n window.removeEventListener(\"pointermove\", onPointerMove);\n window.removeEventListener(\"pointerup\", onPointerUp);\n };\n\n window.addEventListener(\"pointermove\", onPointerMove);\n window.addEventListener(\"pointerup\", onPointerUp);\n };\n\n const handleWheel = (e: WheelEvent) => {\n if (orientation() === \"horizontal\" && viewportRef) {\n if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {\n e.preventDefault();\n viewportRef.scrollLeft += e.deltaY;\n }\n }\n };\n\n return (\n <div\n class={cn(\"relative overflow-hidden group/scroll-area\", local.class)}\n onWheel={handleWheel}\n {...rest}\n >\n <div\n ref={viewportRef}\n class=\"h-full w-full overflow-auto scrollbar-none rounded-[inherit]\"\n style={{\n \"scrollbar-width\": \"none\",\n \"-ms-overflow-style\": \"none\",\n }}\n >\n {local.children}\n </div>\n\n {(orientation() === \"vertical\" || orientation() === \"both\") && thumbHeight() > 0 && (\n <div\n ref={verticalTrackRef}\n class={cn(\n \"absolute right-0 top-0 bottom-0 w-2.5 p-0.5 select-none transition-opacity duration-300 pointer-events-none\",\n scrollPos.isScrolling() || isDragging()\n ? \"opacity-100\"\n : \"opacity-0 group-hover/scroll-area:opacity-100\"\n )}\n >\n <div\n class=\"w-1.5 rounded-full bg-border hover:bg-muted-foreground/50 transition-colors cursor-pointer pointer-events-auto\"\n style={{\n height: `${thumbHeight()}px`,\n transform: `translateY(${thumbTop()}px)`,\n }}\n onPointerDown={handleVerticalThumbPointerDown}\n />\n </div>\n )}\n\n {(orientation() === \"horizontal\" || orientation() === \"both\") && thumbWidth() > 0 && (\n <div\n ref={horizontalTrackRef}\n class={cn(\n \"absolute bottom-0 left-0 right-0 h-2.5 p-0.5 select-none transition-opacity duration-300 pointer-events-none\",\n scrollPos.isScrolling() || isDragging()\n ? \"opacity-100\"\n : \"opacity-0 group-hover/scroll-area:opacity-100\"\n )}\n >\n <div\n class=\"h-1.5 rounded-full bg-border hover:bg-muted-foreground/50 transition-colors cursor-pointer pointer-events-auto\"\n style={{\n width: `${thumbWidth()}px`,\n transform: `translateX(${thumbLeft()}px)`,\n }}\n onPointerDown={handleHorizontalThumbPointerDown}\n />\n </div>\n )}\n </div>\n );\n};\n",
17
+ "content": "import {\n createSignal,\n createEffect,\n onCleanup,\n splitProps,\n type Component,\n type JSX,\n type Accessor,\n} from \"solid-js\";\nimport { createScrollPosition, createElementSize } from \"@nikala-ui/hooks\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface ScrollAreaProps extends JSX.HTMLAttributes<HTMLDivElement> {\n orientation?: \"vertical\" | \"horizontal\" | \"both\";\n scrollHideDelay?: number;\n class?: string;\n children?: JSX.Element;\n}\n\nexport const ScrollArea: Component<ScrollAreaProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"orientation\",\n \"scrollHideDelay\",\n \"class\",\n \"children\",\n ]);\n\n const orientation = () => local.orientation || \"vertical\";\n let viewportRef: HTMLDivElement | undefined;\n let verticalTrackRef: HTMLDivElement | undefined;\n let horizontalTrackRef: HTMLDivElement | undefined;\n\n const scrollPos = createScrollPosition({\n target: () => viewportRef,\n });\n\n const viewportSize = createElementSize(() => viewportRef);\n\n const [thumbHeight, setThumbHeight] = createSignal(0);\n const [thumbTop, setThumbTop] = createSignal(0);\n const [thumbWidth, setThumbWidth] = createSignal(0);\n const [thumbLeft, setThumbLeft] = createSignal(0);\n const [isDragging, setIsDragging] = createSignal(false);\n\n const updateThumbMetrics = () => {\n if (!viewportRef) return;\n\n const scrollHeight = viewportRef.scrollHeight;\n const clientHeight = viewportRef.clientHeight;\n const scrollWidth = viewportRef.scrollWidth;\n const clientWidth = viewportRef.clientWidth;\n\n const trackHeight = verticalTrackRef ? verticalTrackRef.clientHeight - 4 : clientHeight - 4;\n const trackWidth = horizontalTrackRef ? horizontalTrackRef.clientWidth - 4 : clientWidth - 4;\n\n if (scrollHeight > clientHeight && clientHeight > 0) {\n const vRatio = clientHeight / scrollHeight;\n const calculatedHeight = Math.max(vRatio * trackHeight, 20);\n const maxTop = trackHeight - calculatedHeight;\n const topPct = scrollPos.y() / (scrollHeight - clientHeight);\n setThumbHeight(calculatedHeight);\n setThumbTop(topPct * maxTop);\n } else {\n setThumbHeight(0);\n }\n\n if (scrollWidth > clientWidth && clientWidth > 0) {\n const hRatio = clientWidth / scrollWidth;\n const calculatedWidth = Math.max(hRatio * trackWidth, 20);\n const maxLeft = trackWidth - calculatedWidth;\n const leftPct = scrollPos.x() / (scrollWidth - clientWidth);\n setThumbWidth(calculatedWidth);\n setThumbLeft(leftPct * maxLeft);\n } else {\n setThumbWidth(0);\n }\n };\n\n createEffect(() => {\n viewportSize.width();\n viewportSize.height();\n scrollPos.x();\n scrollPos.y();\n updateThumbMetrics();\n });\n\n const handleVerticalThumbPointerDown = (e: PointerEvent) => {\n if (!viewportRef || !verticalTrackRef) return;\n e.preventDefault();\n e.stopPropagation();\n\n setIsDragging(true);\n const startY = e.clientY;\n const startScrollTop = viewportRef.scrollTop;\n const scrollHeight = viewportRef.scrollHeight;\n const clientHeight = viewportRef.clientHeight;\n const trackHeight = verticalTrackRef.clientHeight - 4;\n\n const maxScrollTop = scrollHeight - clientHeight;\n const maxThumbTop = trackHeight - thumbHeight();\n const ratio = maxThumbTop > 0 ? maxScrollTop / maxThumbTop : 0;\n\n const onPointerMove = (moveEvent: PointerEvent) => {\n const deltaY = moveEvent.clientY - startY;\n viewportRef!.scrollTop = Math.max(0, Math.min(maxScrollTop, startScrollTop + deltaY * ratio));\n };\n\n const onPointerUp = () => {\n setIsDragging(false);\n window.removeEventListener(\"pointermove\", onPointerMove);\n window.removeEventListener(\"pointerup\", onPointerUp);\n };\n\n window.addEventListener(\"pointermove\", onPointerMove);\n window.addEventListener(\"pointerup\", onPointerUp);\n };\n\n const handleHorizontalThumbPointerDown = (e: PointerEvent) => {\n if (!viewportRef || !horizontalTrackRef) return;\n e.preventDefault();\n e.stopPropagation();\n\n setIsDragging(true);\n const startX = e.clientX;\n const startScrollLeft = viewportRef.scrollLeft;\n const scrollWidth = viewportRef.scrollWidth;\n const clientWidth = viewportRef.clientWidth;\n const trackWidth = horizontalTrackRef.clientWidth - 4;\n\n const maxScrollLeft = scrollWidth - clientWidth;\n const maxThumbLeft = trackWidth - thumbWidth();\n const ratio = maxThumbLeft > 0 ? maxScrollLeft / maxThumbLeft : 0;\n\n const onPointerMove = (moveEvent: PointerEvent) => {\n const deltaX = moveEvent.clientX - startX;\n viewportRef!.scrollLeft = Math.max(0, Math.min(maxScrollLeft, startScrollLeft + deltaX * ratio));\n };\n\n const onPointerUp = () => {\n setIsDragging(false);\n window.removeEventListener(\"pointermove\", onPointerMove);\n window.removeEventListener(\"pointerup\", onPointerUp);\n };\n\n window.addEventListener(\"pointermove\", onPointerMove);\n window.addEventListener(\"pointerup\", onPointerUp);\n };\n\n const handleWheel = (e: WheelEvent) => {\n if (orientation() === \"horizontal\" && viewportRef) {\n if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {\n e.preventDefault();\n viewportRef.scrollLeft += e.deltaY;\n }\n }\n };\n\n return (\n <div\n class={cn(\"relative overflow-hidden group/scroll-area\", local.class)}\n onWheel={handleWheel}\n {...rest}\n >\n <div\n ref={viewportRef}\n class=\"h-full w-full overflow-auto scrollbar-none rounded-[inherit]\"\n style={{\n \"scrollbar-width\": \"none\",\n \"-ms-overflow-style\": \"none\",\n }}\n >\n {local.children}\n </div>\n\n {(orientation() === \"vertical\" || orientation() === \"both\") && thumbHeight() > 0 && (\n <div\n ref={verticalTrackRef}\n class={cn(\n \"absolute right-0 top-0 bottom-0 w-2.5 p-0.5 select-none transition-opacity duration-300 pointer-events-none\",\n scrollPos.isScrolling() || isDragging()\n ? \"opacity-100\"\n : \"opacity-0 group-hover/scroll-area:opacity-100\"\n )}\n >\n <div\n class=\"w-1.5 rounded-lg bg-border hover:bg-muted-foreground/50 transition-colors cursor-pointer pointer-events-auto\"\n style={{\n height: `${thumbHeight()}px`,\n transform: `translateY(${thumbTop()}px)`,\n }}\n onPointerDown={handleVerticalThumbPointerDown}\n />\n </div>\n )}\n\n {(orientation() === \"horizontal\" || orientation() === \"both\") && thumbWidth() > 0 && (\n <div\n ref={horizontalTrackRef}\n class={cn(\n \"absolute bottom-0 left-0 right-0 h-2.5 p-0.5 select-none transition-opacity duration-300 pointer-events-none\",\n scrollPos.isScrolling() || isDragging()\n ? \"opacity-100\"\n : \"opacity-0 group-hover/scroll-area:opacity-100\"\n )}\n >\n <div\n class=\"h-1.5 rounded-lg bg-border hover:bg-muted-foreground/50 transition-colors cursor-pointer pointer-events-auto\"\n style={{\n width: `${thumbWidth()}px`,\n transform: `translateX(${thumbLeft()}px)`,\n }}\n onPointerDown={handleHorizontalThumbPointerDown}\n />\n </div>\n )}\n </div>\n );\n};\n",
18
18
  "type": "registry:ui"
19
19
  }
20
20
  ]
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "sidebar",
3
+ "title": "Sidebar",
4
+ "description": "A composable, collapsible, and accessible application sidebar navigation suite with icon mode, mobile drawer, and keyboard shortcuts.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "class-variance-authority",
10
+ "lucide-solid"
11
+ ],
12
+ "registryDependencies": [
13
+ "tooltip",
14
+ "skeleton",
15
+ "separator"
16
+ ],
17
+ "files": [
18
+ {
19
+ "path": "ui/sidebar.tsx",
20
+ "content": "import {\n createContext,\n createSignal,\n createMemo,\n createEffect,\n useContext,\n splitProps,\n onMount,\n onCleanup,\n type Component,\n type JSX,\n type ParentComponent,\n type Accessor,\n Show,\n} from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { PanelLeft, PanelRight } from \"lucide-solid\";\nimport { Tooltip, TooltipTrigger, TooltipContent } from \"./tooltip\";\nimport { Skeleton } from \"./skeleton\";\nimport { Separator } from \"./separator\";\nimport { cn } from \"@/lib/cn\";\n\n/* --- Constants --- */\nconst SIDEBAR_COOKIE_NAME = \"nikala_sidebar_state\";\nconst SIDEBAR_WIDTH = \"17rem\";\nconst SIDEBAR_WIDTH_ICON = \"3.5rem\";\nconst SIDEBAR_KEYBOARD_SHORTCUT = \"b\";\n\n/* --- 1. Context & Types --- */\nexport type SidebarState = \"expanded\" | \"collapsed\";\nexport type SidebarSide = \"left\" | \"right\";\n\nexport interface SidebarContextValue {\n state: Accessor<SidebarState>;\n open: Accessor<boolean>;\n setOpen: (open: boolean) => void;\n openMobile: Accessor<boolean>;\n setOpenMobile: (open: boolean) => void;\n isMobile: Accessor<boolean>;\n toggleSidebar: () => void;\n side: Accessor<SidebarSide>;\n setSide: (side: SidebarSide) => void;\n}\n\nconst SidebarContext = createContext<SidebarContextValue>();\n\nconst fallbackSidebarContext: SidebarContextValue = {\n state: () => \"expanded\",\n open: () => true,\n setOpen: () => {},\n openMobile: () => false,\n setOpenMobile: () => {},\n isMobile: () => false,\n toggleSidebar: () => {},\n side: () => \"left\",\n setSide: () => {},\n};\n\nexport function useSidebar() {\n const context = useContext(SidebarContext);\n return context ?? fallbackSidebarContext;\n}\n\n/* --- 2. SidebarProvider --- */\nexport interface SidebarProviderProps extends JSX.HTMLAttributes<HTMLDivElement> {\n defaultOpen?: boolean;\n open?: Accessor<boolean>;\n onOpenChange?: (open: boolean) => void;\n side?: SidebarSide;\n class?: string;\n style?: JSX.CSSProperties;\n}\n\nexport const SidebarProvider: ParentComponent<SidebarProviderProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"defaultOpen\",\n \"open\",\n \"onOpenChange\",\n \"side\",\n \"class\",\n \"style\",\n \"children\",\n ]);\n\n const [internalOpen, setInternalOpen] = createSignal<boolean>(local.defaultOpen ?? true);\n const [openMobile, setOpenMobile] = createSignal<boolean>(false);\n const [isMobile, setIsMobile] = createSignal<boolean>(false);\n const [side, setSide] = createSignal<SidebarSide>(local.side ?? \"left\");\n\n createEffect(() => {\n if (local.side) {\n setSide(local.side);\n }\n });\n\n const open = () => (local.open !== undefined ? local.open() : internalOpen());\n\n const setOpen = (value: boolean) => {\n if (local.open === undefined) {\n setInternalOpen(value);\n }\n local.onOpenChange?.(value);\n\n if (typeof document !== \"undefined\") {\n document.cookie = `${SIDEBAR_COOKIE_NAME}=${value}; path=/; max-age=${60 * 60 * 24 * 7}`;\n }\n };\n\n const toggleSidebar = () => {\n setOpen(!open());\n };\n\n const state = createMemo<SidebarState>(() => (open() ? \"expanded\" : \"collapsed\"));\n\n onMount(() => {\n if (typeof window === \"undefined\") return;\n\n const checkMobile = () => {\n setIsMobile(window.innerWidth < 768);\n };\n\n checkMobile();\n window.addEventListener(\"resize\", checkMobile, { passive: true });\n\n // Keyboard shortcut (⌘B / Ctrl+B)\n const handleKeyDown = (e: KeyboardEvent) => {\n if (\n (e.metaKey || e.ctrlKey) &&\n e.key.toLowerCase() === SIDEBAR_KEYBOARD_SHORTCUT &&\n !e.defaultPrevented\n ) {\n e.preventDefault();\n toggleSidebar();\n }\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n\n onCleanup(() => {\n window.removeEventListener(\"resize\", checkMobile);\n window.removeEventListener(\"keydown\", handleKeyDown);\n });\n });\n\n const contextValue: SidebarContextValue = {\n state,\n open,\n setOpen,\n openMobile,\n setOpenMobile,\n isMobile,\n toggleSidebar,\n side,\n setSide,\n };\n\n return (\n <SidebarContext.Provider value={contextValue}>\n <div\n style={{\n \"--sidebar-width\": SIDEBAR_WIDTH,\n \"--sidebar-width-icon\": SIDEBAR_WIDTH_ICON,\n ...(typeof local.style === \"object\" ? local.style : {}),\n } as JSX.CSSProperties}\n class={cn(\n \"group/sidebar-wrapper relative flex w-full max-w-full\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n </SidebarContext.Provider>\n );\n};\n\n/* --- 3. Sidebar Root --- */\nexport interface SidebarProps extends JSX.HTMLAttributes<HTMLDivElement> {\n side?: SidebarSide;\n variant?: \"sidebar\" | \"floating\" | \"inset\";\n collapsible?: \"offcanvas\" | \"icon\" | \"none\";\n class?: string;\n}\n\nexport const Sidebar: ParentComponent<SidebarProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"side\",\n \"variant\",\n \"collapsible\",\n \"class\",\n \"children\",\n ]);\n\n const sidebar = useSidebar();\n const side = () => local.side ?? sidebar.side();\n const variant = () => local.variant ?? \"sidebar\";\n const collapsible = () => local.collapsible ?? \"icon\";\n\n createEffect(() => {\n if (local.side) {\n sidebar.setSide(local.side);\n }\n });\n\n const isCollapsed = () => sidebar.state() === \"collapsed\" && collapsible() !== \"none\";\n\n return (\n <div\n data-state={isCollapsed() ? \"collapsed\" : \"expanded\"}\n data-collapsible={isCollapsed() ? collapsible() : \"\"}\n data-variant={variant()}\n data-side={side()}\n class={cn(\n \"group relative flex flex-col bg-card text-card-foreground transition-all duration-200 ease-in-out shrink-0\",\n !isCollapsed()\n ? \"w-[var(--sidebar-width,17rem)]\"\n : collapsible() === \"icon\"\n ? \"w-[var(--sidebar-width-icon,3.5rem)]\"\n : \"w-0 overflow-hidden opacity-0 border-none\",\n // Side borders\n variant() === \"sidebar\" && (side() === \"left\" ? \"border-r border-border\" : \"border-l border-border\"),\n // Floating variant\n variant() === \"floating\" && \"m-2 rounded-lg border border-border shadow-md\",\n // Inset variant\n variant() === \"inset\" && \"m-2 rounded-lg border border-border/80 bg-card shadow-2xs\",\n local.class\n )}\n {...rest}\n >\n <div\n data-sidebar=\"sidebar\"\n class=\"flex h-full w-full flex-col overflow-hidden\"\n >\n {local.children}\n </div>\n </div>\n );\n};\n\n/* --- 4. SidebarTrigger --- */\nexport interface SidebarTriggerProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {\n class?: string;\n}\n\nexport const SidebarTrigger: Component<SidebarTriggerProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"onClick\"]);\n const { toggleSidebar, side } = useSidebar();\n\n return (\n <button\n type=\"button\"\n aria-label=\"Toggle Sidebar\"\n onClick={(e) => {\n if (typeof local.onClick === \"function\") {\n local.onClick(e);\n }\n toggleSidebar();\n }}\n class={cn(\n \"inline-flex size-7 items-center justify-center rounded-md border border-border/60 bg-background text-foreground hover:bg-muted focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring cursor-pointer transition-colors shadow-2xs\",\n local.class\n )}\n {...rest}\n >\n <Show when={side() === \"right\"} fallback={<PanelLeft class=\"size-4\" />}>\n <PanelRight class=\"size-4\" />\n </Show>\n <span class=\"sr-only\">Toggle Sidebar</span>\n </button>\n );\n};\n\n/* --- 5. SidebarRail --- */\nexport interface SidebarRailProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {\n class?: string;\n}\n\nexport const SidebarRail: Component<SidebarRailProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n const { toggleSidebar } = useSidebar();\n\n return (\n <button\n type=\"button\"\n data-sidebar=\"rail\"\n aria-label=\"Toggle Sidebar Rail\"\n tabIndex={-1}\n onClick={toggleSidebar}\n title=\"Toggle Sidebar\"\n class={cn(\n \"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex cursor-w-resize\",\n local.class\n )}\n {...rest}\n />\n );\n};\n\n/* --- 6. SidebarInset --- */\nexport interface SidebarInsetProps extends JSX.HTMLAttributes<HTMLElement> {\n class?: string;\n}\n\nexport const SidebarInset: ParentComponent<SidebarInsetProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <main\n class={cn(\n \"relative flex min-h-0 flex-1 flex-col bg-background\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </main>\n );\n};\n\n/* --- 7. Sidebar Structural Sections --- */\nexport interface SidebarSectionProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const SidebarHeader: ParentComponent<SidebarSectionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n data-sidebar=\"header\"\n class={cn(\"flex flex-col gap-2 p-3 border-b border-border/40 shrink-0 group-data-[collapsible=icon]:p-2 group-data-[collapsible=icon]:items-center\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\nexport const SidebarFooter: ParentComponent<SidebarSectionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n data-sidebar=\"footer\"\n class={cn(\"flex flex-col gap-2 p-3 mt-auto border-t border-border/40 shrink-0 group-data-[collapsible=icon]:p-2 group-data-[collapsible=icon]:items-center\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\nexport const SidebarSeparator: Component<SidebarSectionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <Separator\n data-sidebar=\"separator\"\n class={cn(\"mx-2 w-auto bg-border/50 my-1 group-data-[collapsible=icon]:mx-1\", local.class)}\n {...rest}\n />\n );\n};\n\nexport const SidebarContent: ParentComponent<SidebarSectionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n data-sidebar=\"content\"\n class={cn(\n \"flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto p-2 group-data-[collapsible=icon]:p-1 group-data-[collapsible=icon]:overflow-hidden group-data-[collapsible=icon]:items-center\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 8. Sidebar Groups --- */\nexport const SidebarGroup: ParentComponent<SidebarSectionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n data-sidebar=\"group\"\n class={cn(\"relative flex w-full min-w-0 flex-col p-1 group-data-[collapsible=icon]:p-0 group-data-[collapsible=icon]:items-center\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\nexport interface SidebarGroupLabelProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const SidebarGroupLabel: ParentComponent<SidebarGroupLabelProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n data-sidebar=\"group-label\"\n class={cn(\n \"flex h-7 shrink-0 items-center rounded-md px-2 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground outline-hidden transition-all duration-200 ease-linear group-data-[collapsible=icon]:hidden\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\nexport interface SidebarGroupActionProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {\n class?: string;\n}\n\nexport const SidebarGroupAction: ParentComponent<SidebarGroupActionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <button\n type=\"button\"\n data-sidebar=\"group-action\"\n class={cn(\n \"absolute right-3 top-3 flex size-5 items-center justify-center rounded-md p-0 text-muted-foreground outline-hidden transition-transform hover:bg-muted hover:text-foreground cursor-pointer group-data-[collapsible=icon]:hidden\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </button>\n );\n};\n\nexport const SidebarGroupContent: ParentComponent<SidebarSectionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n data-sidebar=\"group-content\"\n class={cn(\"w-full text-sm\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 9. Sidebar Menu --- */\nexport interface SidebarMenuProps extends JSX.HTMLAttributes<HTMLUListElement> {\n class?: string;\n}\n\nexport const SidebarMenu: ParentComponent<SidebarMenuProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <ul\n data-sidebar=\"menu\"\n class={cn(\"flex w-full min-w-0 flex-col gap-1 list-none p-0 m-0 group-data-[collapsible=icon]:items-center\", local.class)}\n {...rest}\n >\n {local.children}\n </ul>\n );\n};\n\nexport interface SidebarMenuItemProps extends JSX.HTMLAttributes<HTMLLIElement> {\n class?: string;\n}\n\nexport const SidebarMenuItem: ParentComponent<SidebarMenuItemProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <li\n data-sidebar=\"menu-item\"\n class={cn(\"group/menu-item relative flex w-full justify-center\", local.class)}\n {...rest}\n >\n {local.children}\n </li>\n );\n};\n\n/* --- 10. SidebarMenuButton --- */\nexport const sidebarMenuButtonVariants = cva(\n \"peer/menu-button flex w-full items-center gap-2.5 overflow-hidden rounded-md p-2 text-left text-sm font-medium outline-hidden ring-sidebar-ring transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 active:bg-muted disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-muted data-[active=true]:font-semibold data-[active=true]:text-foreground group-data-[collapsible=icon]:size-9 group-data-[collapsible=icon]:p-0 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:[&>span]:hidden cursor-pointer select-none\",\n {\n variants: {\n variant: {\n default: \"hover:bg-muted hover:text-foreground\",\n outline: \"bg-background shadow-2xs hover:bg-muted hover:text-foreground border border-border\",\n },\n size: {\n default: \"h-8 text-sm\",\n sm: \"h-7 text-xs\",\n lg: \"h-10 text-sm\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n);\n\nexport interface SidebarMenuButtonProps\n extends JSX.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof sidebarMenuButtonVariants> {\n isActive?: boolean;\n tooltip?: string;\n class?: string;\n}\n\nexport const SidebarMenuButton: ParentComponent<SidebarMenuButtonProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"isActive\",\n \"tooltip\",\n \"variant\",\n \"size\",\n \"class\",\n \"children\",\n ]);\n\n const sidebar = useSidebar();\n const isCollapsed = () => sidebar.state() === \"collapsed\";\n const tooltipPlacement = () => (sidebar.side() === \"right\" ? \"left\" : \"right\");\n\n const buttonElement = () => (\n <button\n type=\"button\"\n data-sidebar=\"menu-button\"\n data-size={local.size}\n data-active={local.isActive ? \"true\" : \"false\"}\n class={cn(\n sidebarMenuButtonVariants({ variant: local.variant, size: local.size }),\n local.class\n )}\n {...rest}\n >\n {local.children}\n </button>\n );\n\n return (\n <Show\n when={local.tooltip && isCollapsed() && !sidebar.isMobile()}\n fallback={buttonElement()}\n >\n <Tooltip placement={tooltipPlacement()} gutter={8} openDelay={100}>\n <TooltipTrigger as=\"div\" class=\"w-full flex items-center justify-center\">\n {buttonElement()}\n </TooltipTrigger>\n <TooltipContent>\n {local.tooltip}\n </TooltipContent>\n </Tooltip>\n </Show>\n );\n};\n\n/* --- 11. SidebarMenuAction & Badge --- */\nexport interface SidebarMenuActionProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {\n showOnHover?: boolean;\n class?: string;\n}\n\nexport const SidebarMenuAction: ParentComponent<SidebarMenuActionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"showOnHover\", \"class\", \"children\"]);\n\n return (\n <button\n type=\"button\"\n data-sidebar=\"menu-action\"\n class={cn(\n \"absolute right-1 top-1.5 flex size-5 items-center justify-center rounded-md p-0 text-muted-foreground outline-hidden transition-transform hover:bg-muted hover:text-foreground cursor-pointer group-data-[collapsible=icon]:hidden\",\n local.showOnHover && \"opacity-0 group-hover/menu-item:opacity-100 transition-opacity\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </button>\n );\n};\n\nexport interface SidebarMenuBadgeProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const SidebarMenuBadge: ParentComponent<SidebarMenuBadgeProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n data-sidebar=\"menu-badge\"\n class={cn(\n \"pointer-events-none absolute right-1.5 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1.5 text-[11px] font-medium tabular-nums bg-muted text-muted-foreground group-data-[collapsible=icon]:hidden\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\nexport interface SidebarMenuSkeletonProps extends JSX.HTMLAttributes<HTMLDivElement> {\n showIcon?: boolean;\n class?: string;\n}\n\nexport const SidebarMenuSkeleton: Component<SidebarMenuSkeletonProps> = (props) => {\n const [local, rest] = splitProps(props, [\"showIcon\", \"class\"]);\n\n return (\n <div\n data-sidebar=\"menu-skeleton\"\n class={cn(\"flex h-8 items-center gap-2 rounded-md px-2 group-data-[collapsible=icon]:p-0 group-data-[collapsible=icon]:justify-center\", local.class)}\n {...rest}\n >\n <Show when={local.showIcon ?? true}>\n <Skeleton class=\"size-4 rounded-md shrink-0\" />\n </Show>\n <Skeleton class=\"h-4 flex-1 max-w-[80%] group-data-[collapsible=icon]:hidden\" />\n </div>\n );\n};\n\n/* --- 12. Nested Submenus --- */\nexport interface SidebarMenuSubProps extends JSX.HTMLAttributes<HTMLUListElement> {\n class?: string;\n}\n\nexport const SidebarMenuSub: ParentComponent<SidebarMenuSubProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <ul\n data-sidebar=\"menu-sub\"\n class={cn(\n \"mx-3.5 flex min-w-0 flex-col gap-1 border-l border-border/60 px-2.5 py-0.5 list-none group-data-[collapsible=icon]:hidden\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </ul>\n );\n};\n\nexport const SidebarMenuSubItem: ParentComponent<SidebarMenuItemProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <li class={cn(\"relative\", local.class)} {...rest}>\n {local.children}\n </li>\n );\n};\n\nexport interface SidebarMenuSubButtonProps extends JSX.AnchorHTMLAttributes<HTMLAnchorElement> {\n isActive?: boolean;\n size?: \"sm\" | \"md\";\n class?: string;\n}\n\nexport const SidebarMenuSubButton: ParentComponent<SidebarMenuSubButtonProps> = (props) => {\n const [local, rest] = splitProps(props, [\"isActive\", \"size\", \"class\", \"children\"]);\n\n return (\n <a\n data-sidebar=\"menu-sub-button\"\n data-size={local.size ?? \"md\"}\n data-active={local.isActive ? \"true\" : \"false\"}\n class={cn(\n \"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-xs text-muted-foreground outline-hidden hover:bg-muted hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring data-[active=true]:bg-muted data-[active=true]:font-medium data-[active=true]:text-foreground\",\n local.size === \"sm\" && \"text-[11px]\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </a>\n );\n};\n",
21
+ "type": "registry:ui"
22
+ }
23
+ ]
24
+ }
@@ -11,7 +11,7 @@
11
11
  "files": [
12
12
  {
13
13
  "path": "ui/spinner.tsx",
14
- "content": "import { splitProps, type Component, type JSX } from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\nexport const spinnerVariants = cva(\n \"inline-block animate-spin rounded-full border-2 border-current border-r-transparent text-primary\",\n {\n variants: {\n size: {\n sm: \"size-3\",\n default: \"size-4\",\n lg: \"size-6\",\n },\n },\n defaultVariants: {\n size: \"default\",\n },\n }\n);\n\nexport interface SpinnerProps\n extends Omit<JSX.HTMLAttributes<HTMLSpanElement>, \"role\">,\n VariantProps<typeof spinnerVariants> {\n /** Accessible text announced while the spinner is active. */\n label?: string;\n class?: string;\n}\n\n/** A compact, accessible loading indicator for async UI states. */\nexport const Spinner: Component<SpinnerProps> = (props) => {\n const [local, rest] = splitProps(props, [\"size\", \"label\", \"class\"]);\n\n return (\n <span\n role=\"status\"\n aria-label={local.label || \"Loading\"}\n class={cn(spinnerVariants({ size: local.size }), local.class)}\n {...rest}\n />\n );\n};\n",
14
+ "content": "import { splitProps, type Component, type JSX } from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\nexport const spinnerVariants = cva(\n \"inline-block animate-spin rounded-lg border-2 border-current border-r-transparent text-primary\",\n {\n variants: {\n size: {\n sm: \"size-3\",\n default: \"size-4\",\n lg: \"size-6\",\n },\n },\n defaultVariants: {\n size: \"default\",\n },\n }\n);\n\nexport interface SpinnerProps\n extends Omit<JSX.HTMLAttributes<HTMLSpanElement>, \"role\">,\n VariantProps<typeof spinnerVariants> {\n /** Accessible text announced while the spinner is active. */\n label?: string;\n class?: string;\n}\n\n/** A compact, accessible loading indicator for async UI states. */\nexport const Spinner: Component<SpinnerProps> = (props) => {\n const [local, rest] = splitProps(props, [\"size\", \"label\", \"class\"]);\n\n return (\n <span\n role=\"status\"\n aria-label={local.label || \"Loading\"}\n class={cn(spinnerVariants({ size: local.size }), local.class)}\n {...rest}\n />\n );\n};\n",
15
15
  "type": "registry:ui"
16
16
  }
17
17
  ]
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "stat",
3
+ "title": "Stat",
4
+ "description": "Display key performance indicators, statistics, financial data, and metrics with trends and icons.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "class-variance-authority",
10
+ "lucide-solid"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "ui/stat.tsx",
15
+ "content": "import {\n splitProps,\n type JSX,\n type ParentComponent,\n} from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { TrendingUp, TrendingDown, Minus } from \"lucide-solid\";\nimport { cn } from \"@/lib/cn\";\n\n/* --- 1. Stat Group (Container Grid) --- */\nexport interface StatGroupProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n columns?: 1 | 2 | 3 | 4 | 5;\n}\n\nexport const StatGroup: ParentComponent<StatGroupProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"columns\", \"children\"]);\n\n const columnClass = () => {\n switch (local.columns) {\n case 1:\n return \"grid-cols-1\";\n case 2:\n return \"grid-cols-1 sm:grid-cols-2\";\n case 3:\n return \"grid-cols-1 sm:grid-cols-2 lg:grid-cols-3\";\n case 5:\n return \"grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5\";\n case 4:\n default:\n return \"grid-cols-1 sm:grid-cols-2 lg:grid-cols-4\";\n }\n };\n\n return (\n <div\n class={cn(\"grid gap-4 sm:gap-5 w-full\", columnClass(), local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 2. Stat Root Card --- */\nexport const statVariants = cva(\n \"relative flex flex-col justify-between rounded-lg transition-all duration-200\",\n {\n variants: {\n variant: {\n default: \"border border-border bg-card text-card-foreground p-4 sm:p-5 shadow-2xs space-y-2\",\n flat: \"bg-muted/40 text-foreground p-4 sm:p-5 space-y-2\",\n bordered: \"border-2 border-border bg-background text-foreground p-4 sm:p-5 space-y-2\",\n ghost: \"bg-transparent text-foreground p-2 space-y-1\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n);\n\nexport interface StatProps\n extends JSX.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof statVariants> {\n class?: string;\n}\n\nexport const Stat: ParentComponent<StatProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"variant\", \"children\"]);\n\n return (\n <div\n class={cn(statVariants({ variant: local.variant }), local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 3. Stat Header (Top Row) --- */\nexport interface StatHeaderProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const StatHeader: ParentComponent<StatHeaderProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\"flex items-center justify-between gap-2\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 4. Stat Label / Title --- */\nexport interface StatLabelProps extends JSX.HTMLAttributes<HTMLParagraphElement> {\n class?: string;\n}\n\nexport const StatLabel: ParentComponent<StatLabelProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <p\n class={cn(\n \"text-xs sm:text-sm font-medium text-muted-foreground leading-tight tracking-tight\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </p>\n );\n};\n\n/* --- 5. Stat Icon Wrapper --- */\nexport interface StatIconProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const StatIcon: ParentComponent<StatIconProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\n \"flex size-4 sm:size-4.5 shrink-0 items-center justify-center text-muted-foreground\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 6. Stat Value (Big Number) --- */\nexport interface StatValueProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const StatValue: ParentComponent<StatValueProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\n \"text-2xl sm:text-3xl font-bold tracking-tight text-foreground flex items-baseline gap-1 my-0.5\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 7. Stat Unit / Currency (Prefix or Suffix) --- */\nexport interface StatUnitProps extends JSX.HTMLAttributes<HTMLSpanElement> {\n class?: string;\n}\n\nexport const StatUnit: ParentComponent<StatUnitProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <span\n class={cn(\"text-lg sm:text-xl font-semibold text-muted-foreground self-baseline\", local.class)}\n {...rest}\n >\n {local.children}\n </span>\n );\n};\n\n/* --- 8. Stat Trend (Growth / Decline Indicator - No Background) --- */\nexport const statTrendVariants = cva(\n \"inline-flex items-center gap-1 text-xs font-semibold select-none shrink-0 tracking-tight\",\n {\n variants: {\n type: {\n up: \"text-emerald-600 dark:text-emerald-400\",\n down: \"text-red-600 dark:text-red-400\",\n neutral: \"text-muted-foreground\",\n },\n },\n defaultVariants: {\n type: \"up\",\n },\n }\n);\n\nexport interface StatTrendProps\n extends JSX.HTMLAttributes<HTMLSpanElement>,\n VariantProps<typeof statTrendVariants> {\n class?: string;\n hideIcon?: boolean;\n}\n\nexport const StatTrend: ParentComponent<StatTrendProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"type\", \"hideIcon\", \"children\"]);\n\n const Icon = () => {\n if (local.hideIcon) return null;\n if (local.type === \"down\") return <TrendingDown class=\"size-3.5 stroke-[2.5]\" />;\n if (local.type === \"neutral\") return <Minus class=\"size-3.5 stroke-[2.5]\" />;\n return <TrendingUp class=\"size-3.5 stroke-[2.5]\" />;\n };\n\n return (\n <span\n class={cn(statTrendVariants({ type: local.type }), local.class)}\n {...rest}\n >\n <Icon />\n <span>{local.children}</span>\n </span>\n );\n};\n\n/* --- 9. Stat Help Text / Bottom Row (Between Alignment) --- */\nexport interface StatHelpTextProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const StatHelpText: ParentComponent<StatHelpTextProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\n \"flex items-center justify-between gap-2 text-xs text-muted-foreground pt-1\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n",
16
+ "type": "registry:ui"
17
+ }
18
+ ]
19
+ }
@@ -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,18 @@
1
+ {
2
+ "name": "timeline",
3
+ "title": "Timeline",
4
+ "description": "A responsive chronological display for event streams, activity logs, order tracking, and multi-step workflows.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "class-variance-authority"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "ui/timeline.tsx",
14
+ "content": "import {\n createContext,\n useContext,\n splitProps,\n type Component,\n type JSX,\n type Accessor,\n} from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\n/* --- Timeline Context --- */\ninterface TimelineContextValue {\n orientation: Accessor<\"vertical\" | \"horizontal\">;\n align: Accessor<\"left\" | \"right\" | \"alternate\">;\n size: Accessor<\"sm\" | \"default\" | \"lg\">;\n}\n\nconst TimelineContext = createContext<TimelineContextValue>();\n\nexport interface TimelineProps extends JSX.HTMLAttributes<HTMLOListElement> {\n /** Layout orientation of the timeline */\n orientation?: \"vertical\" | \"horizontal\";\n /** Alignment of content relative to the timeline line */\n align?: \"left\" | \"right\" | \"alternate\";\n /** Sizing of dots and spacing */\n size?: \"sm\" | \"default\" | \"lg\";\n class?: string;\n children?: JSX.Element;\n}\n\n/**\n * Root container for chronological timeline event sequences.\n */\nexport const Timeline: Component<TimelineProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"orientation\",\n \"align\",\n \"size\",\n \"class\",\n \"children\",\n ]);\n\n const orientation = () => local.orientation ?? \"vertical\";\n const align = () => local.align ?? \"left\";\n const size = () => local.size ?? \"default\";\n\n const contextValue: TimelineContextValue = {\n orientation,\n align,\n size,\n };\n\n return (\n <TimelineContext.Provider value={contextValue}>\n <ol\n role=\"list\"\n data-orientation={orientation()}\n data-align={align()}\n class={cn(\n \"relative flex\",\n orientation() === \"vertical\"\n ? \"flex-col w-full\"\n : \"flex-row w-full items-start\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </ol>\n </TimelineContext.Provider>\n );\n};\n\n/* --- Timeline Item --- */\nexport interface TimelineItemProps extends JSX.HTMLAttributes<HTMLLIElement> {\n class?: string;\n children?: JSX.Element;\n}\n\n/**\n * Individual event row container in the timeline.\n */\nexport const TimelineItem: Component<TimelineItemProps> = (props) => {\n const context = useContext(TimelineContext);\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n const isVertical = () => !context || context.orientation() === \"vertical\";\n const align = () => context?.align() ?? \"left\";\n\n return (\n <li\n class={cn(\n \"group relative flex\",\n isVertical()\n ? \"min-h-[3.5rem] w-full items-start\"\n : \"flex-1 min-w-0 flex-col items-start\",\n isVertical() && align() === \"right\" && \"flex-row-reverse\",\n isVertical() && align() === \"alternate\" && \"[&:nth-child(even)]:flex-row-reverse\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </li>\n );\n};\n\n/* --- Timeline Separator (Dot + Connector wrapper) --- */\nexport interface TimelineSeparatorProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const TimelineSeparator: Component<TimelineSeparatorProps> = (props) => {\n const context = useContext(TimelineContext);\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n const isVertical = () => !context || context.orientation() === \"vertical\";\n\n return (\n <div\n aria-hidden=\"true\"\n class={cn(\n \"flex shrink-0 relative\",\n isVertical()\n ? \"flex-col items-center self-stretch\"\n : \"flex-row items-center w-full\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- Timeline Point / Dot --- */\nexport const timelinePointVariants = cva(\n \"relative z-10 flex shrink-0 items-center justify-center rounded-lg font-medium transition-all shadow-xs\",\n {\n variants: {\n variant: {\n default: \"border-2 bg-background\",\n solid: \"text-primary-foreground\",\n subtle: \"bg-muted text-muted-foreground\",\n outline: \"border-2 border-border bg-card text-foreground\",\n },\n status: {\n default: \"border-border text-foreground bg-card\",\n primary: \"border-primary bg-primary text-primary-foreground\",\n success: \"border-emerald-500 bg-emerald-500 text-white dark:border-emerald-400 dark:bg-emerald-400\",\n warning: \"border-amber-500 bg-amber-500 text-white dark:border-amber-400 dark:bg-amber-400\",\n destructive: \"border-destructive bg-destructive text-destructive-foreground\",\n muted: \"border-border/60 bg-muted text-muted-foreground\",\n },\n size: {\n sm: \"size-5 text-[10px] [&_svg]:size-3\",\n default: \"size-8 text-xs [&_svg]:size-4\",\n lg: \"size-10 text-sm [&_svg]:size-5\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n status: \"default\",\n size: \"default\",\n },\n }\n);\n\nexport interface TimelinePointProps\n extends JSX.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof timelinePointVariants> {\n class?: string;\n children?: JSX.Element;\n}\n\n/**\n * Status indicator node or icon container in the timeline separator.\n */\nexport const TimelinePoint: Component<TimelinePointProps> = (props) => {\n const context = useContext(TimelineContext);\n const [local, rest] = splitProps(props, [\n \"variant\",\n \"status\",\n \"size\",\n \"class\",\n \"children\",\n ]);\n\n const size = () => local.size || context?.size() || \"default\";\n\n return (\n <div\n class={cn(\n timelinePointVariants({\n variant: local.variant,\n status: local.status,\n size: size(),\n }),\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- Timeline Connector (Line track) --- */\nexport interface TimelineConnectorProps extends JSX.HTMLAttributes<HTMLDivElement> {\n dashed?: boolean;\n class?: string;\n}\n\n/**\n * Line track connecting sequential timeline items.\n */\nexport const TimelineConnector: Component<TimelineConnectorProps> = (props) => {\n const context = useContext(TimelineContext);\n const [local, rest] = splitProps(props, [\"dashed\", \"class\"]);\n\n const isVertical = () => !context || context.orientation() === \"vertical\";\n\n return (\n <div\n aria-hidden=\"true\"\n class={cn(\n \"transition-colors group-last:hidden\",\n isVertical()\n ? \"w-0.5 flex-1 min-h-6 my-1 bg-border\"\n : \"h-0.5 flex-1 min-w-4 mx-2 bg-border\",\n local.dashed && (\n isVertical()\n ? \"border-l-2 border-dashed border-border bg-transparent w-0\"\n : \"border-t-2 border-dashed border-border bg-transparent h-0\"\n ),\n local.class\n )}\n {...rest}\n />\n );\n};\n\n/* --- Timeline Content (Event Details) --- */\nexport interface TimelineContentProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\n/**\n * Primary container holding the description, title, and body for a timeline event.\n */\nexport const TimelineContent: Component<TimelineContentProps> = (props) => {\n const context = useContext(TimelineContext);\n const [local, rest] = splitProps(props, [\"class\"]);\n\n const isVertical = () => !context || context.orientation() === \"vertical\";\n const align = () => context?.align() ?? \"left\";\n\n return (\n <div\n class={cn(\n \"flex flex-col\",\n isVertical() ? \"flex-1 pb-6 pt-0.5\" : \"pt-2 pr-2 text-left\",\n isVertical() && align() === \"left\" && \"text-left pl-3.5 pr-0\",\n isVertical() && align() === \"right\" && \"text-right pr-3.5 pl-0\",\n isVertical() && align() === \"alternate\" && \"text-left pl-3.5 group-even:text-right group-even:pr-3.5 group-even:pl-0\",\n local.class\n )}\n {...rest}\n />\n );\n};\n\n/* --- Timeline Opposite Content --- */\nexport interface TimelineOppositeContentProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\n/**\n * Content placed on the opposite side of the timeline separator (e.g. timestamps in alternate layouts).\n */\nexport const TimelineOppositeContent: Component<TimelineOppositeContentProps> = (props) => {\n const context = useContext(TimelineContext);\n const [local, rest] = splitProps(props, [\"class\"]);\n\n const isVertical = () => !context || context.orientation() === \"vertical\";\n const align = () => context?.align() ?? \"left\";\n\n return (\n <div\n class={cn(\n \"flex flex-col text-xs text-muted-foreground\",\n align() !== \"alternate\" && \"hidden\",\n isVertical()\n ? \"flex-1 pb-6 pt-1 text-right pr-3.5 group-even:text-left group-even:pl-3.5 group-even:pr-0\"\n : \"pb-1 pr-2\",\n local.class\n )}\n {...rest}\n />\n );\n};\n\n/* --- Timeline Title --- */\nexport interface TimelineTitleProps extends JSX.HTMLAttributes<HTMLHeadingElement> {\n class?: string;\n}\n\nexport const TimelineTitle: Component<TimelineTitleProps> = (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\n/* --- Timeline Description --- */\nexport interface TimelineDescriptionProps extends JSX.HTMLAttributes<HTMLParagraphElement> {\n class?: string;\n}\n\nexport const TimelineDescription: Component<TimelineDescriptionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <p\n class={cn(\"mt-1 text-sm text-muted-foreground\", local.class)}\n {...rest}\n />\n );\n};\n\n/* --- Timeline Time --- */\nexport interface TimelineTimeProps extends JSX.HTMLAttributes<HTMLTimeElement> {\n class?: string;\n}\n\nexport const TimelineTime: Component<TimelineTimeProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <time\n class={cn(\"text-xs font-medium text-muted-foreground/80\", local.class)}\n {...rest}\n />\n );\n};\n",
15
+ "type": "registry:ui"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "toggle-group",
3
+ "title": "Toggle Group",
4
+ "description": "A set of two-state buttons that can be toggled on or off with single or multiple selection modes.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "class-variance-authority"
10
+ ],
11
+ "registryDependencies": [
12
+ "create-controllable-signal"
13
+ ],
14
+ "files": [
15
+ {
16
+ "path": "ui/toggle-group.tsx",
17
+ "content": "import {\n createContext,\n useContext,\n splitProps,\n type Component,\n type JSX,\n type Accessor,\n} from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { createControllableSignal } from \"@nikala-ui/hooks\";\nimport { cn } from \"@/lib/cn\";\n\nexport const toggleGroupItemVariants = cva(\n \"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 cursor-pointer\",\n {\n variants: {\n variant: {\n default: \"bg-transparent\",\n outline:\n \"border border-border bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground data-[state=on]:bg-accent data-[state=on]:text-accent-foreground\",\n },\n size: {\n default: \"h-9 px-3 min-w-9\",\n sm: \"h-8 px-2 text-xs min-w-8\",\n lg: \"h-10 px-3 min-w-10\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n);\n\ninterface ToggleGroupContextValue {\n type: \"single\" | \"multiple\";\n value: Accessor<any>;\n onItemSelect: (itemValue: string) => void;\n variant: Accessor<\"default\" | \"outline\">;\n size: Accessor<\"default\" | \"sm\" | \"lg\">;\n disabled: Accessor<boolean>;\n}\n\nconst ToggleGroupContext = createContext<ToggleGroupContextValue>();\n\nexport interface ToggleGroupProps<T extends \"single\" | \"multiple\" = \"single\">\n extends Omit<JSX.HTMLAttributes<HTMLDivElement>, \"onChange\">,\n VariantProps<typeof toggleGroupItemVariants> {\n /** Mode of selection: single choice or multiple choices */\n type?: T;\n /** Controlled value: string for 'single', string[] for 'multiple' */\n value?: T extends \"multiple\" ? string[] : string;\n /** Uncontrolled default value */\n defaultValue?: T extends \"multiple\" ? string[] : string;\n /** Callback fired when selection changes */\n onChange?: (value: T extends \"multiple\" ? string[] : string) => void;\n /** Layout orientation */\n orientation?: \"horizontal\" | \"vertical\";\n /** Disables all buttons in the group */\n disabled?: boolean;\n class?: string;\n children?: JSX.Element;\n}\n\n/**\n * Root container for grouping connected toggle items with shared single/multiple selection state.\n */\nexport const ToggleGroup = <T extends \"single\" | \"multiple\" = \"single\">(\n props: ToggleGroupProps<T>\n) => {\n const [local, rest] = splitProps(props, [\n \"type\",\n \"value\",\n \"defaultValue\",\n \"onChange\",\n \"orientation\",\n \"variant\",\n \"size\",\n \"disabled\",\n \"class\",\n \"children\",\n ]);\n\n const type = () => local.type ?? (\"single\" as T);\n const variant = () => local.variant ?? \"default\";\n const size = () => local.size ?? \"default\";\n const disabled = () => local.disabled ?? false;\n const orientation = () => local.orientation ?? \"horizontal\";\n\n const [currentValue, setCurrentValue] = createControllableSignal<any>({\n value: () => local.value,\n defaultValue: local.defaultValue ?? (type() === \"multiple\" ? [] : undefined),\n onChange: (val) => local.onChange?.(val),\n });\n\n const onItemSelect = (itemValue: string) => {\n if (disabled()) return;\n\n if (type() === \"multiple\") {\n const currentList = Array.isArray(currentValue()) ? currentValue() : [];\n if (currentList.includes(itemValue)) {\n setCurrentValue(currentList.filter((v: string) => v !== itemValue));\n } else {\n setCurrentValue([...currentList, itemValue]);\n }\n } else {\n const current = currentValue();\n if (current === itemValue) {\n setCurrentValue(undefined);\n } else {\n setCurrentValue(itemValue);\n }\n }\n };\n\n const contextValue: ToggleGroupContextValue = {\n type: type(),\n value: currentValue,\n onItemSelect,\n variant,\n size,\n disabled,\n };\n\n return (\n <ToggleGroupContext.Provider value={contextValue}>\n <div\n role=\"group\"\n data-orientation={orientation()}\n class={cn(\n \"flex items-center justify-center gap-1 rounded-lg\",\n orientation() === \"vertical\" ? \"flex-col\" : \"flex-row\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n </ToggleGroupContext.Provider>\n );\n};\n\nexport interface ToggleGroupItemProps\n extends JSX.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof toggleGroupItemVariants> {\n /** Unique value representing this toggle item within the group */\n value: string;\n class?: string;\n children?: JSX.Element;\n}\n\n/**\n * Individual toggle button item within a ToggleGroup.\n */\nexport const ToggleGroupItem: Component<ToggleGroupItemProps> = (props) => {\n const context = useContext(ToggleGroupContext);\n\n if (!context) {\n throw new Error(\"ToggleGroupItem must be used within a ToggleGroup\");\n }\n\n const [local, rest] = splitProps(props, [\n \"value\",\n \"variant\",\n \"size\",\n \"disabled\",\n \"class\",\n \"children\",\n ]);\n\n const isSelected = () => {\n const groupValue = context.value();\n if (context.type === \"multiple\") {\n return Array.isArray(groupValue) && groupValue.includes(local.value);\n }\n return groupValue === local.value;\n };\n\n const isDisabled = () => local.disabled || context.disabled();\n const itemVariant = () => local.variant || context.variant();\n const itemSize = () => local.size || context.size();\n\n return (\n <button\n type=\"button\"\n role={context.type === \"single\" ? \"radio\" : \"checkbox\"}\n aria-checked={isSelected()}\n data-state={isSelected() ? \"on\" : \"off\"}\n disabled={isDisabled()}\n onClick={() => context.onItemSelect(local.value)}\n class={cn(\n toggleGroupItemVariants({\n variant: itemVariant(),\n size: itemSize(),\n }),\n local.class\n )}\n {...rest}\n >\n {local.children}\n </button>\n );\n};\n",
18
+ "type": "registry:ui"
19
+ }
20
+ ]
21
+ }