@nikala-ui/core 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +42 -0
  2. package/package.json +25 -0
  3. package/registry/accordion.json +18 -0
  4. package/registry/alert.json +18 -0
  5. package/registry/avatar.json +17 -0
  6. package/registry/badge.json +18 -0
  7. package/registry/banner.json +19 -0
  8. package/registry/breadcrumb.json +17 -0
  9. package/registry/button.json +18 -0
  10. package/registry/card.json +17 -0
  11. package/registry/checkbox.json +17 -0
  12. package/registry/command.json +25 -0
  13. package/registry/dialog.json +18 -0
  14. package/registry/dropdown-menu.json +18 -0
  15. package/registry/index.json +297 -0
  16. package/registry/input-group.json +21 -0
  17. package/registry/input.json +17 -0
  18. package/registry/kbd.json +18 -0
  19. package/registry/label.json +18 -0
  20. package/registry/list.json +20 -0
  21. package/registry/radio-group.json +18 -0
  22. package/registry/select.json +18 -0
  23. package/registry/separator.json +17 -0
  24. package/registry/sheet.json +19 -0
  25. package/registry/skeleton.json +17 -0
  26. package/registry/switch.json +17 -0
  27. package/registry/tabs.json +17 -0
  28. package/registry/textarea.json +17 -0
  29. package/registry/theme-manager.json +38 -0
  30. package/src/index.css +11 -0
  31. package/src/lib/cn.ts +9 -0
  32. package/src/registry/components/ui/accordion.tsx +128 -0
  33. package/src/registry/components/ui/alert.tsx +155 -0
  34. package/src/registry/components/ui/avatar.tsx +114 -0
  35. package/src/registry/components/ui/badge.tsx +50 -0
  36. package/src/registry/components/ui/banner.tsx +189 -0
  37. package/src/registry/components/ui/breadcrumb.tsx +136 -0
  38. package/src/registry/components/ui/button.tsx +63 -0
  39. package/src/registry/components/ui/card.tsx +113 -0
  40. package/src/registry/components/ui/checkbox.tsx +85 -0
  41. package/src/registry/components/ui/command.tsx +286 -0
  42. package/src/registry/components/ui/dialog.tsx +195 -0
  43. package/src/registry/components/ui/dropdown-menu.tsx +250 -0
  44. package/src/registry/components/ui/input-group.tsx +80 -0
  45. package/src/registry/components/ui/input.tsx +28 -0
  46. package/src/registry/components/ui/kbd.tsx +73 -0
  47. package/src/registry/components/ui/label.tsx +30 -0
  48. package/src/registry/components/ui/list.tsx +222 -0
  49. package/src/registry/components/ui/radio-group.tsx +95 -0
  50. package/src/registry/components/ui/select.tsx +138 -0
  51. package/src/registry/components/ui/separator.tsx +37 -0
  52. package/src/registry/components/ui/sheet.tsx +216 -0
  53. package/src/registry/components/ui/skeleton.tsx +24 -0
  54. package/src/registry/components/ui/switch.tsx +79 -0
  55. package/src/registry/components/ui/tabs.tsx +212 -0
  56. package/src/registry/components/ui/textarea.tsx +82 -0
  57. package/src/registry/components/ui/theme-toggle.tsx +195 -0
  58. package/src/registry/index.ts +50 -0
  59. package/src/registry/metadata.ts +152 -0
  60. package/src/registry/providers/theme-provider.tsx +208 -0
  61. package/src/registry/providers/theme-script.tsx +58 -0
  62. package/src/registry/providers/theme-transitions.ts +108 -0
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "theme-manager",
3
+ "title": "Theme Manager",
4
+ "description": "Zero-dependency ThemeProvider and ThemeToggle component for switching light, dark, and system themes.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "@kobalte/core",
10
+ "lucide-solid"
11
+ ],
12
+ "registryDependencies": [
13
+ "button",
14
+ "dropdown-menu"
15
+ ],
16
+ "files": [
17
+ {
18
+ "path": "providers/theme-provider.tsx",
19
+ "content": "import {\n createContext,\n createEffect,\n createSignal,\n onCleanup,\n onMount,\n useContext,\n type ParentComponent,\n type Accessor,\n} from \"solid-js\";\n\nexport type Theme = \"light\" | \"dark\" | \"system\";\nexport type AccentColor = \"wine\" | \"violet\" | \"sky\" | \"emerald\" | \"rose\" | \"amber\" | \"zinc\";\nexport type Radius = \"0\" | \"0.3\" | \"0.5\" | \"0.75\" | \"1.0\";\n\nexport interface ThemeProviderProps {\n /** Initial default theme mode if no saved preference is found in localStorage */\n defaultTheme?: Theme;\n /** Initial default accent color override if needed */\n defaultAccent?: AccentColor;\n /** Initial default border radius override if needed */\n defaultRadius?: Radius;\n /** Key used to store theme preferences in localStorage */\n storageKey?: string;\n}\n\nconst ACCENT_COLORS: Record<\n AccentColor,\n { light: string; dark: string; lightFg: string; darkFg: string }\n> = {\n wine: { light: \"#722f37\", dark: \"#9e3b47\", lightFg: \"#ffffff\", darkFg: \"#ffffff\" },\n violet: { light: \"#7c3aed\", dark: \"#8b5cf6\", lightFg: \"#ffffff\", darkFg: \"#ffffff\" },\n sky: { light: \"#0284c7\", dark: \"#38bdf8\", lightFg: \"#ffffff\", darkFg: \"#0f172a\" },\n emerald: { light: \"#059669\", dark: \"#34d399\", lightFg: \"#ffffff\", darkFg: \"#052e16\" },\n rose: { light: \"#e11d48\", dark: \"#fb7185\", lightFg: \"#ffffff\", darkFg: \"#ffffff\" },\n amber: { light: \"#d97706\", dark: \"#fbbf24\", lightFg: \"#ffffff\", darkFg: \"#111827\" },\n zinc: { light: \"#18181b\", dark: \"#fafafa\", lightFg: \"#fafafa\", darkFg: \"#18181b\" },\n};\n\ninterface ThemeProviderContextValue {\n theme: Accessor<Theme>;\n setTheme: (theme: Theme) => void;\n accent: Accessor<AccentColor | undefined>;\n setAccent: (accent: AccentColor) => void;\n radius: Accessor<Radius | undefined>;\n setRadius: (radius: Radius) => void;\n}\n\nconst ThemeProviderContext = createContext<ThemeProviderContextValue>();\n\n/**\n * Context provider managing application theme state (light/dark/system), accent colors, and border radius.\n */\nexport const ThemeProvider: ParentComponent<ThemeProviderProps> = (props) => {\n const storageKey = props.storageKey || \"nikala-theme\";\n const defaultTheme = props.defaultTheme || \"system\";\n\n // Safely read saved values from localStorage without forcing unnecessary fallbacks\n const getInitialTheme = (): Theme => {\n if (typeof window === \"undefined\") return defaultTheme;\n try {\n const saved = localStorage.getItem(`${storageKey}-mode`);\n if (saved === \"light\" || saved === \"dark\" || saved === \"system\") return saved;\n } catch { }\n return defaultTheme;\n };\n\n const getInitialAccent = (): AccentColor | undefined => {\n if (typeof window === \"undefined\") return props.defaultAccent;\n try {\n const saved = localStorage.getItem(`${storageKey}-accent`);\n if (saved && ACCENT_COLORS[saved as AccentColor]) return saved as AccentColor;\n } catch { }\n return props.defaultAccent;\n };\n\n const getInitialRadius = (): Radius | undefined => {\n if (typeof window === \"undefined\") return props.defaultRadius;\n try {\n const saved = localStorage.getItem(`${storageKey}-radius`);\n if (saved) return saved as Radius;\n } catch { }\n return props.defaultRadius;\n };\n\n const [theme, setThemeSignal] = createSignal<Theme>(getInitialTheme());\n const [accent, setAccentSignal] = createSignal<AccentColor | undefined>(getInitialAccent());\n const [radius, setRadiusSignal] = createSignal<Radius | undefined>(getInitialRadius());\n\n // Applies classes and CSS custom properties when custom overrides are explicitly set\n const applyTheme = (\n targetTheme: Theme,\n currentAccent: AccentColor | undefined,\n currentRadius: Radius | undefined\n ) => {\n if (typeof window === \"undefined\") return;\n\n const root = document.documentElement;\n root.classList.remove(\"light\", \"dark\");\n\n let resolvedDark = false;\n if (targetTheme === \"system\") {\n resolvedDark = window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n root.classList.add(resolvedDark ? \"dark\" : \"light\");\n } else {\n resolvedDark = targetTheme === \"dark\";\n root.classList.add(targetTheme);\n }\n\n // Override --primary CSS variables ONLY if explicitly chosen\n if (currentAccent && ACCENT_COLORS[currentAccent]) {\n const accentData = ACCENT_COLORS[currentAccent];\n const primaryHex = resolvedDark ? accentData.dark : accentData.light;\n const primaryFgHex = resolvedDark ? accentData.darkFg : accentData.lightFg;\n\n root.style.setProperty(\"--primary\", primaryHex);\n root.style.setProperty(\"--primary-foreground\", primaryFgHex);\n }\n\n // Override --radius CSS variable ONLY if explicitly chosen\n if (currentRadius) {\n root.style.setProperty(\"--radius\", `${currentRadius}rem`);\n }\n };\n\n // Reactively apply theme updates and store preferences in localStorage\n createEffect(() => {\n const t = theme();\n const a = accent();\n const r = radius();\n\n applyTheme(t, a, r);\n\n if (typeof window !== \"undefined\") {\n try {\n localStorage.setItem(`${storageKey}-mode`, t);\n if (a) localStorage.setItem(`${storageKey}-accent`, a);\n if (r) localStorage.setItem(`${storageKey}-radius`, r);\n } catch { }\n }\n });\n\n onMount(() => {\n if (typeof window === \"undefined\") return;\n\n const mediaQuery = window.matchMedia(\"(prefers-color-scheme: dark)\");\n\n const handleSystemChange = (e: MediaQueryListEvent) => {\n if (theme() === \"system\") {\n const root = document.documentElement;\n root.classList.remove(\"light\", \"dark\");\n root.classList.add(e.matches ? \"dark\" : \"light\");\n\n const currentAccent = accent();\n if (currentAccent && ACCENT_COLORS[currentAccent]) {\n const accentData = ACCENT_COLORS[currentAccent];\n const primaryHex = e.matches ? accentData.dark : accentData.light;\n const primaryFgHex = e.matches ? accentData.darkFg : accentData.lightFg;\n\n root.style.setProperty(\"--primary\", primaryHex);\n root.style.setProperty(\"--primary-foreground\", primaryFgHex);\n }\n }\n };\n\n if (mediaQuery.addEventListener) {\n mediaQuery.addEventListener(\"change\", handleSystemChange);\n } else if (\"addListener\" in mediaQuery) {\n (mediaQuery as any).addListener(handleSystemChange);\n }\n\n onCleanup(() => {\n if (mediaQuery.removeEventListener) {\n mediaQuery.removeEventListener(\"change\", handleSystemChange);\n } else if (\"removeListener\" in mediaQuery) {\n (mediaQuery as any).removeListener(handleSystemChange);\n }\n });\n });\n\n const value: ThemeProviderContextValue = {\n theme,\n setTheme: (newTheme: Theme) => setThemeSignal(newTheme),\n accent,\n setAccent: (newAccent: AccentColor) => setAccentSignal(newAccent),\n radius,\n setRadius: (newRadius: Radius) => setRadiusSignal(newRadius),\n };\n\n return (\n <ThemeProviderContext.Provider value={value}>\n {props.children}\n </ThemeProviderContext.Provider>\n );\n};\n\n/**\n * Accesses Nikala UI theme state, accent colors, border radius, and update functions.\n */\nexport function useTheme(): ThemeProviderContextValue {\n const context = useContext(ThemeProviderContext);\n if (!context) {\n throw new Error(\"useTheme must be used within a ThemeProvider\");\n }\n return context;\n}\n\nexport { ThemeScript, type ThemeScriptProps } from \"./theme-script\";",
20
+ "type": "registry:ui"
21
+ },
22
+ {
23
+ "path": "providers/theme-transitions.ts",
24
+ "content": "export type ThemeEffect = \"none\" | \"circular\" | \"fade\";\n\n/**\n * Injects required CSS view-transition pseudo-element styles to prevent browser mix-blend artifacts.\n */\nfunction ensureTransitionStyles() {\n if (typeof document === \"undefined\") return;\n const styleId = \"nikala-view-transition-styles\";\n if (!document.getElementById(styleId)) {\n const style = document.createElement(\"style\");\n style.id = styleId;\n style.textContent = `\n ::view-transition-old(root),\n ::view-transition-new(root) {\n animation: none;\n mix-blend-mode: normal;\n }\n ::view-transition-old(root) {\n z-index: 1;\n }\n ::view-transition-new(root) {\n z-index: 9999;\n }\n `;\n document.head.appendChild(style);\n }\n}\n\n/**\n * Executes a theme change with the specified transition effect using the Web View Transitions API.\n *\n * @param effect - Desired transition animation (\"none\", \"circular\", \"fade\")\n * @param event - Mouse or Pointer event to calculate transition center coordinates\n * @param updateThemeCallback - Callback function performing the actual theme state change\n */\nexport function runThemeTransition(\n effect: ThemeEffect = \"none\",\n event: MouseEvent | undefined,\n updateThemeCallback: () => void\n) {\n // Safe fallback if View Transitions API is unsupported or user prefers reduced motion\n if (\n effect === \"none\" ||\n typeof document === \"undefined\" ||\n !(document as any).startViewTransition ||\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n ) {\n updateThemeCallback();\n return;\n }\n\n ensureTransitionStyles();\n\n // Circular expanding ripple transition originating from click coordinates\n if (effect === \"circular\" && event) {\n const x = event.clientX;\n const y = event.clientY;\n\n const endRadius = Math.hypot(\n Math.max(x, window.innerWidth - x),\n Math.max(y, window.innerHeight - y)\n );\n\n const transition = (document as any).startViewTransition(() => {\n updateThemeCallback();\n });\n\n transition.ready.then(() => {\n document.documentElement.animate(\n {\n clipPath: [\n `circle(0px at ${x}px ${y}px)`,\n `circle(${endRadius}px at ${x}px ${y}px)`,\n ],\n },\n {\n duration: 500,\n easing: \"ease-in-out\",\n pseudoElement: \"::view-transition-new(root)\",\n }\n );\n });\n return;\n }\n\n // Smooth opacity fade view transition\n if (effect === \"fade\") {\n const transition = (document as any).startViewTransition(() => {\n updateThemeCallback();\n });\n\n transition.ready.then(() => {\n document.documentElement.animate(\n {\n opacity: [0, 1],\n },\n {\n duration: 350,\n easing: \"ease-in-out\",\n pseudoElement: \"::view-transition-new(root)\",\n }\n );\n });\n return;\n }\n\n updateThemeCallback();\n}",
25
+ "type": "registry:ui"
26
+ },
27
+ {
28
+ "path": "providers/theme-script.tsx",
29
+ "content": "import { type Component } from \"solid-js\";\n\nexport interface ThemeScriptProps {\n /** Storage key namespace used in localStorage (default: \"nikala-theme\") */\n storageKey?: string;\n /** Initial default theme mode if no saved preference exists (default: \"system\") */\n defaultTheme?: \"light\" | \"dark\" | \"system\";\n /** Initial default primary accent color if no saved preference exists */\n defaultAccent?: string;\n /** Initial default border radius if no saved preference exists */\n defaultRadius?: string;\n}\n\n/**\n * Pre-hydration inline script executed synchronously before DOM paint to prevent theme flickering (anti-FOUC).\n */\nexport const ThemeScript: Component<ThemeScriptProps> = (props) => {\n const key = props.storageKey || \"nikala-theme\";\n const defTheme = props.defaultTheme || \"system\";\n const defAccent = props.defaultAccent || \"\";\n const defRadius = props.defaultRadius || \"\";\n\n const scriptText = `(function(){try{\n var key = '${key}';\n var mode = localStorage.getItem(key + '-mode') || '${defTheme}';\n var accent = localStorage.getItem(key + '-accent') || '${defAccent}';\n var radius = localStorage.getItem(key + '-radius') || '${defRadius}';\n\n var isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;\n var resolvedDark = mode === 'dark' || (mode === 'system' && isDark);\n\n var root = document.documentElement;\n root.classList.remove('light', 'dark');\n root.classList.add(resolvedDark ? 'dark' : 'light');\n root.style.colorScheme = resolvedDark ? 'dark' : 'light';\n\n var colorMap = {\n wine: resolvedDark ? '#9e3b47' : '#722f37',\n violet: resolvedDark ? '#8b5cf6' : '#7c3aed',\n sky: resolvedDark ? '#38bdf8' : '#0284c7',\n emerald: resolvedDark ? '#34d399' : '#059669',\n rose: resolvedDark ? '#fb7185' : '#e11d48',\n amber: resolvedDark ? '#fbbf24' : '#d97706',\n zinc: resolvedDark ? '#fafafa' : '#18181b'\n };\n\n if (accent && colorMap[accent]) {\n root.style.setProperty('--primary', colorMap[accent]);\n }\n\n if (radius) {\n var radVal = radius.endsWith('rem') ? radius : radius + 'rem';\n root.style.setProperty('--radius', radVal);\n }\n}catch(e){}})();`;\n\n return <script innerHTML={scriptText} />;\n};",
30
+ "type": "registry:ui"
31
+ },
32
+ {
33
+ "path": "ui/theme-toggle.tsx",
34
+ "content": "import { splitProps, type Component, For, Show } from \"solid-js\";\nimport { Sun, Moon, Monitor } from \"lucide-solid\";\nimport {\n useTheme,\n type AccentColor,\n type Radius,\n type Theme,\n} from \"../../providers/theme-provider\";\nimport {\n runThemeTransition,\n type ThemeEffect,\n} from \"../../providers/theme-transitions\";\nimport { Button } from \"./button\";\nimport {\n DropdownMenu,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n} from \"./dropdown-menu\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface ThemeToggleProps {\n /** Display mode: \"mini\" for compact dropdown, \"max\" for full customizer panel (default: \"mini\") */\n mode?: \"mini\" | \"max\";\n /** Transition animation effect when changing themes (\"none\", \"circular\", \"fade\") */\n effect?: ThemeEffect;\n class?: string;\n}\n\nconst ACCENT_OPTIONS: { name: AccentColor; label: string; color: string }[] = [\n { name: \"wine\", label: \"Wine\", color: \"bg-[#722f37]\" },\n { name: \"violet\", label: \"Violet\", color: \"bg-[#7c3aed]\" },\n { name: \"sky\", label: \"Sky\", color: \"bg-[#0284c7]\" },\n { name: \"emerald\", label: \"Emerald\", color: \"bg-[#059669]\" },\n { name: \"rose\", label: \"Rose\", color: \"bg-[#e11d48]\" },\n { name: \"amber\", label: \"Amber\", color: \"bg-[#d97706]\" },\n { name: \"zinc\", label: \"Zinc\", color: \"bg-[#18181b]\" },\n];\n\nconst RADIUS_OPTIONS: { value: Radius; label: string }[] = [\n { value: \"0\", label: \"0\" },\n { value: \"0.3\", label: \"0.3\" },\n { value: \"0.5\", label: \"0.5\" },\n { value: \"0.75\", label: \"0.75\" },\n { value: \"1.0\", label: \"1.0\" },\n];\n\n/**\n * Interactive UI theme switcher supporting mini/max modes and View Transition animations.\n */\nexport const ThemeToggle: Component<ThemeToggleProps> = (props) => {\n const [local] = splitProps(props, [\"mode\", \"effect\", \"class\"]);\n const { theme, setTheme, accent, setAccent, radius, setRadius } = useTheme();\n\n const mode = () => local.mode || \"mini\";\n const effect = () => local.effect || \"none\";\n\n /* Reactive accessor determining whether dark mode is active */\n const isDarkMode = () => {\n const currentTheme = theme();\n if (currentTheme === \"dark\") return true;\n if (currentTheme === \"light\") return false;\n return (\n typeof window !== \"undefined\" &&\n window.matchMedia(\"(prefers-color-scheme: dark)\").matches\n );\n };\n\n const changeThemeWithEffect = (newTheme: Theme, e: MouseEvent) => {\n runThemeTransition(effect(), e, () => {\n setTheme(newTheme);\n });\n };\n\n return (\n <DropdownMenu placement=\"bottom-end\">\n <DropdownMenuTrigger\n as={Button}\n variant=\"outline\"\n size=\"icon\"\n class={cn(\"relative h-9 w-9 cursor-pointer\", local.class)}\n >\n {/* Reactive Sun / Moon Icon Toggle */}\n <Show\n when={isDarkMode()}\n fallback={<Sun class=\"h-4 w-4 text-foreground transition-transform\" />}\n >\n <Moon class=\"h-4 w-4 text-foreground transition-transform\" />\n </Show>\n\n <span class=\"sr-only\">Toggle theme</span>\n </DropdownMenuTrigger>\n\n <Show\n when={mode() === \"max\"}\n fallback={\n /* Mini Mode: Compact Dropdown */\n <DropdownMenuContent>\n <DropdownMenuItem onClick={(e: MouseEvent) => changeThemeWithEffect(\"light\", e)}>\n <Sun class=\"mr-2 h-4 w-4 text-muted-foreground\" />\n Light\n </DropdownMenuItem>\n\n <DropdownMenuItem onClick={(e: MouseEvent) => changeThemeWithEffect(\"dark\", e)}>\n <Moon class=\"mr-2 h-4 w-4 text-muted-foreground\" />\n Dark\n </DropdownMenuItem>\n\n <DropdownMenuItem onClick={(e: MouseEvent) => changeThemeWithEffect(\"system\", e)}>\n <Monitor class=\"mr-2 h-4 w-4 text-muted-foreground\" />\n System\n </DropdownMenuItem>\n </DropdownMenuContent>\n }\n >\n {/* Max Mode: Full Theme Customizer Panel */}\n <DropdownMenuContent class=\"w-64 p-3\">\n <DropdownMenuLabel class=\"px-0 pt-0 text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n Theme Mode\n </DropdownMenuLabel>\n <div class=\"grid grid-cols-3 gap-1 my-1.5\">\n <Button\n variant={theme() === \"light\" ? \"default\" : \"outline\"}\n size=\"sm\"\n onClick={(e: MouseEvent) => changeThemeWithEffect(\"light\", e)}\n class=\"h-8 text-xs cursor-pointer\"\n >\n Light\n </Button>\n <Button\n variant={theme() === \"dark\" ? \"default\" : \"outline\"}\n size=\"sm\"\n onClick={(e: MouseEvent) => changeThemeWithEffect(\"dark\", e)}\n class=\"h-8 text-xs cursor-pointer\"\n >\n Dark\n </Button>\n <Button\n variant={theme() === \"system\" ? \"default\" : \"outline\"}\n size=\"sm\"\n onClick={(e: MouseEvent) => changeThemeWithEffect(\"system\", e)}\n class=\"h-8 text-xs cursor-pointer\"\n >\n System\n </Button>\n </div>\n\n <DropdownMenuSeparator class=\"my-2\" />\n\n <DropdownMenuLabel class=\"px-0 text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n Brand Accent Color\n </DropdownMenuLabel>\n <div class=\"flex flex-wrap gap-1.5 my-1.5\">\n <For each={ACCENT_OPTIONS}>\n {(opt) => (\n <button\n type=\"button\"\n title={opt.label}\n onClick={() => setAccent(opt.name)}\n class={cn(\n \"h-6 w-6 rounded-md transition-all cursor-pointer border border-border flex items-center justify-center\",\n opt.color,\n accent() === opt.name ? \"ring-2 ring-primary ring-offset-2 ring-offset-background scale-110\" : \"hover:scale-105\"\n )}\n />\n )}\n </For>\n </div>\n\n <DropdownMenuSeparator class=\"my-2\" />\n\n <DropdownMenuLabel class=\"px-0 text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n Border Radius\n </DropdownMenuLabel>\n <div class=\"grid grid-cols-5 gap-1 my-1.5\">\n <For each={RADIUS_OPTIONS}>\n {(r) => (\n <Button\n variant={radius() === r.value ? \"default\" : \"outline\"}\n size=\"sm\"\n onClick={() => setRadius(r.value)}\n class=\"h-7 text-xs px-1 cursor-pointer\"\n >\n {r.label}\n </Button>\n )}\n </For>\n </div>\n </DropdownMenuContent>\n </Show>\n </DropdownMenu>\n );\n};",
35
+ "type": "registry:ui"
36
+ }
37
+ ]
38
+ }
package/src/index.css ADDED
@@ -0,0 +1,11 @@
1
+ // src/index.css
2
+ @import "tailwindcss";
3
+
4
+ @theme {
5
+ --color-primary: #722f37;
6
+ --color-primary-hover: #4a1c21;
7
+ --color-surface: #1a1a1a;
8
+ --color-surface-light: #2a2a2a;
9
+ --color-text: #f5f5dc;
10
+ --color-text-muted: #a0a0a0;
11
+ }
package/src/lib/cn.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ /**
5
+ * Combines multiple class names and resolves Tailwind CSS conflicts.
6
+ */
7
+ export function cn(...inputs: ClassValue[]) {
8
+ return twMerge(clsx(inputs));
9
+ }
@@ -0,0 +1,128 @@
1
+ import { splitProps, type JSX, type ValidComponent } from "solid-js";
2
+ import * as AccordionPrimitive from "@kobalte/core/accordion";
3
+ import type { PolymorphicProps } from "@kobalte/core/polymorphic";
4
+ import { cn } from "@/lib/cn";
5
+
6
+ export type AccordionRootProps<T extends ValidComponent = "div"> = Omit<
7
+ AccordionPrimitive.AccordionRootProps<T>,
8
+ "value" | "defaultValue"
9
+ > & {
10
+ /** Accordion mode: "single" allows one open item, "multiple" allows many */
11
+ type?: "single" | "multiple";
12
+ /** Controlled value (single string or array of strings) */
13
+ value?: string | string[];
14
+ /** Default initial value (single string or array of strings) */
15
+ defaultValue?: string | string[];
16
+ class?: string;
17
+ };
18
+
19
+ /**
20
+ * Root Accordion component built on top of Kobalte headless primitives.
21
+ */
22
+ export const Accordion = <T extends ValidComponent = "div">(
23
+ props: PolymorphicProps<T, AccordionRootProps<T>>
24
+ ) => {
25
+ const [local, rest] = splitProps(props as AccordionRootProps, [
26
+ "class",
27
+ "type",
28
+ "multiple",
29
+ ]);
30
+
31
+ // Support both `type="multiple"` and `multiple={true}`
32
+ const isMultiple = () => local.multiple ?? local.type === "multiple";
33
+
34
+ return (
35
+ <AccordionPrimitive.Root
36
+ multiple={isMultiple()}
37
+ class={cn("w-full divide-y divide-border", local.class)}
38
+ {...(rest as any)}
39
+ />
40
+ );
41
+ };
42
+
43
+ export type AccordionItemProps<T extends ValidComponent = "div"> =
44
+ AccordionPrimitive.AccordionItemProps<T> & {
45
+ class?: string;
46
+ value: string;
47
+ };
48
+
49
+ /**
50
+ * Individual Accordion section item wrapper.
51
+ */
52
+ export const AccordionItem = <T extends ValidComponent = "div">(
53
+ props: PolymorphicProps<T, AccordionItemProps<T>>
54
+ ) => {
55
+ const [local, rest] = splitProps(props as AccordionItemProps, ["class"]);
56
+
57
+ return (
58
+ <AccordionPrimitive.Item
59
+ class={cn("border-b border-border", local.class)}
60
+ {...rest}
61
+ />
62
+ );
63
+ };
64
+
65
+ export type AccordionTriggerProps<T extends ValidComponent = "button"> =
66
+ AccordionPrimitive.AccordionTriggerProps<T> & {
67
+ class?: string;
68
+ children?: JSX.Element;
69
+ };
70
+
71
+ /**
72
+ * Header trigger button toggling the expansion of an AccordionItem.
73
+ */
74
+ export const AccordionTrigger = <T extends ValidComponent = "button">(
75
+ props: PolymorphicProps<T, AccordionTriggerProps<T>>
76
+ ) => {
77
+ const [local, rest] = splitProps(props as AccordionTriggerProps, ["class", "children"]);
78
+
79
+ return (
80
+ <AccordionPrimitive.Header class="flex">
81
+ <AccordionPrimitive.Trigger
82
+ class={cn(
83
+ "flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-expanded]>svg]:rotate-180 cursor-pointer text-foreground",
84
+ local.class
85
+ )}
86
+ {...rest}
87
+ >
88
+ {local.children}
89
+ <svg
90
+ class="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200"
91
+ viewBox="0 0 24 24"
92
+ fill="none"
93
+ stroke="currentColor"
94
+ stroke-width="2"
95
+ >
96
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
97
+ </svg>
98
+ </AccordionPrimitive.Trigger>
99
+ </AccordionPrimitive.Header>
100
+ );
101
+ };
102
+
103
+ export type AccordionContentProps<T extends ValidComponent = "div"> =
104
+ AccordionPrimitive.AccordionContentProps<T> & {
105
+ class?: string;
106
+ children?: JSX.Element;
107
+ };
108
+
109
+ /**
110
+ * Collapsible content panel revealed when the associated AccordionItem is open.
111
+ */
112
+ export const AccordionContent = <T extends ValidComponent = "div">(
113
+ props: PolymorphicProps<T, AccordionContentProps<T>>
114
+ ) => {
115
+ const [local, rest] = splitProps(props as AccordionContentProps, ["class", "children"]);
116
+
117
+ return (
118
+ <AccordionPrimitive.Content
119
+ class={cn(
120
+ "overflow-hidden text-sm text-muted-foreground transition-all",
121
+ local.class
122
+ )}
123
+ {...rest}
124
+ >
125
+ <div class="pb-4 pt-0">{local.children}</div>
126
+ </AccordionPrimitive.Content>
127
+ );
128
+ };
@@ -0,0 +1,155 @@
1
+ import {
2
+ createSignal,
3
+ splitProps,
4
+ onMount,
5
+ onCleanup,
6
+ Show,
7
+ type Component,
8
+ type JSX,
9
+ } from "solid-js";
10
+ import { cva, type VariantProps } from "class-variance-authority";
11
+ import { cn } from "@/lib/cn";
12
+
13
+ /**
14
+ * CVA variants for Alert status styles and color themes.
15
+ */
16
+ export const alertVariants = cva(
17
+ "relative w-full rounded-lg border p-4 transition-all duration-200 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
18
+ {
19
+ variants: {
20
+ variant: {
21
+ default:
22
+ "bg-background text-foreground border-border",
23
+ info:
24
+ "bg-blue-50 text-blue-900 border-blue-200 dark:bg-blue-950/50 dark:text-blue-200 dark:border-blue-900/50",
25
+ success:
26
+ "bg-emerald-50 text-emerald-900 border-emerald-200 dark:bg-emerald-950/50 dark:text-emerald-200 dark:border-emerald-900/50",
27
+ warning:
28
+ "bg-amber-50 text-amber-900 border-amber-200 dark:bg-amber-950/50 dark:text-amber-200 dark:border-amber-950/50",
29
+ destructive:
30
+ "bg-destructive/15 text-destructive border-destructive/30",
31
+ },
32
+ },
33
+ defaultVariants: {
34
+ variant: "default",
35
+ },
36
+ }
37
+ );
38
+
39
+ export interface AlertProps
40
+ extends JSX.HTMLAttributes<HTMLDivElement>,
41
+ VariantProps<typeof alertVariants> {
42
+ /** Display an interactive close (X) button */
43
+ closable?: boolean;
44
+ /** Auto-dismiss timer duration in milliseconds */
45
+ duration?: number;
46
+ /** Callback fired when the alert is dismissed */
47
+ onClose?: () => void;
48
+ class?: string;
49
+ }
50
+
51
+ /**
52
+ * Nikala UI Alert notification banner with auto-dismiss timer and theme variants.
53
+ */
54
+ export const Alert: Component<AlertProps> = (props) => {
55
+ const [local, rest] = splitProps(props, [
56
+ "variant",
57
+ "closable",
58
+ "duration",
59
+ "onClose",
60
+ "class",
61
+ "children",
62
+ ]);
63
+
64
+ const [isVisible, setIsVisible] = createSignal(true);
65
+ let timer: ReturnType<typeof setTimeout> | undefined;
66
+
67
+ const handleClose = () => {
68
+ setIsVisible(false);
69
+ if (typeof local.onClose === "function") {
70
+ local.onClose();
71
+ }
72
+ };
73
+
74
+ onMount(() => {
75
+ if (local.duration && local.duration > 0) {
76
+ timer = setTimeout(() => {
77
+ handleClose();
78
+ }, local.duration);
79
+ }
80
+ });
81
+
82
+ onCleanup(() => {
83
+ if (timer) clearTimeout(timer);
84
+ });
85
+
86
+ return (
87
+ <Show when={isVisible()}>
88
+ <div
89
+ role="alert"
90
+ class={cn(
91
+ alertVariants({ variant: local.variant }),
92
+ "animate-in fade-in-0 duration-200",
93
+ local.class
94
+ )}
95
+ {...rest}
96
+ >
97
+ {local.children}
98
+ <Show when={local.closable || typeof local.onClose === "function"}>
99
+ <button
100
+ type="button"
101
+ aria-label="Close alert"
102
+ onClick={handleClose}
103
+ class="absolute right-3 top-3 rounded-md p-1 text-zinc-500 hover:bg-zinc-100 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-50 transition-colors cursor-pointer"
104
+ >
105
+ <svg
106
+ class="h-4 w-4"
107
+ viewBox="0 0 24 24"
108
+ fill="none"
109
+ stroke="currentColor"
110
+ stroke-width="2"
111
+ >
112
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
113
+ </svg>
114
+ </button>
115
+ </Show>
116
+ </div>
117
+ </Show>
118
+ );
119
+ };
120
+
121
+ export interface AlertTitleProps extends JSX.HTMLAttributes<HTMLHeadingElement> {
122
+ class?: string;
123
+ }
124
+
125
+ /**
126
+ * Header title element for the Alert banner.
127
+ */
128
+ export const AlertTitle: Component<AlertTitleProps> = (props) => {
129
+ const [local, rest] = splitProps(props, ["class"]);
130
+
131
+ return (
132
+ <h5
133
+ class={cn("mb-1 font-medium leading-none tracking-tight text-foreground", local.class)}
134
+ {...rest}
135
+ />
136
+ );
137
+ };
138
+
139
+ export interface AlertDescriptionProps extends JSX.HTMLAttributes<HTMLDivElement> {
140
+ class?: string;
141
+ }
142
+
143
+ /**
144
+ * Body text content for the Alert banner.
145
+ */
146
+ export const AlertDescription: Component<AlertDescriptionProps> = (props) => {
147
+ const [local, rest] = splitProps(props, ["class"]);
148
+
149
+ return (
150
+ <div
151
+ class={cn("text-sm [&_p]:leading-relaxed text-muted-foreground", local.class)}
152
+ {...rest}
153
+ />
154
+ );
155
+ };
@@ -0,0 +1,114 @@
1
+ import {
2
+ createSignal,
3
+ createEffect,
4
+ Show,
5
+ splitProps,
6
+ type Component,
7
+ type JSX,
8
+ } from "solid-js";
9
+ import { cn } from "@/lib/cn";
10
+
11
+ export interface AvatarProps extends JSX.HTMLAttributes<HTMLDivElement> {
12
+ class?: string;
13
+ }
14
+
15
+ /**
16
+ * Root Avatar container component.
17
+ */
18
+ export const Avatar: Component<AvatarProps> = (props) => {
19
+ const [local, rest] = splitProps(props, ["class"]);
20
+
21
+ return (
22
+ <div
23
+ class={cn(
24
+ "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full border border-border bg-muted",
25
+ local.class
26
+ )}
27
+ {...rest}
28
+ />
29
+ );
30
+ };
31
+
32
+ export interface AvatarImageProps
33
+ extends JSX.ImgHTMLAttributes<HTMLImageElement> {
34
+ class?: string;
35
+ }
36
+
37
+ /**
38
+ * Image element for the Avatar component with background status loader.
39
+ */
40
+ export const AvatarImage: Component<AvatarImageProps> = (props) => {
41
+ const [local, rest] = splitProps(props, [
42
+ "class",
43
+ "src",
44
+ "alt",
45
+ "onLoad",
46
+ "onError",
47
+ ]);
48
+ const [status, setStatus] = createSignal<"loading" | "loaded" | "error">(
49
+ "loading"
50
+ );
51
+
52
+ /* Pre-test image loading in background JS to prevent native broken icon flashes */
53
+ createEffect(() => {
54
+ const src = local.src;
55
+ if (!src) {
56
+ setStatus("error");
57
+ return;
58
+ }
59
+
60
+ const img = new Image();
61
+ img.src = src;
62
+ img.onload = () => setStatus("loaded");
63
+ img.onerror = () => setStatus("error");
64
+ });
65
+
66
+ const handleLoad: JSX.EventHandlerUnion<HTMLImageElement, Event> = (e) => {
67
+ setStatus("loaded");
68
+ if (typeof local.onLoad === "function") {
69
+ (local.onLoad as (e: Event) => void)(e);
70
+ }
71
+ };
72
+
73
+ const handleError: JSX.EventHandlerUnion<HTMLImageElement, Event> = (e) => {
74
+ setStatus("error");
75
+ if (typeof local.onError === "function") {
76
+ (local.onError as (e: Event) => void)(e);
77
+ }
78
+ };
79
+
80
+ return (
81
+ <Show when={status() === "loaded"}>
82
+ <img
83
+ src={local.src}
84
+ alt={local.alt}
85
+ class={cn("aspect-square h-full w-full object-cover", local.class)}
86
+ onLoad={handleLoad}
87
+ onError={handleError}
88
+ {...rest}
89
+ />
90
+ </Show>
91
+ );
92
+ };
93
+
94
+ export interface AvatarFallbackProps
95
+ extends JSX.HTMLAttributes<HTMLDivElement> {
96
+ class?: string;
97
+ }
98
+
99
+ /**
100
+ * Fallback container for rendering initials or icons when avatar image is missing/broken.
101
+ */
102
+ export const AvatarFallback: Component<AvatarFallbackProps> = (props) => {
103
+ const [local, rest] = splitProps(props, ["class"]);
104
+
105
+ return (
106
+ <div
107
+ class={cn(
108
+ "flex h-full w-full items-center justify-center bg-muted text-sm font-medium text-muted-foreground select-none",
109
+ local.class
110
+ )}
111
+ {...rest}
112
+ />
113
+ );
114
+ };
@@ -0,0 +1,50 @@
1
+ import { splitProps, type Component, type JSX } from "solid-js";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "@/lib/cn";
4
+
5
+ /**
6
+ * Class variance authority configuration for badge styling variants.
7
+ */
8
+ export const badgeVariants = cva(
9
+ "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
10
+ {
11
+ variants: {
12
+ variant: {
13
+ default:
14
+ "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
15
+ secondary:
16
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
17
+ destructive:
18
+ "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
19
+ outline:
20
+ "text-foreground border-border",
21
+ },
22
+ },
23
+ defaultVariants: {
24
+ variant: "default",
25
+ },
26
+ }
27
+ );
28
+
29
+ /**
30
+ * Props interface for the Badge component extending standard HTML div attributes.
31
+ */
32
+ export interface BadgeProps
33
+ extends JSX.HTMLAttributes<HTMLDivElement>,
34
+ VariantProps<typeof badgeVariants> {
35
+ class?: string;
36
+ }
37
+
38
+ /**
39
+ * Nikala UI Badge component built for SolidJS with Tailwind CSS v4 styling.
40
+ */
41
+ export const Badge: Component<BadgeProps> = (props) => {
42
+ // Use splitProps to preserve SolidJS reactivity
43
+ const [local, rest] = splitProps(props, ["variant", "class", "children"]);
44
+
45
+ return (
46
+ <div class={cn(badgeVariants({ variant: local.variant }), local.class)} {...rest}>
47
+ {local.children}
48
+ </div>
49
+ );
50
+ };