@nikala-ui/core 0.10.1 → 0.11.0-nightly.b01fa01
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/banner.json +1 -1
- package/registry/bubble.json +18 -0
- package/registry/button-group.json +20 -0
- package/registry/button.json +1 -1
- package/registry/checkbox.json +4 -1
- package/registry/combobox.json +1 -1
- package/registry/command.json +3 -2
- package/registry/context-menu.json +1 -1
- package/registry/create-app-updater.json +13 -0
- package/registry/create-chat-scroll.json +13 -0
- package/registry/create-document-tabs.json +13 -0
- package/registry/create-drop-zone.json +13 -0
- package/registry/create-global-shortcut.json +13 -0
- package/registry/create-pagination.json +13 -0
- package/registry/create-tauri-window.json +13 -0
- package/registry/dialog.json +1 -1
- package/registry/dropdown-menu.json +3 -2
- package/registry/dropzone.json +21 -0
- package/registry/field.json +1 -1
- package/registry/footer.json +18 -0
- package/registry/forgot-password-01.json +28 -0
- package/registry/form-message.json +3 -2
- package/registry/hero-01.json +22 -0
- package/registry/icon-button.json +1 -1
- package/registry/index.json +432 -5
- 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/number-input.json +1 -1
- package/registry/otp-verification-01.json +26 -0
- package/registry/pagination.json +22 -0
- package/registry/popover.json +4 -1
- package/registry/rating.json +19 -0
- package/registry/register-01.json +31 -0
- package/registry/resizable.json +1 -1
- package/registry/review-card.json +23 -0
- package/registry/scroll-area.json +1 -1
- package/registry/select.json +3 -2
- package/registry/sheet.json +1 -1
- package/registry/sidebar.json +24 -0
- package/registry/spinner.json +1 -1
- package/registry/stat.json +19 -0
- package/registry/status.json +1 -1
- package/registry/switch.json +4 -1
- package/registry/table.json +17 -0
- package/registry/tabs.json +4 -1
- package/registry/theme-manager.json +4 -3
- package/registry/timeline.json +18 -0
- package/registry/titlebar-tabs.json +24 -0
- package/registry/titlebar.json +26 -0
- package/registry/toggle-group.json +21 -0
- package/registry/updater-modal.json +26 -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/banner.tsx +0 -2
- package/src/registry/components/ui/bubble.tsx +142 -0
- package/src/registry/components/ui/button-group.tsx +42 -0
- package/src/registry/components/ui/button.tsx +1 -1
- package/src/registry/components/ui/checkbox.tsx +1 -1
- package/src/registry/components/ui/combobox.tsx +83 -18
- package/src/registry/components/ui/command.tsx +101 -105
- package/src/registry/components/ui/context-menu.tsx +67 -22
- package/src/registry/components/ui/dialog.tsx +42 -32
- package/src/registry/components/ui/dropdown-menu.tsx +69 -31
- package/src/registry/components/ui/dropzone.tsx +189 -0
- package/src/registry/components/ui/field.tsx +7 -3
- package/src/registry/components/ui/footer.tsx +247 -0
- package/src/registry/components/ui/form-message.tsx +2 -2
- package/src/registry/components/ui/icon-button.tsx +1 -1
- 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 +375 -0
- package/src/registry/components/ui/navigation-menu.tsx +358 -0
- package/src/registry/components/ui/number-input.tsx +3 -3
- package/src/registry/components/ui/pagination.tsx +258 -0
- package/src/registry/components/ui/popover.tsx +1 -1
- package/src/registry/components/ui/rating.tsx +185 -0
- package/src/registry/components/ui/resizable.tsx +3 -1
- package/src/registry/components/ui/review-card.tsx +195 -0
- package/src/registry/components/ui/scroll-area.tsx +13 -3
- package/src/registry/components/ui/select.tsx +16 -19
- package/src/registry/components/ui/sheet.tsx +58 -53
- 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/status.tsx +7 -5
- package/src/registry/components/ui/switch.tsx +1 -1
- package/src/registry/components/ui/table.tsx +160 -0
- package/src/registry/components/ui/tabs.tsx +1 -1
- package/src/registry/components/ui/theme-toggle.tsx +80 -79
- package/src/registry/components/ui/timeline.tsx +350 -0
- package/src/registry/components/ui/titlebar-tabs.tsx +412 -0
- package/src/registry/components/ui/titlebar.tsx +399 -0
- package/src/registry/components/ui/toggle-group.tsx +203 -0
- package/src/registry/components/ui/updater-modal.tsx +254 -0
- package/src/registry/index.ts +3 -3
- package/src/registry/metadata.ts +184 -5
- package/src/registry/providers/theme-transitions.ts +81 -50
|
@@ -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 type ValidComponent,\n} from \"solid-js\";\nimport { Dynamic } from \"solid-js/web\";\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<T extends ValidComponent = \"a\"> extends JSX.AnchorHTMLAttributes<HTMLAnchorElement> {\n class?: string;\n as?: T;\n href?: string;\n}\n\nexport const NavbarBrand = <T extends ValidComponent = \"a\">(props: NavbarBrandProps<T>) => {\n const [local, rest] = splitProps(props as NavbarBrandProps, [\"class\", \"as\", \"children\"]);\n\n return (\n <Dynamic\n component={local.as || \"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 </Dynamic>\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.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<T extends ValidComponent = \"a\">\n extends JSX.AnchorHTMLAttributes<HTMLAnchorElement>,\n VariantProps<typeof navbarLinkVariants> {\n class?: string;\n isActive?: boolean;\n as?: T;\n href?: string;\n}\n\nexport const NavbarLink = <T extends ValidComponent = \"a\">(props: NavbarLinkProps<T>) => {\n const [local, rest] = splitProps(props as NavbarLinkProps, [\"class\", \"variant\", \"isActive\", \"as\", \"children\"]);\n\n return (\n <Dynamic\n component={local.as || \"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 </Dynamic>\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 \"@/hooks/create-click-outside\";\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\nfunction 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
|
+
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"files": [
|
|
18
18
|
{
|
|
19
19
|
"path": "ui/number-input.tsx",
|
|
20
|
-
"content": "import { createSignal, onCleanup, splitProps, type Component, type JSX } from \"solid-js\";\nimport { NumberField as KobalteNumberField } from \"@kobalte/core/number-field\";\nimport { Plus, Minus } from \"lucide-solid\";\nimport { Input } from \"
|
|
20
|
+
"content": "import { createSignal, onCleanup, splitProps, type Component, type JSX } from \"solid-js\";\nimport { NumberField as KobalteNumberField } from \"@kobalte/core/number-field\";\nimport { Plus, Minus } from \"lucide-solid\";\nimport { Input } from \"@/components/ui/input\";\nimport { Button } from \"@/components/ui/button\";\nimport { createLongPress } from \"@/hooks/create-long-press\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface NumberInputProps {\n value?: number;\n defaultValue?: number;\n onValueChange?: (value: number) => void;\n minValue?: number;\n maxValue?: number;\n step?: number;\n allowNegative?: boolean;\n disabled?: boolean;\n readOnly?: boolean;\n class?: string;\n id?: string;\n}\n\nexport const NumberInput: Component<NumberInputProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"value\",\n \"defaultValue\",\n \"onValueChange\",\n \"minValue\",\n \"maxValue\",\n \"step\",\n \"allowNegative\",\n \"disabled\",\n \"readOnly\",\n \"class\",\n \"id\",\n ]);\n\n const [internalVal, setInternalVal] = createSignal<number>(\n local.value !== undefined\n ? local.value\n : local.defaultValue !== undefined\n ? local.defaultValue\n : 0\n );\n\n let autoRepeatInterval: ReturnType<typeof setInterval> | undefined;\n\n const currentVal = () => (local.value !== undefined ? local.value : internalVal());\n\n const effectiveMin = () => {\n if (local.minValue !== undefined) return local.minValue;\n return local.allowNegative ? -Infinity : 0;\n };\n\n const effectiveMax = () => {\n if (local.maxValue !== undefined) return local.maxValue;\n return Infinity;\n };\n\n const handleValueChange = (val: number) => {\n if (isNaN(val)) return;\n const clamped = Math.max(effectiveMin(), Math.min(effectiveMax(), val));\n setInternalVal(clamped);\n local.onValueChange?.(clamped);\n };\n\n const stopAutoRepeat = () => {\n if (autoRepeatInterval) {\n clearInterval(autoRepeatInterval);\n autoRepeatInterval = undefined;\n }\n };\n\n const startAutoRepeat = (direction: 1 | -1) => {\n if (local.disabled || local.readOnly) return;\n stopAutoRepeat();\n const stepAmount = local.step || 1;\n autoRepeatInterval = setInterval(() => {\n handleValueChange(currentVal() + direction * stepAmount);\n }, 75);\n };\n\n /* Nikala UI createLongPress hook for Increment Trigger */\n const incrementLongPress = createLongPress(\n () => {\n startAutoRepeat(1);\n },\n {\n threshold: 300,\n onCancel: stopAutoRepeat,\n }\n );\n\n /* Nikala UI createLongPress hook for Decrement Trigger */\n const decrementLongPress = createLongPress(\n () => {\n startAutoRepeat(-1);\n },\n {\n threshold: 300,\n onCancel: stopAutoRepeat,\n }\n );\n\n onCleanup(() => stopAutoRepeat());\n\n return (\n <KobalteNumberField\n value={currentVal()}\n onRawValueChange={handleValueChange}\n minValue={effectiveMin()}\n maxValue={effectiveMax()}\n step={local.step || 1}\n disabled={local.disabled}\n readOnly={local.readOnly}\n id={local.id}\n class={cn(\"relative flex items-center max-w-[160px]\", local.class)}\n {...rest}\n >\n <KobalteNumberField.Input\n as={Input}\n class=\"pr-16 text-center font-mono focus-visible:ring-1\"\n />\n\n <div class=\"absolute right-1 flex items-center gap-0.5\">\n <KobalteNumberField.DecrementTrigger\n as={Button}\n variant=\"ghost\"\n size=\"icon\"\n class=\"h-7 w-7 rounded-sm p-0 text-muted-foreground hover:text-foreground select-none\"\n aria-label=\"Decrement value\"\n {...decrementLongPress.props}\n >\n <Minus class=\"h-3.5 w-3.5\" />\n </KobalteNumberField.DecrementTrigger>\n\n <KobalteNumberField.IncrementTrigger\n as={Button}\n variant=\"ghost\"\n size=\"icon\"\n class=\"h-7 w-7 rounded-sm p-0 text-muted-foreground hover:text-foreground select-none\"\n aria-label=\"Increment value\"\n {...incrementLongPress.props}\n >\n <Plus class=\"h-3.5 w-3.5\" />\n </KobalteNumberField.IncrementTrigger>\n </div>\n </KobalteNumberField>\n );\n};\n",
|
|
21
21
|
"type": "registry:ui"
|
|
22
22
|
}
|
|
23
23
|
]
|
|
@@ -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
|
+
}
|
package/registry/popover.json
CHANGED
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
"@kobalte/core",
|
|
10
10
|
"lucide-solid"
|
|
11
11
|
],
|
|
12
|
+
"registryDependencies": [
|
|
13
|
+
"create-click-outside"
|
|
14
|
+
],
|
|
12
15
|
"files": [
|
|
13
16
|
{
|
|
14
17
|
"path": "ui/popover.tsx",
|
|
15
|
-
"content": "import { splitProps, type Component, type ComponentProps } from \"solid-js\";\nimport { Popover as KobaltePopover } from \"@kobalte/core/popover\";\nimport { createClickOutside } from \"
|
|
18
|
+
"content": "import { splitProps, type Component, type ComponentProps } from \"solid-js\";\nimport { Popover as KobaltePopover } from \"@kobalte/core/popover\";\nimport { createClickOutside } from \"@/hooks/create-click-outside\";\nimport { X } from \"lucide-solid\";\nimport { cn } from \"@/lib/cn\";\n\nexport const Popover = KobaltePopover;\n\nexport const PopoverTrigger = KobaltePopover.Trigger;\n\nexport const PopoverArrow: Component<ComponentProps<typeof KobaltePopover.Arrow>> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <KobaltePopover.Arrow\n class={cn(\"fill-popover stroke-border\", local.class)}\n {...rest}\n />\n );\n};\n\nexport const PopoverContent: Component<ComponentProps<typeof KobaltePopover.Content>> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\", \"onInteractOutside\"]);\n let contentRef: HTMLElement | undefined;\n\n createClickOutside({\n target: () => contentRef,\n onInteractOutside: (e) => {\n if (typeof local.onInteractOutside === \"function\") {\n local.onInteractOutside(e as any);\n }\n },\n });\n\n return (\n <KobaltePopover.Portal>\n <KobaltePopover.Content\n ref={(el) => {\n contentRef = el;\n if (typeof (props as any).ref === \"function\") (props as any).ref(el);\n }}\n class={cn(\n \"z-50 w-72 rounded-lg border border-border bg-popover p-4 text-popover-foreground shadow-md outline-none data-expanded:animate-in data-closed:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-closed:zoom-out-95 data-expanded:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </KobaltePopover.Content>\n </KobaltePopover.Portal>\n );\n};\n\nexport const PopoverTitle: Component<ComponentProps<typeof KobaltePopover.Title>> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <KobaltePopover.Title\n class={cn(\"text-sm font-semibold text-foreground\", local.class)}\n {...rest}\n />\n );\n};\n\nexport const PopoverDescription: Component<ComponentProps<typeof KobaltePopover.Description>> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <KobaltePopover.Description\n class={cn(\"text-sm text-muted-foreground\", local.class)}\n {...rest}\n />\n );\n};\n\nexport const PopoverCloseButton: Component<ComponentProps<typeof KobaltePopover.CloseButton>> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <KobaltePopover.CloseButton\n class={cn(\n \"absolute right-2 top-2 rounded-md p-1 opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none\",\n local.class\n )}\n {...rest}\n >\n {local.children || <X class=\"h-4 w-4 text-muted-foreground\" />}\n </KobaltePopover.CloseButton>\n );\n};",
|
|
16
19
|
"type": "registry:ui"
|
|
17
20
|
}
|
|
18
21
|
]
|
|
@@ -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
|
+
}
|
package/registry/resizable.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
{
|
|
16
16
|
"path": "ui/resizable.tsx",
|
|
17
|
-
"content": "import {\n createSignal,\n createContext,\n useContext,\n splitProps,\n type Component,\n type JSX,\n type Accessor,\n} from \"solid-js\";\nimport { createElementSize } from \"
|
|
17
|
+
"content": "import {\n createSignal,\n createContext,\n useContext,\n splitProps,\n type Component,\n type JSX,\n type Accessor,\n} from \"solid-js\";\nimport { createElementSize } from \"@/hooks/create-resize-observer\";\nimport { GripVertical, GripHorizontal } from \"lucide-solid\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface ResizableContextValue {\n orientation: Accessor<\"horizontal\" | \"vertical\">;\n registerPanel: (id: string, initialSizes: number) => void;\n sizes: Accessor<Record<string, number>>;\n startDragging: (handleIndex: number, event: PointerEvent) => void;\n containerRef: () => HTMLDivElement | undefined;\n}\n\nconst ResizableContext = createContext<ResizableContextValue>();\n\nexport interface ResizableGroupProps extends JSX.HTMLAttributes<HTMLDivElement> {\n orientation?: \"horizontal\" | \"vertical\";\n class?: string;\n children?: JSX.Element;\n}\n\nexport const ResizableGroup: Component<ResizableGroupProps> = (props) => {\n const [local, rest] = splitProps(props, [\"orientation\", \"class\", \"children\"]);\n const orientation = () => local.orientation || \"horizontal\";\n let containerEl: HTMLDivElement | undefined;\n\n const [panelOrder, setPanelOrder] = createSignal<string[]>([]);\n const [sizes, setSizes] = createSignal<Record<string, number>>({});\n\n const registerPanel = (id: string, initialSize: number) => {\n setPanelOrder((prev) => (prev.includes(id) ? prev : [...prev, id]));\n setSizes((prev) => (prev[id] !== undefined ? prev : { ...prev, [id]: initialSize }));\n };\n\n const startDragging = (handleIndex: number, event: PointerEvent) => {\n if (!containerEl) return;\n event.preventDefault();\n\n const order = panelOrder();\n if (handleIndex < 0 || handleIndex >= order.length - 1) return;\n\n const leftId = order[handleIndex];\n const rightId = order[handleIndex + 1];\n\n const isHoriz = orientation() === \"horizontal\";\n const startPos = isHoriz ? event.clientX : event.clientY;\n const rect = containerEl.getBoundingClientRect();\n const totalPx = isHoriz ? rect.width : rect.height;\n\n const startLeftPct = sizes()[leftId] ?? 50;\n const startRightPct = sizes()[rightId] ?? 50;\n\n const onPointerMove = (e: PointerEvent) => {\n const currentPos = isHoriz ? e.clientX : e.clientY;\n const deltaPx = currentPos - startPos;\n const deltaPct = (deltaPx / totalPx) * 100;\n\n let newLeft = startLeftPct + deltaPct;\n let newRight = startRightPct - deltaPct;\n\n // Min size limits (10% minimum)\n if (newLeft < 10) {\n newLeft = 10;\n newRight = startLeftPct + startRightPct - 10;\n } else if (newRight < 10) {\n newRight = 10;\n newLeft = startLeftPct + startRightPct - 10;\n }\n\n setSizes((prev) => ({\n ...prev,\n [leftId]: newLeft,\n [rightId]: newRight,\n }));\n };\n\n const onPointerUp = () => {\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 return (\n <ResizableContext.Provider\n value={{\n orientation,\n registerPanel,\n sizes,\n startDragging,\n containerRef: () => containerEl,\n }}\n >\n <div\n ref={containerEl}\n class={cn(\n \"flex h-full w-full overflow-hidden rounded-lg border border-border bg-background\",\n orientation() === \"vertical\" ? \"flex-col\" : \"flex-row\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n </ResizableContext.Provider>\n );\n};\n\nexport interface ResizablePanelProps extends JSX.HTMLAttributes<HTMLDivElement> {\n id: string;\n initialSize?: number;\n class?: string;\n children?: JSX.Element;\n}\n\nexport const ResizablePanel: Component<ResizablePanelProps> = (props) => {\n const [local, rest] = splitProps(props, [\"id\", \"initialSize\", \"class\", \"children\"]);\n const ctx = useContext(ResizableContext);\n\n if (!ctx) {\n throw new Error(\"ResizablePanel must be used within a ResizableGroup\");\n }\n\n ctx.registerPanel(local.id, local.initialSize ?? 50);\n\n const currentPct = () => ctx.sizes()[local.id] ?? local.initialSize ?? 50;\n\n // Utilize Nikala UI createElementSize hook for reactive size inspection\n const containerSize = createElementSize(() => ctx.containerRef());\n\n return (\n <div\n class={cn(\"overflow-auto transition-[flex-basis] duration-75\", local.class)}\n style={{\n \"flex-basis\": `${currentPct()}%`,\n \"flex-grow\": 0,\n \"flex-shrink\": 0,\n }}\n {...rest}\n >\n {local.children}\n </div>\n );\n};\n\nexport interface ResizableHandleProps extends JSX.HTMLAttributes<HTMLDivElement> {\n handleIndex: number;\n withHandle?: boolean;\n class?: string;\n}\n\nexport const ResizableHandle: Component<ResizableHandleProps> = (props) => {\n const [local, rest] = splitProps(props, [\"handleIndex\", \"withHandle\", \"class\"]);\n const ctx = useContext(ResizableContext);\n\n if (!ctx) {\n throw new Error(\"ResizableHandle must be used within a ResizableGroup\");\n }\n\n const isHoriz = () => ctx.orientation() === \"horizontal\";\n\n return (\n <div\n role=\"separator\"\n tabIndex={0}\n class={cn(\n \"relative flex select-none items-center justify-center bg-border transition-colors hover:bg-primary/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring cursor-col-resize\",\n isHoriz() ? \"h-full w-1.5 cursor-col-resize\" : \"h-1.5 w-full cursor-row-resize\",\n local.class\n )}\n onPointerDown={(e) => ctx.startDragging(local.handleIndex, e)}\n {...rest}\n >\n {local.withHandle && (\n <div class=\"z-10 flex h-4 w-3 items-center justify-center rounded-xs border border-border bg-muted shadow-2xs\">\n {isHoriz() ? (\n <GripVertical class=\"h-2.5 w-2.5 text-muted-foreground\" />\n ) : (\n <GripHorizontal class=\"h-2.5 w-2.5 text-muted-foreground\" />\n )}\n </div>\n )}\n </div>\n );\n};\n",
|
|
18
18
|
"type": "registry:ui"
|
|
19
19
|
}
|
|
20
20
|
]
|
|
@@ -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
|
|
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 } from \"@/hooks/create-scroll-position\";\nimport { createElementSize } from \"@/hooks/create-resize-observer\";\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 // Track available lengths (considering 4px track padding)\n const trackHeight = verticalTrackRef ? verticalTrackRef.clientHeight - 4 : clientHeight - 4;\n const trackWidth = horizontalTrackRef ? horizontalTrackRef.clientWidth - 4 : clientWidth - 4;\n\n // Vertical thumb metrics\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 // Horizontal thumb metrics\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 // Enable mouse drag on Vertical Thumb\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 // Enable mouse drag on Horizontal Thumb\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 // Convert vertical mouse wheel into horizontal scroll proportionally\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 {/* Scroll Viewport Container */}\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 {/* Vertical Scrollbar Track & Thumb */}\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 {/* Horizontal Scrollbar Track & Thumb */}\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
|
]
|
package/registry/select.json
CHANGED
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
"@kobalte/core"
|
|
10
10
|
],
|
|
11
11
|
"registryDependencies": [
|
|
12
|
-
"scroll-area"
|
|
12
|
+
"scroll-area",
|
|
13
|
+
"create-click-outside"
|
|
13
14
|
],
|
|
14
15
|
"files": [
|
|
15
16
|
{
|
|
16
17
|
"path": "ui/select.tsx",
|
|
17
|
-
"content": "import { splitProps, type
|
|
18
|
+
"content": "import { splitProps, type JSX, type ValidComponent } from \"solid-js\";\nimport * as SelectPrimitive from \"@kobalte/core/select\";\nimport type { PolymorphicProps } from \"@kobalte/core/polymorphic\";\nimport { createClickOutside } from \"@/hooks/create-click-outside\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { cn } from \"@/lib/cn\";\n\nexport type SelectRootProps<Option = any, OptGroup = any, T extends ValidComponent = \"div\"> =\n SelectPrimitive.SelectRootProps<Option, OptGroup, T> & {\n class?: string;\n };\n\n/**\n * Root Select component built on top of Kobalte headless primitives.\n */\nexport const Select = <Option = any, OptGroup = any, T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, SelectRootProps<Option, OptGroup, T>>\n) => {\n const [local, rest] = splitProps(props as SelectRootProps, [\"class\"]);\n\n return <SelectPrimitive.Root class={cn(\"relative w-full\", local.class)} {...(rest as any)} />;\n};\n\nexport type SelectTriggerProps<T extends ValidComponent = \"button\"> =\n SelectPrimitive.SelectTriggerProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Trigger button opening the Select options list.\n */\nexport const SelectTrigger = <T extends ValidComponent = \"button\">(\n props: PolymorphicProps<T, SelectTriggerProps<T>>\n) => {\n const [local, rest] = splitProps(props as SelectTriggerProps, [\"class\", \"children\"]);\n\n return (\n <SelectPrimitive.Trigger\n class={cn(\n \"flex h-9 w-full items-center justify-between rounded-md border border-input bg-muted px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer text-foreground\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n <SelectPrimitive.Icon\n as=\"svg\"\n class=\"h-4 w-4 opacity-50 transition-transform\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M19 9l-7 7-7-7\" />\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n );\n};\n\nexport type SelectValueProps<Option, T extends ValidComponent = \"span\"> =\n SelectPrimitive.SelectValueProps<Option, T> & {\n class?: string;\n };\n\n/**\n * Renders the currently selected option text or placeholder.\n */\nexport const SelectValue = <Option = any, T extends ValidComponent = \"span\">(\n props: PolymorphicProps<T, SelectValueProps<Option, T>>\n) => {\n const [local, rest] = splitProps(props as SelectValueProps<Option>, [\"class\"]);\n\n return (\n <SelectPrimitive.Value\n class={cn(\"block truncate\", local.class)}\n {...(rest as any)}\n />\n );\n};\n\nexport type SelectContentProps<T extends ValidComponent = \"div\"> =\n SelectPrimitive.SelectContentProps<T> & {\n class?: string;\n };\n\n/**\n * Portaled overlay container rendering the listbox options.\n */\nexport const SelectContent = <T extends ValidComponent = \"div\">(\n props: PolymorphicProps<T, SelectContentProps<T>>\n) => {\n const [local, rest] = splitProps(props as SelectContentProps, [\"class\"]);\n let contentRef: HTMLElement | undefined;\n\n createClickOutside({\n target: () => contentRef,\n onInteractOutside: (e) => {\n if (typeof (props as any).onInteractOutside === \"function\") {\n (props as any).onInteractOutside(e);\n }\n },\n });\n\n return (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n ref={(el) => {\n contentRef = el;\n if (typeof (props as any).ref === \"function\") (props as any).ref(el);\n }}\n class={cn(\n \"relative z-50 min-w-8rem overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md animate-in fade-in-80 max-h-60\",\n local.class\n )}\n {...rest}\n >\n <ScrollArea class=\"max-h-60 w-full\">\n <SelectPrimitive.Listbox class=\"p-1 outline-none\" />\n </ScrollArea>\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n );\n};\n\nexport type SelectItemProps<T extends ValidComponent = \"li\"> =\n SelectPrimitive.SelectItemProps<T> & {\n class?: string;\n children?: JSX.Element;\n };\n\n/**\n * Individual option item choice inside SelectContent.\n */\nexport const SelectItem = <T extends ValidComponent = \"li\">(\n props: PolymorphicProps<T, SelectItemProps<T>>\n) => {\n const [local, rest] = splitProps(props as SelectItemProps, [\"class\", \"children\"]);\n\n return (\n <SelectPrimitive.Item\n class={cn(\n \"relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-disabled:pointer-events-none data-highlighted:bg-accent data-highlighted:text-accent-foreground\",\n local.class\n )}\n {...rest}\n >\n <SelectPrimitive.ItemIndicator class=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <svg class=\"h-4 w-4 fill-none stroke-current stroke-2\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M5 13l4 4L19 7\" />\n </svg>\n </SelectPrimitive.ItemIndicator>\n <SelectPrimitive.ItemLabel>{local.children}</SelectPrimitive.ItemLabel>\n </SelectPrimitive.Item>\n );\n};",
|
|
18
19
|
"type": "registry:ui"
|
|
19
20
|
}
|
|
20
21
|
]
|