@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,189 @@
|
|
|
1
|
+
// src/components/ui/banner.tsx
|
|
2
|
+
import {
|
|
3
|
+
createSignal,
|
|
4
|
+
onMount,
|
|
5
|
+
onCleanup,
|
|
6
|
+
Show,
|
|
7
|
+
splitProps,
|
|
8
|
+
type Component,
|
|
9
|
+
type JSX,
|
|
10
|
+
} from "solid-js";
|
|
11
|
+
import { Dynamic } from "solid-js/web";
|
|
12
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
13
|
+
import { Info, AlertTriangle, CheckCircle, AlertCircle, X } from "lucide-solid";
|
|
14
|
+
import { cn } from "@/lib/cn";
|
|
15
|
+
|
|
16
|
+
export const bannerVariants = cva(
|
|
17
|
+
"relative flex w-full items-center justify-between gap-3 px-4 py-2.5 text-sm transition-all duration-300 ease-in-out",
|
|
18
|
+
{
|
|
19
|
+
variants: {
|
|
20
|
+
variant: {
|
|
21
|
+
default:
|
|
22
|
+
"bg-zinc-900 text-zinc-50 dark:bg-zinc-100 dark:text-zinc-900",
|
|
23
|
+
warning:
|
|
24
|
+
"bg-amber-500/15 text-amber-900 border-b border-amber-500/20 dark:text-amber-200",
|
|
25
|
+
info:
|
|
26
|
+
"bg-sky-500/15 text-sky-900 border-b border-sky-500/20 dark:text-sky-200",
|
|
27
|
+
success:
|
|
28
|
+
"bg-emerald-500/15 text-emerald-900 border-b border-emerald-500/20 dark:text-emerald-200",
|
|
29
|
+
destructive:
|
|
30
|
+
"bg-rose-500/15 text-rose-900 border-b border-rose-500/20 dark:text-rose-200",
|
|
31
|
+
pirosmani:
|
|
32
|
+
"bg-[#722f37] text-white border-b border-[#8a3943] shadow-sm",
|
|
33
|
+
},
|
|
34
|
+
sticky: {
|
|
35
|
+
true: "sticky top-0 z-50 backdrop-blur-sm",
|
|
36
|
+
false: "relative",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
defaultVariants: {
|
|
40
|
+
variant: "default",
|
|
41
|
+
sticky: false,
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
export interface BannerProps
|
|
47
|
+
extends JSX.HTMLAttributes<HTMLDivElement>,
|
|
48
|
+
VariantProps<typeof bannerVariants> {
|
|
49
|
+
dismissible?: boolean;
|
|
50
|
+
autoHideDelay?: number;
|
|
51
|
+
storageKey?: string;
|
|
52
|
+
showIcon?: boolean;
|
|
53
|
+
icon?: Component<{ class?: string }>;
|
|
54
|
+
link?: string;
|
|
55
|
+
linkText?: string;
|
|
56
|
+
linkTarget?: string;
|
|
57
|
+
onDismiss?: () => void;
|
|
58
|
+
class?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Nikala UI Banner Component.
|
|
63
|
+
*/
|
|
64
|
+
export const Banner: Component<BannerProps> = (props) => {
|
|
65
|
+
const [local, rest] = splitProps(props, [
|
|
66
|
+
"variant",
|
|
67
|
+
"sticky",
|
|
68
|
+
"dismissible",
|
|
69
|
+
"autoHideDelay",
|
|
70
|
+
"storageKey",
|
|
71
|
+
"showIcon",
|
|
72
|
+
"icon",
|
|
73
|
+
"link",
|
|
74
|
+
"linkText",
|
|
75
|
+
"linkTarget",
|
|
76
|
+
"onDismiss",
|
|
77
|
+
"class",
|
|
78
|
+
"children",
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
const [visible, setVisible] = createSignal(true);
|
|
82
|
+
let timerId: ReturnType<typeof setTimeout> | undefined;
|
|
83
|
+
|
|
84
|
+
onMount(() => {
|
|
85
|
+
if (local.storageKey) {
|
|
86
|
+
try {
|
|
87
|
+
const isDismissed = localStorage.getItem(local.storageKey);
|
|
88
|
+
if (isDismissed === "true") {
|
|
89
|
+
setVisible(false);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
} catch (e) { }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (local.autoHideDelay && local.autoHideDelay > 0) {
|
|
96
|
+
timerId = setTimeout(() => {
|
|
97
|
+
handleDismiss();
|
|
98
|
+
}, local.autoHideDelay);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
onCleanup(() => {
|
|
103
|
+
if (timerId) clearTimeout(timerId);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const handleDismiss = () => {
|
|
107
|
+
setVisible(false);
|
|
108
|
+
|
|
109
|
+
if (local.storageKey) {
|
|
110
|
+
try {
|
|
111
|
+
localStorage.setItem(local.storageKey, "true");
|
|
112
|
+
} catch (e) {}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (typeof local.onDismiss === "function") {
|
|
116
|
+
local.onDismiss();
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/* Default icon selector based on banner variant */
|
|
121
|
+
const getDefaultIcon = () => {
|
|
122
|
+
switch (local.variant) {
|
|
123
|
+
case "warning":
|
|
124
|
+
return AlertTriangle;
|
|
125
|
+
case "success":
|
|
126
|
+
return CheckCircle;
|
|
127
|
+
case "destructive":
|
|
128
|
+
return AlertCircle;
|
|
129
|
+
default:
|
|
130
|
+
return Info;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const activeIcon = () => local.icon || getDefaultIcon();
|
|
135
|
+
const shouldShowIcon = () => local.showIcon !== false;
|
|
136
|
+
const shouldBeDismissible = () => local.dismissible !== false;
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<Show when={visible()}>
|
|
140
|
+
<div
|
|
141
|
+
class={cn(
|
|
142
|
+
bannerVariants({ variant: local.variant, sticky: local.sticky }),
|
|
143
|
+
local.class
|
|
144
|
+
)}
|
|
145
|
+
{...rest}
|
|
146
|
+
>
|
|
147
|
+
<div class="flex flex-1 items-center justify-center gap-2 text-center sm:text-left">
|
|
148
|
+
{/* Dynamic Lucide Icon */}
|
|
149
|
+
<Show when={shouldShowIcon()}>
|
|
150
|
+
<span class="inline-flex shrink-0 items-center justify-center">
|
|
151
|
+
<Dynamic component={activeIcon()} class="h-4 w-4 opacity-80" />
|
|
152
|
+
</span>
|
|
153
|
+
</Show>
|
|
154
|
+
|
|
155
|
+
{/* Text Content */}
|
|
156
|
+
<div class="flex-1 text-xs font-medium sm:text-sm">
|
|
157
|
+
{local.children}
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
{/* Action Link */}
|
|
161
|
+
<Show when={local.link}>
|
|
162
|
+
<div class="shrink-0">
|
|
163
|
+
<a
|
|
164
|
+
href={local.link}
|
|
165
|
+
target={local.linkTarget || "_blank"}
|
|
166
|
+
rel="noreferrer"
|
|
167
|
+
class="underline font-semibold hover:opacity-80 transition-opacity"
|
|
168
|
+
>
|
|
169
|
+
{local.linkText || "Learn more"}
|
|
170
|
+
</a>
|
|
171
|
+
</div>
|
|
172
|
+
</Show>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
{/* Close Button */}
|
|
176
|
+
<Show when={shouldBeDismissible()}>
|
|
177
|
+
<button
|
|
178
|
+
type="button"
|
|
179
|
+
onClick={handleDismiss}
|
|
180
|
+
class="shrink-0 rounded-md p-1 opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring"
|
|
181
|
+
aria-label="Dismiss banner"
|
|
182
|
+
>
|
|
183
|
+
<X class="h-4 w-4" />
|
|
184
|
+
</button>
|
|
185
|
+
</Show>
|
|
186
|
+
</div>
|
|
187
|
+
</Show>
|
|
188
|
+
);
|
|
189
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { splitProps, type Component, type JSX } from "solid-js";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface BreadcrumbProps extends JSX.HTMLAttributes<HTMLElement> {
|
|
5
|
+
class?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Root navigation container for the Nikala UI Breadcrumb hierarchy.
|
|
10
|
+
*/
|
|
11
|
+
export const Breadcrumb: Component<BreadcrumbProps> = (props) => {
|
|
12
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
13
|
+
return <nav aria-label="breadcrumb" class={cn("relative w-full", local.class)} {...rest} />;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export interface BreadcrumbListProps extends JSX.HTMLAttributes<HTMLOListElement> {
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Ordered list container housing all breadcrumb path items.
|
|
22
|
+
*/
|
|
23
|
+
export const BreadcrumbList: Component<BreadcrumbListProps> = (props) => {
|
|
24
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
25
|
+
return (
|
|
26
|
+
<ol
|
|
27
|
+
class={cn(
|
|
28
|
+
"flex flex-wrap items-center gap-1.5 text-sm text-muted-foreground break-words sm:gap-2.5",
|
|
29
|
+
local.class
|
|
30
|
+
)}
|
|
31
|
+
{...rest}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export interface BreadcrumbItemProps extends JSX.HTMLAttributes<HTMLLIElement> {
|
|
37
|
+
class?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Individual list item element wrapper in the breadcrumb hierarchy.
|
|
42
|
+
*/
|
|
43
|
+
export const BreadcrumbItem: Component<BreadcrumbItemProps> = (props) => {
|
|
44
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
45
|
+
return <li class={cn("inline-flex items-center gap-1.5", local.class)} {...rest} />;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export interface BreadcrumbLinkProps extends JSX.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
49
|
+
class?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Clickable breadcrumb trail link element.
|
|
54
|
+
*/
|
|
55
|
+
export const BreadcrumbLink: Component<BreadcrumbLinkProps> = (props) => {
|
|
56
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
57
|
+
return (
|
|
58
|
+
<a
|
|
59
|
+
class={cn(
|
|
60
|
+
"transition-colors hover:text-foreground underline-offset-4 hover:underline",
|
|
61
|
+
local.class
|
|
62
|
+
)}
|
|
63
|
+
{...rest}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export interface BreadcrumbPageProps extends JSX.HTMLAttributes<HTMLSpanElement> {
|
|
69
|
+
class?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Non-clickable active page indicator in breadcrumb hierarchy.
|
|
74
|
+
*/
|
|
75
|
+
export const BreadcrumbPage: Component<BreadcrumbPageProps> = (props) => {
|
|
76
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
77
|
+
return (
|
|
78
|
+
<span
|
|
79
|
+
role="link"
|
|
80
|
+
aria-disabled="true"
|
|
81
|
+
aria-current="page"
|
|
82
|
+
class={cn("font-normal text-foreground", local.class)}
|
|
83
|
+
{...rest}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export interface BreadcrumbSeparatorProps extends JSX.HTMLAttributes<HTMLLIElement> {
|
|
89
|
+
class?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Visual separator icon rendered between breadcrumb path links.
|
|
94
|
+
*/
|
|
95
|
+
export const BreadcrumbSeparator: Component<BreadcrumbSeparatorProps> = (props) => {
|
|
96
|
+
const [local, rest] = splitProps(props, ["class", "children"]);
|
|
97
|
+
return (
|
|
98
|
+
<li
|
|
99
|
+
role="presentation"
|
|
100
|
+
aria-hidden="true"
|
|
101
|
+
class={cn("[&>svg]:size-3.5 text-muted-foreground", local.class)}
|
|
102
|
+
{...rest}
|
|
103
|
+
>
|
|
104
|
+
{local.children || (
|
|
105
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
106
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
|
|
107
|
+
</svg>
|
|
108
|
+
)}
|
|
109
|
+
</li>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export interface BreadcrumbEllipsisProps extends JSX.HTMLAttributes<HTMLSpanElement> {
|
|
114
|
+
class?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Ellipsis element representing truncated intermediate breadcrumb items.
|
|
119
|
+
*/
|
|
120
|
+
export const BreadcrumbEllipsis: Component<BreadcrumbEllipsisProps> = (props) => {
|
|
121
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
122
|
+
return (
|
|
123
|
+
<span
|
|
124
|
+
role="presentation"
|
|
125
|
+
aria-hidden="true"
|
|
126
|
+
class={cn("flex h-9 w-9 items-center justify-center text-muted-foreground", local.class)}
|
|
127
|
+
{...rest}
|
|
128
|
+
>
|
|
129
|
+
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
130
|
+
<circle cx="12" cy="12" r="1" />
|
|
131
|
+
<circle cx="19" cy="12" r="1" />
|
|
132
|
+
<circle cx="5" cy="12" r="1" />
|
|
133
|
+
</svg>
|
|
134
|
+
</span>
|
|
135
|
+
);
|
|
136
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
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 button styling variants and sizes.
|
|
7
|
+
*/
|
|
8
|
+
export const buttonVariants = cva(
|
|
9
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 cursor-pointer",
|
|
10
|
+
{
|
|
11
|
+
variants: {
|
|
12
|
+
variant: {
|
|
13
|
+
default:
|
|
14
|
+
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
|
15
|
+
destructive:
|
|
16
|
+
"bg-red-600 text-zinc-50 shadow-sm hover:bg-red-600/90 dark:bg-red-900 dark:text-zinc-50 dark:hover:bg-red-900/90",
|
|
17
|
+
outline:
|
|
18
|
+
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
|
19
|
+
secondary:
|
|
20
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
21
|
+
ghost:
|
|
22
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
23
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
24
|
+
},
|
|
25
|
+
size: {
|
|
26
|
+
default: "h-9 px-4 py-2",
|
|
27
|
+
sm: "h-8 rounded-md px-3 text-xs",
|
|
28
|
+
lg: "h-10 rounded-md px-8",
|
|
29
|
+
icon: "h-9 w-9",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: {
|
|
33
|
+
variant: "default",
|
|
34
|
+
size: "default",
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Props interface for the Button component extending standard HTML button attributes.
|
|
41
|
+
*/
|
|
42
|
+
export interface ButtonProps
|
|
43
|
+
extends JSX.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
44
|
+
VariantProps<typeof buttonVariants> {
|
|
45
|
+
class?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Nikala UI Button component built for SolidJS with Tailwind CSS v4 styling.
|
|
50
|
+
*/
|
|
51
|
+
export const Button: Component<ButtonProps> = (props) => {
|
|
52
|
+
// Use splitProps to preserve SolidJS reactivity for destructured props
|
|
53
|
+
const [local, rest] = splitProps(props, ["variant", "size", "class", "children"]);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<button
|
|
57
|
+
class={cn(buttonVariants({ variant: local.variant, size: local.size }), local.class)}
|
|
58
|
+
{...rest}
|
|
59
|
+
>
|
|
60
|
+
{local.children}
|
|
61
|
+
</button>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { splitProps, type Component, type JSX } from "solid-js";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface CardProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
class?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Root Card container component.
|
|
10
|
+
*/
|
|
11
|
+
export const Card: Component<CardProps> = (props) => {
|
|
12
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
class={cn(
|
|
17
|
+
"rounded-lg border border-border bg-card text-card-foreground shadow-sm",
|
|
18
|
+
local.class
|
|
19
|
+
)}
|
|
20
|
+
{...rest}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export interface CardHeaderProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
26
|
+
class?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Header wrapper for the Card component.
|
|
31
|
+
*/
|
|
32
|
+
export const CardHeader: Component<CardHeaderProps> = (props) => {
|
|
33
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div
|
|
37
|
+
class={cn("flex flex-col space-y-1.5 p-6", local.class)}
|
|
38
|
+
{...rest}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export interface CardTitleProps extends JSX.HTMLAttributes<HTMLHeadingElement> {
|
|
44
|
+
class?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Main title heading for the Card header.
|
|
49
|
+
*/
|
|
50
|
+
export const CardTitle: Component<CardTitleProps> = (props) => {
|
|
51
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<h3
|
|
55
|
+
class={cn("font-semibold leading-none tracking-tight text-lg", local.class)}
|
|
56
|
+
{...rest}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export interface CardDescriptionProps extends JSX.HTMLAttributes<HTMLParagraphElement> {
|
|
62
|
+
class?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Subtitle description text for the Card header.
|
|
67
|
+
*/
|
|
68
|
+
export const CardDescription: Component<CardDescriptionProps> = (props) => {
|
|
69
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<p
|
|
73
|
+
class={cn("text-sm text-muted-foreground", local.class)}
|
|
74
|
+
{...rest}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export interface CardContentProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
80
|
+
class?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Main body content area for the Card.
|
|
85
|
+
*/
|
|
86
|
+
export const CardContent: Component<CardContentProps> = (props) => {
|
|
87
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div
|
|
91
|
+
class={cn("p-6 pt-0", local.class)}
|
|
92
|
+
{...rest}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export interface CardFooterProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
98
|
+
class?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Footer action area for the Card.
|
|
103
|
+
*/
|
|
104
|
+
export const CardFooter: Component<CardFooterProps> = (props) => {
|
|
105
|
+
const [local, rest] = splitProps(props, ["class"]);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div
|
|
109
|
+
class={cn("flex items-center p-6 pt-0", local.class)}
|
|
110
|
+
{...rest}
|
|
111
|
+
/>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createSignal, splitProps, Show, type Component, type JSX } from "solid-js";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface CheckboxProps
|
|
5
|
+
extends Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, "onChange"> {
|
|
6
|
+
/** Controlled checked state */
|
|
7
|
+
checked?: boolean;
|
|
8
|
+
/** Uncontrolled default checked state */
|
|
9
|
+
defaultChecked?: boolean;
|
|
10
|
+
/** Event handler triggered when checked state changes */
|
|
11
|
+
onChange?: (checked: boolean) => void;
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Nikala UI Checkbox component built for SolidJS with Tailwind CSS v4 styling.
|
|
17
|
+
*/
|
|
18
|
+
export const Checkbox: Component<CheckboxProps> = (props) => {
|
|
19
|
+
// Use splitProps to preserve SolidJS reactivity
|
|
20
|
+
const [local, rest] = splitProps(props, [
|
|
21
|
+
"checked",
|
|
22
|
+
"defaultChecked",
|
|
23
|
+
"onChange",
|
|
24
|
+
"disabled",
|
|
25
|
+
"class",
|
|
26
|
+
"onClick",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
// Internal state for uncontrolled mode
|
|
30
|
+
const [internalChecked, setInternalChecked] = createSignal(
|
|
31
|
+
local.defaultChecked ?? false
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Computed checked state
|
|
35
|
+
const isChecked = () =>
|
|
36
|
+
local.checked !== undefined ? local.checked : internalChecked();
|
|
37
|
+
|
|
38
|
+
const toggle = (
|
|
39
|
+
e: MouseEvent & { currentTarget: HTMLButtonElement; target: Element }
|
|
40
|
+
) => {
|
|
41
|
+
if (local.disabled) return;
|
|
42
|
+
|
|
43
|
+
const nextState = !isChecked();
|
|
44
|
+
if (local.checked === undefined) {
|
|
45
|
+
setInternalChecked(nextState);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof local.onChange === "function") {
|
|
49
|
+
local.onChange(nextState);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (typeof local.onClick === "function") {
|
|
53
|
+
local.onClick(e);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
role="checkbox"
|
|
61
|
+
aria-checked={isChecked()}
|
|
62
|
+
data-state={isChecked() ? "checked" : "unchecked"}
|
|
63
|
+
disabled={local.disabled}
|
|
64
|
+
onClick={toggle}
|
|
65
|
+
class={cn(
|
|
66
|
+
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground flex items-center justify-center transition-colors",
|
|
67
|
+
local.class
|
|
68
|
+
)}
|
|
69
|
+
{...rest}
|
|
70
|
+
>
|
|
71
|
+
<Show when={isChecked()}>
|
|
72
|
+
<svg
|
|
73
|
+
class="h-3.5 w-3.5 fill-none stroke-current stroke-[3]"
|
|
74
|
+
viewBox="0 0 24 24"
|
|
75
|
+
>
|
|
76
|
+
<path
|
|
77
|
+
stroke-linecap="round"
|
|
78
|
+
stroke-linejoin="round"
|
|
79
|
+
d="M5 13l4 4L19 7"
|
|
80
|
+
/>
|
|
81
|
+
</svg>
|
|
82
|
+
</Show>
|
|
83
|
+
</button>
|
|
84
|
+
);
|
|
85
|
+
};
|