@nikala-ui/core 0.10.1-nightly.4e09f2a → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/registry/bubble.json +18 -0
- package/registry/create-chat-scroll.json +13 -0
- package/registry/create-pagination.json +13 -0
- package/registry/footer.json +18 -0
- package/registry/forgot-password-01.json +28 -0
- package/registry/hero-01.json +22 -0
- package/registry/index.json +288 -0
- package/registry/login-01.json +28 -0
- package/registry/marker.json +17 -0
- package/registry/marquee.json +17 -0
- package/registry/message.json +17 -0
- package/registry/navbar.json +19 -0
- package/registry/navigation-menu.json +22 -0
- package/registry/otp-verification-01.json +26 -0
- package/registry/pagination.json +22 -0
- package/registry/rating.json +19 -0
- package/registry/register-01.json +31 -0
- package/registry/review-card.json +23 -0
- package/registry/scroll-area.json +1 -1
- package/registry/sidebar.json +24 -0
- package/registry/spinner.json +1 -1
- package/registry/stat.json +19 -0
- package/registry/timeline.json +18 -0
- package/registry/toggle-group.json +21 -0
- package/src/registry/blocks/forgot-password-01.tsx +141 -0
- package/src/registry/blocks/hero-01.tsx +28 -0
- package/src/registry/blocks/login-01.tsx +231 -0
- package/src/registry/blocks/otp-verification-01.tsx +170 -0
- package/src/registry/blocks/register-01.tsx +318 -0
- package/src/registry/components/ui/bubble.tsx +142 -0
- package/src/registry/components/ui/footer.tsx +247 -0
- package/src/registry/components/ui/marker.tsx +102 -0
- package/src/registry/components/ui/marquee.tsx +118 -0
- package/src/registry/components/ui/message.tsx +168 -0
- package/src/registry/components/ui/navbar.tsx +368 -0
- package/src/registry/components/ui/navigation-menu.tsx +358 -0
- package/src/registry/components/ui/pagination.tsx +258 -0
- package/src/registry/components/ui/rating.tsx +185 -0
- package/src/registry/components/ui/review-card.tsx +195 -0
- package/src/registry/components/ui/scroll-area.tsx +2 -2
- package/src/registry/components/ui/sidebar.tsx +692 -0
- package/src/registry/components/ui/spinner.tsx +1 -1
- package/src/registry/components/ui/stat.tsx +245 -0
- package/src/registry/components/ui/timeline.tsx +350 -0
- package/src/registry/components/ui/toggle-group.tsx +203 -0
- package/src/registry/index.ts +3 -3
- package/src/registry/metadata.ts +120 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "marquee",
|
|
3
|
+
"title": "Marquee",
|
|
4
|
+
"description": "A smooth, GPU-accelerated infinite scrolling ticker component for logo clouds, testimonials, and live ribbons.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "ui/marquee.tsx",
|
|
13
|
+
"content": "import {\n splitProps,\n createMemo,\n Index,\n type JSX,\n type ParentComponent,\n} from \"solid-js\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface MarqueeProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n reverse?: boolean;\n pauseOnHover?: boolean;\n vertical?: boolean;\n repeat?: number;\n duration?: number | string;\n gap?: string;\n fadeEdges?: boolean;\n}\n\nexport const Marquee: ParentComponent<MarqueeProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"class\",\n \"reverse\",\n \"pauseOnHover\",\n \"vertical\",\n \"repeat\",\n \"duration\",\n \"gap\",\n \"fadeEdges\",\n \"children\",\n ]);\n\n const repeatCount = () => local.repeat ?? 4;\n const durationValue = () =>\n typeof local.duration === \"number\"\n ? `${local.duration}s`\n : local.duration || \"40s\";\n const gapValue = () => local.gap || \"1rem\";\n\n const repeatArray = createMemo(() =>\n Array.from({ length: repeatCount() }, (_, i) => i)\n );\n\n return (\n <>\n <style>{`\n @keyframes nikala-marquee {\n from {\n transform: translateX(0);\n }\n to {\n transform: translateX(calc(-100% - var(--marquee-gap, 1rem)));\n }\n }\n @keyframes nikala-marquee-vertical {\n from {\n transform: translateY(0);\n }\n to {\n transform: translateY(calc(-100% - var(--marquee-gap, 1rem)));\n }\n }\n .nikala-marquee-track {\n animation: nikala-marquee var(--marquee-duration, 40s) linear infinite;\n }\n .nikala-marquee-track-vertical {\n animation: nikala-marquee-vertical var(--marquee-duration, 40s) linear infinite;\n }\n [data-reverse=\"true\"] .nikala-marquee-track,\n [data-reverse=\"true\"] .nikala-marquee-track-vertical {\n animation-direction: reverse;\n }\n [data-pause=\"true\"]:hover .nikala-marquee-track,\n [data-pause=\"true\"]:hover .nikala-marquee-track-vertical {\n animation-play-state: paused;\n }\n `}</style>\n <div\n data-reverse={local.reverse ? \"true\" : \"false\"}\n data-pause={local.pauseOnHover ? \"true\" : \"false\"}\n style={{\n \"--marquee-duration\": durationValue(),\n \"--marquee-gap\": gapValue(),\n }}\n class={cn(\n \"group flex overflow-hidden p-2 select-none\",\n local.vertical ? \"flex-col h-full\" : \"flex-row w-full\",\n local.fadeEdges &&\n (local.vertical\n ? \"[mask-image:linear-gradient(to_bottom,transparent,black_10%,black_90%,transparent)]\"\n : \"[mask-image:linear-gradient(to_right,transparent,black_10%,black_90%,transparent)]\"),\n local.class\n )}\n {...rest}\n >\n <Index each={repeatArray()}>\n {() => (\n <div\n class={cn(\n \"flex shrink-0 justify-around items-center\",\n local.vertical\n ? \"flex-col min-h-full nikala-marquee-track-vertical\"\n : \"flex-row min-w-full nikala-marquee-track\"\n )}\n style={{\n gap: gapValue(),\n [local.vertical ? \"margin-bottom\" : \"margin-right\"]: gapValue(),\n }}\n >\n {local.children}\n </div>\n )}\n </Index>\n </div>\n </>\n );\n};\n",
|
|
14
|
+
"type": "registry:ui"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "message",
|
|
3
|
+
"title": "Message",
|
|
4
|
+
"description": "A structured chat and conversation message layout with avatars, alignment, headers, footers, and actions.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
{
|
|
12
|
+
"path": "ui/message.tsx",
|
|
13
|
+
"content": "import {\n createContext,\n useContext,\n splitProps,\n type Component,\n type JSX,\n type ParentComponent,\n type Accessor,\n} from \"solid-js\";\nimport { cn } from \"@/lib/cn\";\n\n/* --- 1. Message Context --- */\nexport type MessageAlignment = \"start\" | \"end\";\n\ninterface MessageContextValue {\n align: Accessor<MessageAlignment>;\n}\n\nconst MessageContext = createContext<MessageContextValue>();\n\nexport function useMessage() {\n const context = useContext(MessageContext);\n if (!context) {\n throw new Error(\"useMessage must be used within a <Message />\");\n }\n return context;\n}\n\n/* --- 2. Message Root --- */\nexport interface MessageProps extends JSX.HTMLAttributes<HTMLDivElement> {\n align?: MessageAlignment;\n class?: string;\n}\n\nexport const Message: ParentComponent<MessageProps> = (props) => {\n const [local, rest] = splitProps(props, [\"align\", \"class\", \"children\"]);\n const align = () => local.align ?? \"start\";\n\n const contextValue: MessageContextValue = {\n align,\n };\n\n return (\n <MessageContext.Provider value={contextValue}>\n <div\n data-align={align()}\n class={cn(\n \"group/message flex w-full gap-3\",\n align() === \"end\" ? \"flex-row-reverse items-start\" : \"flex-row items-start\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n </MessageContext.Provider>\n );\n};\n\n/* --- 3. MessageAvatar --- */\nexport interface MessageAvatarProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const MessageAvatar: ParentComponent<MessageAvatarProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\"shrink-0 select-none pt-0.5\", local.class)}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 4. MessageHeader --- */\nexport interface MessageHeaderProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const MessageHeader: ParentComponent<MessageHeaderProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n const { align } = useMessage();\n\n return (\n <div\n class={cn(\n \"flex items-center gap-2 text-xs text-muted-foreground select-none mb-1\",\n align() === \"end\" ? \"justify-end\" : \"justify-start\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 5. MessageContent --- */\nexport interface MessageContentProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const MessageContent: ParentComponent<MessageContentProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n const { align } = useMessage();\n\n return (\n <div\n class={cn(\n \"flex flex-col gap-1.5 max-w-[85%] sm:max-w-[75%]\",\n align() === \"end\" ? \"items-end text-right\" : \"items-start text-left\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 6. MessageFooter --- */\nexport interface MessageFooterProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const MessageFooter: ParentComponent<MessageFooterProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n const { align } = useMessage();\n\n return (\n <div\n class={cn(\n \"flex items-center gap-1.5 text-[11px] text-muted-foreground select-none mt-1\",\n align() === \"end\" ? \"justify-end\" : \"justify-start\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- 7. MessageActions --- */\nexport interface MessageActionsProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const MessageActions: ParentComponent<MessageActionsProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n const { align } = useMessage();\n\n return (\n <div\n class={cn(\n \"opacity-0 group-hover/message:opacity-100 focus-within:opacity-100 transition-opacity flex items-center gap-1 p-0.5 rounded-md border border-border bg-background shadow-xs\",\n align() === \"end\" ? \"flex-row-reverse\" : \"flex-row\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n",
|
|
14
|
+
"type": "registry:ui"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "navbar",
|
|
3
|
+
"title": "Navbar",
|
|
4
|
+
"description": "A responsive, accessible, and composable top navigation header suite supporting nested dropdown flyouts, floating card containers, and mobile navigation drawers.",
|
|
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/navbar.tsx",
|
|
15
|
+
"content": "import {\n createContext,\n createSignal,\n useContext,\n splitProps,\n Show,\n type JSX,\n type ParentComponent,\n type Component,\n type Accessor,\n} from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Menu, X } from \"lucide-solid\";\nimport { cn } from \"@/lib/cn\";\n\n/* --- Navbar Context --- */\ninterface NavbarContextValue {\n isOpen: Accessor<boolean>;\n setIsOpen: (open: boolean) => void;\n toggleMobileMenu: () => void;\n}\n\nconst NavbarContext = createContext<NavbarContextValue>();\n\nexport function useNavbar() {\n const context = useContext(NavbarContext);\n if (!context) {\n throw new Error(\"useNavbar must be used within a <Navbar /> component\");\n }\n return context;\n}\n\n/* --- Root Navbar --- */\nexport const navbarVariants = cva(\n \"w-full transition-all duration-200 z-40\",\n {\n variants: {\n variant: {\n default: \"border-b border-border/60 bg-background/80 backdrop-blur-md\",\n floating: \"my-3 rounded-lg border border-border/80 bg-background/90 backdrop-blur-md shadow-sm\",\n bordered: \"border-b border-border bg-background\",\n transparent: \"bg-transparent border-transparent\",\n },\n isSticky: {\n true: \"sticky top-0\",\n false: \"relative\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n isSticky: true,\n },\n }\n);\n\nexport interface NavbarProps\n extends JSX.HTMLAttributes<HTMLElement>,\n VariantProps<typeof navbarVariants> {\n maxWidth?: \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\" | \"full\";\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n}\n\nexport const Navbar: ParentComponent<NavbarProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"class\",\n \"variant\",\n \"isSticky\",\n \"maxWidth\",\n \"open\",\n \"onOpenChange\",\n \"children\",\n ]);\n\n const [internalOpen, setInternalOpen] = createSignal(false);\n const isOpen = () => (local.open !== undefined ? local.open : internalOpen());\n\n const setIsOpen = (open: boolean) => {\n if (local.open === undefined) {\n setInternalOpen(open);\n }\n local.onOpenChange?.(open);\n };\n\n const toggleMobileMenu = () => setIsOpen(!isOpen());\n\n const maxWidthClass = () => {\n switch (local.maxWidth) {\n case \"sm\":\n return \"max-w-screen-sm\";\n case \"md\":\n return \"max-w-screen-md\";\n case \"lg\":\n return \"max-w-screen-lg\";\n case \"xl\":\n return \"max-w-screen-xl\";\n case \"full\":\n return \"max-w-full\";\n case \"2xl\":\n default:\n return \"max-w-screen-2xl\";\n }\n };\n\n return (\n <NavbarContext.Provider value={{ isOpen, setIsOpen, toggleMobileMenu }}>\n <header\n class={cn(\n navbarVariants({\n variant: local.variant,\n isSticky: local.isSticky,\n }),\n local.variant === \"floating\" && cn(\"mx-auto\", maxWidthClass()),\n local.class\n )}\n {...rest}\n >\n <div class={cn(\"w-full\", local.variant !== \"floating\" && cn(\"mx-auto\", maxWidthClass()))}>\n {local.children}\n </div>\n </header>\n </NavbarContext.Provider>\n );\n};\n\n/* --- Navbar Container (Top Flex Row) --- */\nexport interface NavbarContainerProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const NavbarContainer: ParentComponent<NavbarContainerProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\n \"flex h-14 md:h-16 items-center justify-between px-4 sm:px-6 lg:px-8 gap-3 w-full\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- Navbar Brand / Logo Container --- */\nexport interface NavbarBrandProps extends JSX.AnchorHTMLAttributes<HTMLAnchorElement> {\n class?: string;\n}\n\nexport const NavbarBrand: ParentComponent<NavbarBrandProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <a\n class={cn(\n \"flex items-center gap-2 font-bold text-foreground transition-opacity hover:opacity-90 cursor-pointer shrink-0\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </a>\n );\n};\n\n/* --- Navbar Content / Section --- */\nexport const navbarContentVariants = cva(\"flex items-center gap-1 sm:gap-2 h-full\", {\n variants: {\n justify: {\n start: \"justify-start flex-1\",\n center: \"justify-center flex-1\",\n end: \"justify-end flex-1\",\n },\n },\n defaultVariants: {\n justify: \"center\",\n },\n});\n\nexport interface NavbarContentProps\n extends JSX.HTMLAttributes<HTMLDivElement>,\n VariantProps<typeof navbarContentVariants> {\n class?: string;\n hideOnMobile?: boolean;\n}\n\nexport const NavbarContent: ParentComponent<NavbarContentProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"justify\", \"hideOnMobile\", \"children\"]);\n\n return (\n <div\n class={cn(\n navbarContentVariants({ justify: local.justify }),\n local.hideOnMobile !== false && \"hidden md:flex\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\n/* --- Navbar Item --- */\nexport interface NavbarItemProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n isActive?: boolean;\n}\n\nexport const NavbarItem: ParentComponent<NavbarItemProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"isActive\", \"children\"]);\n\n return (\n <div\n class={cn(\n \"relative flex items-center h-full text-sm font-medium\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n <Show when={local.isActive}>\n <div class=\"absolute -bottom-px left-0 right-0 h-0.5 bg-primary rounded-lg z-10\" />\n </Show>\n </div>\n );\n};\n\n/* --- Navbar Link --- */\nexport const navbarLinkVariants = cva(\n \"inline-flex h-9 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors cursor-pointer select-none focus-visible:outline-hidden\",\n {\n variants: {\n variant: {\n default:\n \"text-muted-foreground hover:bg-muted hover:text-foreground\",\n active:\n \"text-foreground font-semibold bg-muted/80\",\n ghost:\n \"text-foreground hover:text-primary\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n);\n\nexport interface NavbarLinkProps\n extends JSX.AnchorHTMLAttributes<HTMLAnchorElement>,\n VariantProps<typeof navbarLinkVariants> {\n class?: string;\n isActive?: boolean;\n}\n\nexport const NavbarLink: ParentComponent<NavbarLinkProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"variant\", \"isActive\", \"children\"]);\n\n return (\n <a\n class={cn(\n navbarLinkVariants({\n variant: local.isActive ? \"active\" : local.variant,\n }),\n local.class\n )}\n {...rest}\n >\n {local.children}\n </a>\n );\n};\n\n/* --- Navbar Actions Container --- */\nexport interface NavbarActionsProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const NavbarActions: ParentComponent<NavbarActionsProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div class={cn(\"flex items-center gap-2 shrink-0\", local.class)} {...rest}>\n {local.children}\n </div>\n );\n};\n\n/* --- Navbar Mobile Toggle Button --- */\nexport interface NavbarMobileToggleProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {\n class?: string;\n}\n\nexport const NavbarMobileToggle: Component<NavbarMobileToggleProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n const { isOpen, toggleMobileMenu } = useNavbar();\n\n return (\n <button\n type=\"button\"\n onClick={toggleMobileMenu}\n aria-label=\"Toggle navigation menu\"\n aria-expanded={isOpen()}\n class={cn(\n \"inline-flex md:hidden size-8 items-center justify-center rounded-md border border-border/60 bg-muted/30 text-muted-foreground hover:bg-muted hover:text-foreground transition-colors cursor-pointer focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring shrink-0\",\n local.class\n )}\n {...rest}\n >\n <Show when={isOpen()} fallback={<Menu class=\"size-4\" />}>\n <X class=\"size-4\" />\n </Show>\n </button>\n );\n};\n\n/* --- Navbar Mobile Menu Container --- */\nexport interface NavbarMobileMenuProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const NavbarMobileMenu: ParentComponent<NavbarMobileMenuProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n const { isOpen } = useNavbar();\n\n return (\n <Show when={isOpen()}>\n <div\n class={cn(\n \"md:hidden w-full border-t border-border/60 bg-background/95 backdrop-blur-lg px-4 py-3 space-y-1 shadow-md transition-all\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n </Show>\n );\n};\n\n/* --- Navbar Mobile Item & Link --- */\nexport interface NavbarMobileLinkProps extends JSX.AnchorHTMLAttributes<HTMLAnchorElement> {\n class?: string;\n isActive?: boolean;\n}\n\nexport const NavbarMobileLink: ParentComponent<NavbarMobileLinkProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"isActive\", \"children\"]);\n const { setIsOpen } = useNavbar();\n\n return (\n <a\n onClick={() => setIsOpen(false)}\n class={cn(\n \"flex w-full items-center justify-between rounded-md px-3 py-2 text-sm font-medium transition-colors cursor-pointer\",\n local.isActive\n ? \"bg-primary text-primary-foreground font-semibold shadow-2xs\"\n : \"text-muted-foreground hover:bg-muted/80 hover:text-foreground\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </a>\n );\n};\n",
|
|
16
|
+
"type": "registry:ui"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "navigation-menu",
|
|
3
|
+
"title": "Navigation Menu",
|
|
4
|
+
"description": "A responsive and accessible top header navigation menu with mega-menu dropdowns and link previews.",
|
|
5
|
+
"type": "registry:ui",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"clsx",
|
|
8
|
+
"tailwind-merge",
|
|
9
|
+
"class-variance-authority",
|
|
10
|
+
"lucide-solid"
|
|
11
|
+
],
|
|
12
|
+
"registryDependencies": [
|
|
13
|
+
"create-click-outside"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
{
|
|
17
|
+
"path": "ui/navigation-menu.tsx",
|
|
18
|
+
"content": "import {\n createContext,\n createSignal,\n useContext,\n splitProps,\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 { createClickOutside } from \"@nikala-ui/hooks\";\nimport { cn } from \"@/lib/cn\";\nimport { ChevronDown } from \"lucide-solid\";\n\n/* --- Context Definition --- */\ninterface NavigationMenuContextValue {\n activeValue: Accessor<string | null>;\n setActiveValue: (value: string | null) => void;\n onItemMouseEnter: (value: string) => void;\n onItemMouseLeave: () => void;\n delayMs: Accessor<number>;\n}\n\nconst NavigationMenuContext = createContext<NavigationMenuContextValue>();\n\nexport function useNavigationMenu() {\n const context = useContext(NavigationMenuContext);\n if (!context) {\n throw new Error(\"useNavigationMenu must be used within a <NavigationMenu />\");\n }\n return context;\n}\n\n/* --- 1. NavigationMenu Root --- */\nexport interface NavigationMenuProps extends JSX.HTMLAttributes<HTMLElement> {\n class?: string;\n delayMs?: number;\n value?: string | null;\n onValueChange?: (value: string | null) => void;\n}\n\nexport const NavigationMenu: ParentComponent<NavigationMenuProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"delayMs\", \"value\", \"onValueChange\", \"children\"]);\n\n const [internalValue, setInternalValue] = createSignal<string | null>(local.value ?? null);\n const activeValue = () => (local.value !== undefined ? local.value : internalValue());\n\n const setActiveValue = (val: string | null) => {\n if (local.value === undefined) {\n setInternalValue(val);\n }\n local.onValueChange?.(val);\n };\n\n let rootRef!: HTMLElement;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const delay = () => local.delayMs ?? 150;\n\n const onItemMouseEnter = (val: string) => {\n clearTimeout(timer);\n const ms = delay();\n if (ms <= 0) {\n setActiveValue(val);\n } else {\n timer = setTimeout(() => {\n setActiveValue(val);\n }, ms);\n }\n };\n\n const onItemMouseLeave = () => {\n clearTimeout(timer);\n const ms = delay();\n timer = setTimeout(() => {\n setActiveValue(null);\n }, Math.max(50, ms));\n };\n\n createClickOutside({\n target: () => rootRef,\n onInteractOutside: () => {\n setActiveValue(null);\n },\n });\n\n onCleanup(() => {\n clearTimeout(timer);\n });\n\n const contextValue: NavigationMenuContextValue = {\n activeValue,\n setActiveValue,\n onItemMouseEnter,\n onItemMouseLeave,\n delayMs: delay,\n };\n\n return (\n <NavigationMenuContext.Provider value={contextValue}>\n <nav\n ref={rootRef}\n aria-label=\"Main Navigation\"\n class={cn(\"relative z-10 flex max-w-max flex-1 items-center justify-center\", local.class)}\n {...rest}\n >\n {local.children}\n </nav>\n </NavigationMenuContext.Provider>\n );\n};\n\n/* --- 2. NavigationMenuList --- */\nexport interface NavigationMenuListProps extends JSX.HTMLAttributes<HTMLUListElement> {\n class?: string;\n}\n\nexport const NavigationMenuList: ParentComponent<NavigationMenuListProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <ul\n class={cn(\n \"group flex flex-1 list-none items-center justify-center space-x-1 p-1\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </ul>\n );\n};\n\n/* --- 3. NavigationMenuItem Context & Component --- */\ninterface NavigationMenuItemContextValue {\n value: string;\n isOpen: Accessor<boolean>;\n}\n\nconst NavigationMenuItemContext = createContext<NavigationMenuItemContextValue>();\n\nfunction useNavigationMenuItem() {\n const context = useContext(NavigationMenuItemContext);\n if (!context) {\n throw new Error(\"useNavigationMenuItem must be used within a <NavigationMenuItem />\");\n }\n return context;\n}\n\nexport interface NavigationMenuItemProps extends JSX.HTMLAttributes<HTMLLIElement> {\n value?: string;\n class?: string;\n}\n\nlet itemCounter = 0;\n\nexport const NavigationMenuItem: ParentComponent<NavigationMenuItemProps> = (props) => {\n const itemId = props.value ?? `nav-item-${++itemCounter}`;\n const [local, rest] = splitProps(props, [\"class\", \"value\", \"children\"]);\n const rootContext = useNavigationMenu();\n\n const isOpen = () => rootContext.activeValue() === itemId;\n\n const itemContext: NavigationMenuItemContextValue = {\n value: itemId,\n isOpen,\n };\n\n return (\n <NavigationMenuItemContext.Provider value={itemContext}>\n <li\n class={cn(\"relative\", local.class)}\n onMouseEnter={() => rootContext.onItemMouseEnter(itemId)}\n onMouseLeave={() => rootContext.onItemMouseLeave()}\n {...rest}\n >\n {local.children}\n </li>\n </NavigationMenuItemContext.Provider>\n );\n};\n\n/* --- 4. Trigger Style Helper --- */\nexport const navigationMenuTriggerVariants = cva(\n \"group inline-flex h-9 items-center justify-center rounded-md px-3 text-sm font-medium transition-colors focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50 cursor-pointer select-none gap-1\",\n {\n variants: {\n variant: {\n default:\n \"bg-transparent text-muted-foreground hover:bg-muted hover:text-foreground data-[state=open]:bg-muted data-[state=open]:text-foreground\",\n filled:\n \"bg-muted/60 text-foreground hover:bg-muted border border-border/40 data-[state=open]:bg-muted\",\n ghost:\n \"bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground data-[state=open]:bg-accent/60\",\n outline:\n \"border border-border bg-background hover:bg-accent hover:text-accent-foreground shadow-2xs data-[state=open]:bg-accent\",\n },\n size: {\n default: \"h-9 px-3 text-sm\",\n sm: \"h-8 px-2.5 text-xs\",\n lg: \"h-10 px-4 text-base\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n);\n\nexport function navigationMenuTriggerStyle(\n variants?: VariantProps<typeof navigationMenuTriggerVariants>\n) {\n return navigationMenuTriggerVariants(variants);\n}\n\n/* --- 5. NavigationMenuTrigger --- */\nexport interface NavigationMenuTriggerProps\n extends JSX.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof navigationMenuTriggerVariants> {\n class?: string;\n hideChevron?: boolean;\n}\n\nexport const NavigationMenuTrigger: ParentComponent<NavigationMenuTriggerProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"variant\", \"size\", \"hideChevron\", \"children\"]);\n const rootContext = useNavigationMenu();\n const itemContext = useNavigationMenuItem();\n\n const handleClick = (e: MouseEvent) => {\n e.preventDefault();\n if (itemContext.isOpen()) {\n rootContext.setActiveValue(null);\n } else {\n rootContext.setActiveValue(itemContext.value);\n }\n };\n\n return (\n <button\n type=\"button\"\n aria-expanded={itemContext.isOpen()}\n data-state={itemContext.isOpen() ? \"open\" : \"closed\"}\n onClick={handleClick}\n class={cn(\n navigationMenuTriggerVariants({ variant: local.variant, size: local.size }),\n \"gap-1.5\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n <Show when={!local.hideChevron}>\n <ChevronDown\n class={cn(\n \"size-3.5 text-muted-foreground transition-transform duration-200\",\n itemContext.isOpen() && \"rotate-180 text-foreground\"\n )}\n aria-hidden=\"true\"\n />\n </Show>\n </button>\n );\n};\n\n/* --- 6. NavigationMenuContent (Mega Menu Flyout) --- */\nexport interface NavigationMenuContentProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const NavigationMenuContent: ParentComponent<NavigationMenuContentProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n const itemContext = useNavigationMenuItem();\n\n return (\n <Show when={itemContext.isOpen()}>\n <div\n data-state={itemContext.isOpen() ? \"open\" : \"closed\"}\n class={cn(\n \"absolute left-0 top-full mt-1.5 w-auto rounded-lg border border-border bg-popover p-4 text-popover-foreground shadow-lg outline-hidden z-50\",\n \"animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 duration-200\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n </Show>\n );\n};\n\n/* --- 7. NavigationMenuLink --- */\nexport interface NavigationMenuLinkProps extends JSX.AnchorHTMLAttributes<HTMLAnchorElement> {\n class?: string;\n active?: boolean;\n}\n\nexport const NavigationMenuLink: ParentComponent<NavigationMenuLinkProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"active\", \"children\"]);\n\n return (\n <a\n data-active={local.active ? \"\" : undefined}\n class={cn(\n \"block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-hidden transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground\",\n local.active && \"bg-accent/60 text-accent-foreground\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </a>\n );\n};\n\n/* --- 8. NavigationMenuViewport --- */\nexport interface NavigationMenuViewportProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const NavigationMenuViewport: Component<NavigationMenuViewportProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <div class=\"absolute left-0 top-full flex justify-center\">\n <div\n class={cn(\n \"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-lg transition-all\",\n local.class\n )}\n {...rest}\n />\n </div>\n );\n};\n\n/* --- 9. NavigationMenuIndicator --- */\nexport interface NavigationMenuIndicatorProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const NavigationMenuIndicator: Component<NavigationMenuIndicatorProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <div\n class={cn(\n \"top-full z-1 flex h-1.5 items-end justify-center overflow-hidden transition-[width,transform] duration-200\",\n local.class\n )}\n {...rest}\n >\n <div class=\"relative top-[60%] h-2 w-2 rotate-45 rounded-tl-xs bg-border shadow-md\" />\n </div>\n );\n};\n",
|
|
19
|
+
"type": "registry:ui"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -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-
|
|
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
|
+
}
|
package/registry/spinner.json
CHANGED
|
@@ -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-
|
|
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,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
|
+
}
|