@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.
- package/README.md +42 -0
- package/package.json +25 -0
- package/registry/accordion.json +18 -0
- package/registry/alert.json +18 -0
- package/registry/avatar.json +17 -0
- package/registry/badge.json +18 -0
- package/registry/banner.json +19 -0
- package/registry/breadcrumb.json +17 -0
- package/registry/button.json +18 -0
- package/registry/card.json +17 -0
- package/registry/checkbox.json +17 -0
- package/registry/command.json +25 -0
- package/registry/dialog.json +18 -0
- package/registry/dropdown-menu.json +18 -0
- package/registry/index.json +297 -0
- package/registry/input-group.json +21 -0
- package/registry/input.json +17 -0
- package/registry/kbd.json +18 -0
- package/registry/label.json +18 -0
- package/registry/list.json +20 -0
- package/registry/radio-group.json +18 -0
- package/registry/select.json +18 -0
- package/registry/separator.json +17 -0
- package/registry/sheet.json +19 -0
- package/registry/skeleton.json +17 -0
- package/registry/switch.json +17 -0
- package/registry/tabs.json +17 -0
- package/registry/textarea.json +17 -0
- package/registry/theme-manager.json +38 -0
- package/src/index.css +11 -0
- package/src/lib/cn.ts +9 -0
- package/src/registry/components/ui/accordion.tsx +128 -0
- package/src/registry/components/ui/alert.tsx +155 -0
- package/src/registry/components/ui/avatar.tsx +114 -0
- package/src/registry/components/ui/badge.tsx +50 -0
- package/src/registry/components/ui/banner.tsx +189 -0
- package/src/registry/components/ui/breadcrumb.tsx +136 -0
- package/src/registry/components/ui/button.tsx +63 -0
- package/src/registry/components/ui/card.tsx +113 -0
- package/src/registry/components/ui/checkbox.tsx +85 -0
- package/src/registry/components/ui/command.tsx +286 -0
- package/src/registry/components/ui/dialog.tsx +195 -0
- package/src/registry/components/ui/dropdown-menu.tsx +250 -0
- package/src/registry/components/ui/input-group.tsx +80 -0
- package/src/registry/components/ui/input.tsx +28 -0
- package/src/registry/components/ui/kbd.tsx +73 -0
- package/src/registry/components/ui/label.tsx +30 -0
- package/src/registry/components/ui/list.tsx +222 -0
- package/src/registry/components/ui/radio-group.tsx +95 -0
- package/src/registry/components/ui/select.tsx +138 -0
- package/src/registry/components/ui/separator.tsx +37 -0
- package/src/registry/components/ui/sheet.tsx +216 -0
- package/src/registry/components/ui/skeleton.tsx +24 -0
- package/src/registry/components/ui/switch.tsx +79 -0
- package/src/registry/components/ui/tabs.tsx +212 -0
- package/src/registry/components/ui/textarea.tsx +82 -0
- package/src/registry/components/ui/theme-toggle.tsx +195 -0
- package/src/registry/index.ts +50 -0
- package/src/registry/metadata.ts +152 -0
- package/src/registry/providers/theme-provider.tsx +208 -0
- package/src/registry/providers/theme-script.tsx +58 -0
- 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
package/src/lib/cn.ts
ADDED
|
@@ -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
|
+
};
|