@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,250 @@
1
+ import { splitProps, type Component, type JSX, type ValidComponent } from "solid-js";
2
+ import * as DropdownMenuPrimitive from "@kobalte/core/dropdown-menu";
3
+ import type { PolymorphicProps } from "@kobalte/core/polymorphic";
4
+ import { cn } from "@/lib/cn";
5
+
6
+ export type DropdownMenuRootProps = Omit<
7
+ DropdownMenuPrimitive.DropdownMenuRootProps,
8
+ "placement"
9
+ > & {
10
+ placement?:
11
+ | "top"
12
+ | "top-start"
13
+ | "top-end"
14
+ | "right"
15
+ | "right-start"
16
+ | "right-end"
17
+ | "bottom"
18
+ | "bottom-start"
19
+ | "bottom-end"
20
+ | "left"
21
+ | "left-start"
22
+ | "left-end";
23
+ };
24
+
25
+ export const DropdownMenu: Component<DropdownMenuRootProps> = (props) => {
26
+ return <DropdownMenuPrimitive.Root {...props} />;
27
+ };
28
+
29
+ export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
30
+ export const DropdownMenuGroup = DropdownMenuPrimitive.Group;
31
+ export const DropdownMenuSub = DropdownMenuPrimitive.Sub;
32
+ export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
33
+
34
+ export type DropdownMenuSubTriggerProps<T extends ValidComponent = "div"> =
35
+ DropdownMenuPrimitive.DropdownMenuSubTriggerProps<T> & {
36
+ class?: string;
37
+ children?: JSX.Element;
38
+ inset?: boolean;
39
+ };
40
+
41
+ export const DropdownMenuSubTrigger = <T extends ValidComponent = "div">(
42
+ props: PolymorphicProps<T, DropdownMenuSubTriggerProps<T>>
43
+ ) => {
44
+ const [local, rest] = splitProps(props as DropdownMenuSubTriggerProps, ["class", "children", "inset"]);
45
+
46
+ return (
47
+ <DropdownMenuPrimitive.SubTrigger
48
+ class={cn(
49
+ "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent text-foreground",
50
+ local.inset && "pl-8",
51
+ local.class
52
+ )}
53
+ {...(rest as any)}
54
+ >
55
+ {local.children}
56
+ <svg class="ml-auto h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
57
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
58
+ </svg>
59
+ </DropdownMenuPrimitive.SubTrigger>
60
+ );
61
+ };
62
+
63
+ export type DropdownMenuSubContentProps<T extends ValidComponent = "div"> =
64
+ DropdownMenuPrimitive.DropdownMenuSubContentProps<T> & {
65
+ class?: string;
66
+ };
67
+
68
+ export const DropdownMenuSubContent = <T extends ValidComponent = "div">(
69
+ props: PolymorphicProps<T, DropdownMenuSubContentProps<T>>
70
+ ) => {
71
+ const [local, rest] = splitProps(props as DropdownMenuSubContentProps, ["class"]);
72
+
73
+ return (
74
+ <DropdownMenuPrimitive.Portal>
75
+ <DropdownMenuPrimitive.SubContent
76
+ class={cn(
77
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0",
78
+ local.class
79
+ )}
80
+ {...(rest as any)}
81
+ />
82
+ </DropdownMenuPrimitive.Portal>
83
+ );
84
+ };
85
+
86
+ export type DropdownMenuContentProps<T extends ValidComponent = "div"> =
87
+ DropdownMenuPrimitive.DropdownMenuContentProps<T> & {
88
+ class?: string;
89
+ };
90
+
91
+ export const DropdownMenuContent = <T extends ValidComponent = "div">(
92
+ props: PolymorphicProps<T, DropdownMenuContentProps<T>>
93
+ ) => {
94
+ const [local, rest] = splitProps(props as DropdownMenuContentProps, ["class"]);
95
+
96
+ return (
97
+ <DropdownMenuPrimitive.Portal>
98
+ <DropdownMenuPrimitive.Content
99
+ class={cn(
100
+ "z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0",
101
+ local.class
102
+ )}
103
+ {...(rest as any)}
104
+ />
105
+ </DropdownMenuPrimitive.Portal>
106
+ );
107
+ };
108
+
109
+ export type DropdownMenuItemProps<T extends ValidComponent = "div"> =
110
+ DropdownMenuPrimitive.DropdownMenuItemProps<T> & {
111
+ class?: string;
112
+ inset?: boolean;
113
+ variant?: "default" | "destructive";
114
+ };
115
+
116
+ export const DropdownMenuItem = <T extends ValidComponent = "div">(
117
+ props: PolymorphicProps<T, DropdownMenuItemProps<T>>
118
+ ) => {
119
+ const [local, rest] = splitProps(props as DropdownMenuItemProps, [
120
+ "class",
121
+ "inset",
122
+ "variant",
123
+ ]);
124
+
125
+ return (
126
+ <DropdownMenuPrimitive.Item
127
+ class={cn(
128
+ "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
129
+ local.inset && "pl-8",
130
+ local.variant === "destructive" &&
131
+ "text-destructive focus:bg-destructive/15 focus:text-destructive",
132
+ local.class
133
+ )}
134
+ {...(rest as any)}
135
+ />
136
+ );
137
+ };
138
+
139
+ export type DropdownMenuCheckboxItemProps<T extends ValidComponent = "div"> =
140
+ DropdownMenuPrimitive.DropdownMenuCheckboxItemProps<T> & {
141
+ class?: string;
142
+ children?: JSX.Element;
143
+ };
144
+
145
+ export const DropdownMenuCheckboxItem = <T extends ValidComponent = "div">(
146
+ props: PolymorphicProps<T, DropdownMenuCheckboxItemProps<T>>
147
+ ) => {
148
+ const [local, rest] = splitProps(props as DropdownMenuCheckboxItemProps, ["class", "children"]);
149
+
150
+ return (
151
+ <DropdownMenuPrimitive.CheckboxItem
152
+ class={cn(
153
+ "relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
154
+ local.class
155
+ )}
156
+ {...(rest as any)}
157
+ >
158
+ <span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
159
+ <DropdownMenuPrimitive.ItemIndicator>
160
+ <svg class="h-4 w-4 fill-none stroke-current stroke-2" viewBox="0 0 24 24">
161
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
162
+ </svg>
163
+ </DropdownMenuPrimitive.ItemIndicator>
164
+ </span>
165
+ {local.children}
166
+ </DropdownMenuPrimitive.CheckboxItem>
167
+ );
168
+ };
169
+
170
+ export type DropdownMenuRadioItemProps<T extends ValidComponent = "div"> =
171
+ DropdownMenuPrimitive.DropdownMenuRadioItemProps<T> & {
172
+ class?: string;
173
+ children?: JSX.Element;
174
+ };
175
+
176
+ export const DropdownMenuRadioItem = <T extends ValidComponent = "div">(
177
+ props: PolymorphicProps<T, DropdownMenuRadioItemProps<T>>
178
+ ) => {
179
+ const [local, rest] = splitProps(props as DropdownMenuRadioItemProps, ["class", "children"]);
180
+
181
+ return (
182
+ <DropdownMenuPrimitive.RadioItem
183
+ class={cn(
184
+ "relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
185
+ local.class
186
+ )}
187
+ {...(rest as any)}
188
+ >
189
+ <span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
190
+ <DropdownMenuPrimitive.ItemIndicator>
191
+ <span class="h-2 w-2 rounded-full bg-current" />
192
+ </DropdownMenuPrimitive.ItemIndicator>
193
+ </span>
194
+ {local.children}
195
+ </DropdownMenuPrimitive.RadioItem>
196
+ );
197
+ };
198
+
199
+ export interface DropdownMenuLabelProps extends JSX.HTMLAttributes<HTMLDivElement> {
200
+ class?: string;
201
+ inset?: boolean;
202
+ }
203
+
204
+ export const DropdownMenuLabel: Component<DropdownMenuLabelProps> = (props) => {
205
+ const [local, rest] = splitProps(props, ["class", "inset"]);
206
+
207
+ return (
208
+ <div
209
+ class={cn(
210
+ "px-2 py-1.5 text-sm font-semibold text-foreground",
211
+ local.inset && "pl-8",
212
+ local.class
213
+ )}
214
+ {...rest}
215
+ />
216
+ );
217
+ };
218
+
219
+ export type DropdownMenuSeparatorProps<T extends ValidComponent = "hr"> =
220
+ DropdownMenuPrimitive.DropdownMenuSeparatorProps<T> & {
221
+ class?: string;
222
+ };
223
+
224
+ export const DropdownMenuSeparator = <T extends ValidComponent = "hr">(
225
+ props: PolymorphicProps<T, DropdownMenuSeparatorProps<T>>
226
+ ) => {
227
+ const [local, rest] = splitProps(props as DropdownMenuSeparatorProps, ["class"]);
228
+
229
+ return (
230
+ <DropdownMenuPrimitive.Separator
231
+ class={cn("-mx-1 my-1 h-px bg-border", local.class)}
232
+ {...(rest as any)}
233
+ />
234
+ );
235
+ };
236
+
237
+ export interface DropdownMenuShortcutProps extends JSX.HTMLAttributes<HTMLSpanElement> {
238
+ class?: string;
239
+ }
240
+
241
+ export const DropdownMenuShortcut: Component<DropdownMenuShortcutProps> = (props) => {
242
+ const [local, rest] = splitProps(props, ["class"]);
243
+
244
+ return (
245
+ <span
246
+ class={cn("ml-auto text-xs tracking-widest text-muted-foreground", local.class)}
247
+ {...rest}
248
+ />
249
+ );
250
+ };
@@ -0,0 +1,80 @@
1
+ // src/components/ui/input-group.tsx
2
+ import { splitProps, type Component, type JSX } from "solid-js";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import { cn } from "@/lib/cn";
5
+
6
+ /* --- Main InputGroup Wrapper --- */
7
+ export interface InputGroupProps extends JSX.HTMLAttributes<HTMLDivElement> {
8
+ class?: string;
9
+ }
10
+
11
+ export const InputGroup: Component<InputGroupProps> = (props) => {
12
+ const [local, rest] = splitProps(props, ["class", "children"]);
13
+
14
+ return (
15
+ <div
16
+ class={cn(
17
+ "relative flex w-full items-center rounded-md border border-input bg-background text-sm shadow-2xs transition-colors focus-within:border-primary focus-within:ring-1 focus-within:ring-primary",
18
+ local.class
19
+ )}
20
+ {...rest}
21
+ >
22
+ {local.children}
23
+ </div>
24
+ );
25
+ };
26
+
27
+ /* --- InputGroup Addon (Prefix / Suffix) --- */
28
+ export const addonVariants = cva(
29
+ "flex items-center shrink-0 text-muted-foreground select-none",
30
+ {
31
+ variants: {
32
+ align: {
33
+ "inline-start": "order-first pl-3 pr-1",
34
+ "inline-end": "order-last pr-3 pl-1 gap-1",
35
+ },
36
+ },
37
+ defaultVariants: {
38
+ align: "inline-start",
39
+ },
40
+ }
41
+ );
42
+
43
+ export interface InputGroupAddonProps
44
+ extends JSX.HTMLAttributes<HTMLDivElement>,
45
+ VariantProps<typeof addonVariants> {
46
+ class?: string;
47
+ }
48
+
49
+ export const InputGroupAddon: Component<InputGroupAddonProps> = (props) => {
50
+ const [local, rest] = splitProps(props, ["align", "class", "children"]);
51
+
52
+ return (
53
+ <div
54
+ class={cn(addonVariants({ align: local.align }), local.class)}
55
+ {...rest}
56
+ >
57
+ {local.children}
58
+ </div>
59
+ );
60
+ };
61
+
62
+ /* --- InputGroup Native Input --- */
63
+ export interface InputGroupInputProps
64
+ extends JSX.InputHTMLAttributes<HTMLInputElement> {
65
+ class?: string;
66
+ }
67
+
68
+ export const InputGroupInput: Component<InputGroupInputProps> = (props) => {
69
+ const [local, rest] = splitProps(props, ["class"]);
70
+
71
+ return (
72
+ <input
73
+ class={cn(
74
+ "flex h-9 w-full flex-1 border-0 bg-transparent px-2.5 py-1 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
75
+ local.class
76
+ )}
77
+ {...rest}
78
+ />
79
+ );
80
+ };
@@ -0,0 +1,28 @@
1
+ import { splitProps, type Component, type JSX } from "solid-js";
2
+ import { cn } from "@/lib/cn";
3
+
4
+ /**
5
+ * Props interface for the Input component extending standard HTML input attributes.
6
+ */
7
+ export interface InputProps extends JSX.InputHTMLAttributes<HTMLInputElement> {
8
+ class?: string;
9
+ }
10
+
11
+ /**
12
+ * Nikala UI Input component built for SolidJS with Tailwind CSS v4 styling.
13
+ */
14
+ export const Input: Component<InputProps> = (props) => {
15
+ // Use splitProps to preserve SolidJS reactivity for destructured props
16
+ const [local, rest] = splitProps(props, ["class", "type"]);
17
+
18
+ return (
19
+ <input
20
+ type={local.type || "text"}
21
+ class={cn(
22
+ "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
23
+ local.class
24
+ )}
25
+ {...rest}
26
+ />
27
+ );
28
+ };
@@ -0,0 +1,73 @@
1
+ // src/components/ui/kbd.tsx
2
+ import { splitProps, type Component, type JSX } from "solid-js";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import { cn } from "@/lib/cn";
5
+
6
+ export const kbdVariants = cva(
7
+ "pointer-events-none inline-flex select-none items-center justify-center gap-1 rounded border border-border bg-muted font-mono font-medium text-muted-foreground shadow-2xs transition-colors",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "border-border bg-muted text-muted-foreground",
12
+ outline: "border-border bg-transparent text-foreground",
13
+ },
14
+ size: {
15
+ sm: "h-4 min-w-[16px] px-1 text-[9px]",
16
+ md: "h-5 min-w-[20px] px-1.5 text-[10px]",
17
+ lg: "h-6 min-w-[24px] px-2 text-xs",
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ variant: "default",
22
+ size: "md",
23
+ },
24
+ }
25
+ );
26
+
27
+ export interface KbdProps
28
+ extends JSX.HTMLAttributes<HTMLElement>,
29
+ VariantProps<typeof kbdVariants> {
30
+ class?: string;
31
+ }
32
+
33
+ /**
34
+ * Nikala UI Kbd (Keyboard Key) Component.
35
+ */
36
+ export const Kbd: Component<KbdProps> = (props) => {
37
+ const [local, rest] = splitProps(props, [
38
+ "variant",
39
+ "size",
40
+ "class",
41
+ "children",
42
+ ]);
43
+
44
+ return (
45
+ <kbd
46
+ class={cn(
47
+ kbdVariants({ variant: local.variant, size: local.size }),
48
+ local.class
49
+ )}
50
+ {...rest}
51
+ >
52
+ {local.children}
53
+ </kbd>
54
+ );
55
+ };
56
+
57
+ /* --- Kbd Group Container --- */
58
+ export interface KbdGroupProps extends JSX.HTMLAttributes<HTMLDivElement> {
59
+ class?: string;
60
+ }
61
+
62
+ export const KbdGroup: Component<KbdGroupProps> = (props) => {
63
+ const [local, rest] = splitProps(props, ["class", "children"]);
64
+
65
+ return (
66
+ <div
67
+ class={cn("inline-flex items-center gap-1", local.class)}
68
+ {...rest}
69
+ >
70
+ {local.children}
71
+ </div>
72
+ );
73
+ };
@@ -0,0 +1,30 @@
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
+ export const labelVariants = cva(
6
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-foreground select-none"
7
+ );
8
+
9
+ export interface LabelProps
10
+ extends JSX.LabelHTMLAttributes<HTMLLabelElement>,
11
+ VariantProps<typeof labelVariants> {
12
+ class?: string;
13
+ }
14
+
15
+ /**
16
+ * Nikala UI Label component for form controls built for SolidJS with Tailwind CSS v4 styling.
17
+ */
18
+ export const Label: Component<LabelProps> = (props) => {
19
+ /* Use splitProps to preserve SolidJS signal reactivity */
20
+ const [local, rest] = splitProps(props, ["class", "children"]);
21
+
22
+ return (
23
+ <label
24
+ class={cn(labelVariants(), local.class)}
25
+ {...rest}
26
+ >
27
+ {local.children}
28
+ </label>
29
+ );
30
+ };
@@ -0,0 +1,222 @@
1
+ // src/components/ui/list.tsx
2
+ import {
3
+ splitProps,
4
+ Show,
5
+ type Component,
6
+ type JSX,
7
+ } from "solid-js";
8
+ import { Dynamic } from "solid-js/web";
9
+ import { cva, type VariantProps } from "class-variance-authority";
10
+ import { ChevronRight } from "lucide-solid";
11
+ import { cn } from "@/lib/cn";
12
+
13
+ /* --- List Container --- */
14
+ export interface ListProps extends JSX.HTMLAttributes<HTMLDivElement> {
15
+ class?: string;
16
+ }
17
+
18
+ export const List: Component<ListProps> = (props) => {
19
+ const [local, rest] = splitProps(props, ["class", "children"]);
20
+ return (
21
+ <div role="list" class={cn("flex flex-col gap-1 w-full p-1", local.class)} {...rest}>
22
+ {local.children}
23
+ </div>
24
+ );
25
+ };
26
+
27
+ /* --- List Group Container --- */
28
+ export interface ListGroupProps extends JSX.HTMLAttributes<HTMLDivElement> {
29
+ class?: string;
30
+ }
31
+
32
+ export const ListGroup: Component<ListGroupProps> = (props) => {
33
+ const [local, rest] = splitProps(props, ["class", "children"]);
34
+ return (
35
+ <div class={cn("flex flex-col gap-1 w-full my-1", local.class)} {...rest}>
36
+ {local.children}
37
+ </div>
38
+ );
39
+ };
40
+
41
+ /* --- List Group Header --- */
42
+ export interface ListHeaderProps extends JSX.HTMLAttributes<HTMLDivElement> {
43
+ title: string;
44
+ class?: string;
45
+ }
46
+
47
+ export const ListHeader: Component<ListHeaderProps> = (props) => {
48
+ const [local, rest] = splitProps(props, ["title", "class"]);
49
+ return (
50
+ <div
51
+ class={cn(
52
+ "px-2 py-1.5 text-xs font-semibold uppercase tracking-wider text-muted-foreground select-none",
53
+ local.class
54
+ )}
55
+ {...rest}
56
+ >
57
+ {local.title}
58
+ </div>
59
+ );
60
+ };
61
+
62
+ /* --- List Item Variants --- */
63
+ export const listItemVariants = cva(
64
+ "group relative flex w-full items-center justify-between gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors outline-none cursor-pointer select-none disabled:pointer-events-none disabled:opacity-50 transform duration-150 ease-in-out",
65
+ {
66
+ variants: {
67
+ hoverVariant: {
68
+ default:
69
+ "hover:bg-accent hover:text-accent-foreground data-highlighted:bg-accent data-highlighted:text-accent-foreground",
70
+ accent:
71
+ "hover:bg-accent/80 hover:text-accent-foreground data-highlighted:bg-accent/80",
72
+ primary:
73
+ "hover:bg-primary/10 hover:text-primary data-highlighted:bg-primary/10 data-highlighted:text-primary",
74
+ muted:
75
+ "hover:bg-muted hover:text-foreground data-highlighted:bg-muted",
76
+ },
77
+ size: {
78
+ sm: "px-2.5 py-1.5 text-xs",
79
+ md: "px-3 py-2 text-sm",
80
+ lg: "px-4 py-3 text-base",
81
+ },
82
+ active: {
83
+ true: "bg-accent text-accent-foreground font-semibold",
84
+ false: "",
85
+ },
86
+ },
87
+ defaultVariants: {
88
+ hoverVariant: "default",
89
+ size: "md",
90
+ active: false,
91
+ },
92
+ }
93
+ );
94
+
95
+ export interface ListItemProps
96
+ extends JSX.HTMLAttributes<HTMLDivElement>,
97
+ VariantProps<typeof listItemVariants> {
98
+ title?: string;
99
+ subtitle?: string;
100
+ avatar?: string;
101
+ avatarFallback?: string;
102
+ icon?: Component<{ class?: string }>;
103
+ shortcut?: string;
104
+ showChevron?: boolean;
105
+ href?: string;
106
+ disabled?: boolean;
107
+ active?: boolean;
108
+ class?: string;
109
+ }
110
+
111
+ /**
112
+ * Nikala UI ListItem Component.
113
+ */
114
+ export const ListItem: Component<ListItemProps> = (props) => {
115
+ const [local, rest] = splitProps(props, [
116
+ "title",
117
+ "subtitle",
118
+ "avatar",
119
+ "avatarFallback",
120
+ "icon",
121
+ "shortcut",
122
+ "showChevron",
123
+ "href",
124
+ "disabled",
125
+ "active",
126
+ "hoverVariant",
127
+ "size",
128
+ "class",
129
+ "children",
130
+ ]);
131
+
132
+ const content = () => (
133
+ <>
134
+ {/* Left Visual: Icon, Avatar, or Image */}
135
+ <div class="flex items-center gap-2.5 min-w-0">
136
+ <Show when={local.avatar}>
137
+ <img
138
+ src={local.avatar}
139
+ alt={local.title || "Avatar"}
140
+ class="w-6 h-6 rounded-full object-cover shrink-0 border border-border"
141
+ />
142
+ </Show>
143
+
144
+ <Show when={!local.avatar && local.avatarFallback}>
145
+ <span class="w-6 h-6 rounded-full bg-primary/10 text-primary text-[10px] font-bold flex items-center justify-center shrink-0 uppercase">
146
+ {local.avatarFallback}
147
+ </span>
148
+ </Show>
149
+
150
+ <Show when={!local.avatar && !local.avatarFallback && local.icon}>
151
+ <span class="shrink-0 text-muted-foreground group-hover:text-foreground transition-colors">
152
+ <Dynamic component={local.icon} class="w-4 h-4" />
153
+ </span>
154
+ </Show>
155
+
156
+ {/* Main Text Content */}
157
+ <div class="flex flex-col min-w-0 text-left">
158
+ <Show when={local.title}>
159
+ <span class="truncate font-medium leading-none">
160
+ {local.title}
161
+ </span>
162
+ </Show>
163
+ <Show when={local.subtitle}>
164
+ <span class="truncate text-xs text-muted-foreground font-normal mt-1">
165
+ {local.subtitle}
166
+ </span>
167
+ </Show>
168
+ {local.children}
169
+ </div>
170
+ </div>
171
+
172
+ {/* Right Visual: Shortcut Badge & Trailing Chevron */}
173
+ <div class="flex items-center gap-2 shrink-0">
174
+ <Show when={local.shortcut}>
175
+ <kbd class="pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded border border-border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground">
176
+ {local.shortcut}
177
+ </kbd>
178
+ </Show>
179
+
180
+ <Show when={local.showChevron}>
181
+ <ChevronRight class="w-4 h-4 text-muted-foreground group-hover:text-foreground transition-colors" />
182
+ </Show>
183
+ </div>
184
+ </>
185
+ );
186
+
187
+ const itemClasses = () =>
188
+ cn(
189
+ listItemVariants({
190
+ hoverVariant: local.hoverVariant,
191
+ size: local.size,
192
+ active: local.active,
193
+ }),
194
+ local.class
195
+ );
196
+
197
+ return (
198
+ <Show
199
+ when={local.href}
200
+ fallback={
201
+ <div
202
+ role="listitem"
203
+ class={itemClasses()}
204
+ aria-disabled={local.disabled}
205
+ {...rest}
206
+ >
207
+ {content()}
208
+ </div>
209
+ }
210
+ >
211
+ <a
212
+ role="listitem"
213
+ href={local.href}
214
+ class={itemClasses()}
215
+ aria-disabled={local.disabled}
216
+ {...(rest as JSX.AnchorHTMLAttributes<HTMLAnchorElement>)}
217
+ >
218
+ {content()}
219
+ </a>
220
+ </Show>
221
+ );
222
+ };