@nikala-ui/core 0.9.10 → 0.9.11-nightly.78f0a94

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.
@@ -0,0 +1,295 @@
1
+ import { splitProps, type JSX, type ValidComponent } from "solid-js";
2
+ import * as ComboboxPrimitive from "@kobalte/core/combobox";
3
+ import { cn } from "@/lib/cn";
4
+
5
+ export type ComboboxRootProps<Option = any, OptGroup = any, T extends ValidComponent = "div"> =
6
+ ComboboxPrimitive.ComboboxRootProps<Option, OptGroup, T> & {
7
+ class?: string;
8
+ children?: JSX.Element;
9
+ triggerMode?: "input" | "focus" | "both" | "manual";
10
+ };
11
+
12
+ /**
13
+ * Root Combobox component providing search, single/multi-selection, and group support.
14
+ * `triggerMode="focus"` or `triggerMode="both"` enables opening dropdown on input click/focus.
15
+ */
16
+ export const Combobox = <Option = any, OptGroup = any, T extends ValidComponent = "div">(
17
+ props: ComboboxRootProps<Option, OptGroup, T>
18
+ ) => {
19
+ const [local, rest] = splitProps(props as ComboboxRootProps, ["class", "children", "triggerMode"]);
20
+
21
+ return (
22
+ <ComboboxPrimitive.Root
23
+ triggerMode={local.triggerMode ?? "input"}
24
+ class={cn("relative w-full", local.class)}
25
+ {...(rest as any)}
26
+ >
27
+ {local.children}
28
+ </ComboboxPrimitive.Root>
29
+ );
30
+ };
31
+
32
+ export type ComboboxControlProps<Option = any, T extends ValidComponent = "div"> =
33
+ ComboboxPrimitive.ComboboxControlProps<Option, T> & {
34
+ class?: string;
35
+ children?: JSX.Element;
36
+ clearable?: boolean;
37
+ onClear?: () => void;
38
+ };
39
+
40
+ /**
41
+ * Input container for Combobox supporting search input, selected tags, and clear button.
42
+ */
43
+ export const ComboboxControl = <Option = any, T extends ValidComponent = "div">(
44
+ props: ComboboxControlProps<Option, T>
45
+ ) => {
46
+ const [local, rest] = splitProps(props as ComboboxControlProps, [
47
+ "class",
48
+ "children",
49
+ "clearable",
50
+ "onClear",
51
+ ]);
52
+
53
+ return (
54
+ <ComboboxPrimitive.Control
55
+ class={cn(
56
+ "flex min-h-9 w-full flex-wrap items-center justify-between rounded-md border border-input bg-muted px-3 py-1 text-sm shadow-sm transition-colors focus-within:ring-1 focus-within:ring-primary focus-within:border-primary disabled:cursor-not-allowed disabled:opacity-50 gap-1.5 text-foreground",
57
+ local.class
58
+ )}
59
+ {...(rest as any)}
60
+ >
61
+ <div class="flex flex-wrap items-center gap-1.5 flex-1 min-w-0">
62
+ {local.children}
63
+ </div>
64
+
65
+ <div class="flex items-center gap-1 shrink-0 self-center">
66
+ {local.clearable && (
67
+ <button
68
+ type="button"
69
+ tabIndex={-1}
70
+ onClick={(e) => {
71
+ e.stopPropagation();
72
+ if (local.onClear) local.onClear();
73
+ }}
74
+ class="rounded-sm p-0.5 opacity-60 hover:opacity-100 hover:bg-accent text-foreground transition-opacity focus:outline-none cursor-pointer"
75
+ aria-label="Clear selection"
76
+ >
77
+ <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
78
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
79
+ </svg>
80
+ </button>
81
+ )}
82
+ <ComboboxTrigger />
83
+ </div>
84
+ </ComboboxPrimitive.Control>
85
+ );
86
+ };
87
+
88
+ export type ComboboxInputProps<T extends ValidComponent = "input"> =
89
+ ComboboxPrimitive.ComboboxInputProps<T> & {
90
+ class?: string;
91
+ openOnFocus?: boolean;
92
+ };
93
+
94
+ /**
95
+ * Search input field embedded within ComboboxControl.
96
+ * Supports `openOnFocus` to automatically trigger the dropdown when focused or clicked.
97
+ */
98
+ export const ComboboxInput = <T extends ValidComponent = "input">(
99
+ props: ComboboxInputProps<T>
100
+ ) => {
101
+ const [local, rest] = splitProps(props as ComboboxInputProps, ["class", "openOnFocus", "onFocus", "onClick"]);
102
+
103
+ return (
104
+ <ComboboxPrimitive.Input
105
+ class={cn(
106
+ "flex-1 bg-transparent py-1 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed text-foreground min-w-16",
107
+ local.class
108
+ )}
109
+ onFocus={(e: FocusEvent) => {
110
+ if (typeof local.onFocus === "function") local.onFocus(e as any);
111
+ }}
112
+ onClick={(e: MouseEvent) => {
113
+ if (typeof local.onClick === "function") local.onClick(e as any);
114
+ }}
115
+ {...(rest as any)}
116
+ />
117
+ );
118
+ };
119
+
120
+ export type ComboboxTriggerProps<T extends ValidComponent = "button"> =
121
+ ComboboxPrimitive.ComboboxTriggerProps<T> & {
122
+ class?: string;
123
+ };
124
+
125
+ /**
126
+ * Dropdown chevron trigger icon.
127
+ */
128
+ export const ComboboxTrigger = <T extends ValidComponent = "button">(
129
+ props: ComboboxTriggerProps<T>
130
+ ) => {
131
+ const [local, rest] = splitProps(props as ComboboxTriggerProps, ["class"]);
132
+
133
+ return (
134
+ <ComboboxPrimitive.Trigger
135
+ class={cn(
136
+ "flex h-4 w-4 items-center justify-center opacity-50 hover:opacity-100 transition-opacity focus:outline-none cursor-pointer",
137
+ local.class
138
+ )}
139
+ {...(rest as any)}
140
+ >
141
+ <ComboboxPrimitive.Icon
142
+ as="svg"
143
+ class="h-4 w-4 stroke-current stroke-2 fill-none"
144
+ viewBox="0 0 24 24"
145
+ >
146
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
147
+ </ComboboxPrimitive.Icon>
148
+ </ComboboxPrimitive.Trigger>
149
+ );
150
+ };
151
+
152
+ export type ComboboxTokenProps<Option = any> = {
153
+ class?: string;
154
+ children?: JSX.Element;
155
+ item?: Option;
156
+ onRemove?: () => void;
157
+ };
158
+
159
+ /**
160
+ * Selected item tag token pill rendered in multi-select mode.
161
+ */
162
+ export const ComboboxToken = <Option = any>(props: ComboboxTokenProps<Option>) => {
163
+ const [local, rest] = splitProps(props, ["class", "children", "onRemove"]);
164
+
165
+ return (
166
+ <span
167
+ class={cn(
168
+ "inline-flex items-center gap-1.5 rounded-md bg-accent px-2 py-0.5 text-xs font-medium text-accent-foreground border border-border shadow-2xs animate-in fade-in-50",
169
+ local.class
170
+ )}
171
+ {...rest}
172
+ >
173
+ <span class="truncate">{local.children}</span>
174
+ <button
175
+ type="button"
176
+ tabIndex={-1}
177
+ onClick={(e) => {
178
+ e.stopPropagation();
179
+ if (local.onRemove) local.onRemove();
180
+ }}
181
+ class="rounded-xs p-0.5 hover:bg-muted-foreground/20 text-muted-foreground hover:text-foreground transition-colors focus:outline-none cursor-pointer"
182
+ aria-label="Remove tag"
183
+ >
184
+ <svg class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
185
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
186
+ </svg>
187
+ </button>
188
+ </span>
189
+ );
190
+ };
191
+
192
+ export type ComboboxContentProps<T extends ValidComponent = "div"> =
193
+ ComboboxPrimitive.ComboboxContentProps<T> & {
194
+ class?: string;
195
+ };
196
+
197
+ /**
198
+ * Portaled popover content listbox container.
199
+ */
200
+ export const ComboboxContent = <T extends ValidComponent = "div">(
201
+ props: ComboboxContentProps<T>
202
+ ) => {
203
+ const [local, rest] = splitProps(props as ComboboxContentProps, ["class"]);
204
+
205
+ return (
206
+ <ComboboxPrimitive.Portal>
207
+ <ComboboxPrimitive.Content
208
+ class={cn(
209
+ "relative z-50 min-w-8rem overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md animate-in fade-in-80 data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95",
210
+ local.class
211
+ )}
212
+ {...(rest as any)}
213
+ >
214
+ <ComboboxPrimitive.Listbox class="max-h-60 overflow-y-auto p-1 outline-none" />
215
+ </ComboboxPrimitive.Content>
216
+ </ComboboxPrimitive.Portal>
217
+ );
218
+ };
219
+
220
+ export type ComboboxItemProps<T extends ValidComponent = "li"> =
221
+ ComboboxPrimitive.ComboboxItemProps<T> & {
222
+ class?: string;
223
+ children?: JSX.Element;
224
+ };
225
+
226
+ /**
227
+ * Individual option item inside listbox supporting custom avatars, icons, and titles.
228
+ */
229
+ export const ComboboxItem = <T extends ValidComponent = "li">(
230
+ props: ComboboxItemProps<T>
231
+ ) => {
232
+ const [local, rest] = splitProps(props as ComboboxItemProps, ["class", "children"]);
233
+
234
+ return (
235
+ <ComboboxPrimitive.Item
236
+ class={cn(
237
+ "relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:bg-accent data-highlighted:text-accent-foreground text-popover-foreground transition-colors",
238
+ local.class
239
+ )}
240
+ {...rest}
241
+ >
242
+ <ComboboxPrimitive.ItemIndicator class="absolute left-2 flex h-4 w-4 items-center justify-center text-primary">
243
+ <svg class="h-4 w-4 fill-none stroke-current stroke-2" viewBox="0 0 24 24">
244
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
245
+ </svg>
246
+ </ComboboxPrimitive.ItemIndicator>
247
+ <ComboboxPrimitive.ItemLabel class="flex items-center gap-2 w-full truncate">
248
+ {local.children}
249
+ </ComboboxPrimitive.ItemLabel>
250
+ </ComboboxPrimitive.Item>
251
+ );
252
+ };
253
+
254
+ export type ComboboxGroupProps<T extends ValidComponent = "li"> =
255
+ ComboboxPrimitive.ComboboxSectionProps<T> & {
256
+ class?: string;
257
+ children?: JSX.Element;
258
+ label?: JSX.Element;
259
+ };
260
+
261
+ /**
262
+ * Group container for organizing related options.
263
+ */
264
+ export const ComboboxGroup = <T extends ValidComponent = "li">(
265
+ props: ComboboxGroupProps<T>
266
+ ) => {
267
+ const [local, rest] = splitProps(props as ComboboxGroupProps, ["class", "children", "label"]);
268
+
269
+ return (
270
+ <ComboboxPrimitive.Section class={cn("px-1 py-1", local.class)} {...(rest as any)}>
271
+ {local.label && (
272
+ <span class="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider block">
273
+ {local.label}
274
+ </span>
275
+ )}
276
+ {local.children}
277
+ </ComboboxPrimitive.Section>
278
+ );
279
+ };
280
+
281
+ /**
282
+ * Empty state notice when no matching search items exist.
283
+ */
284
+ export const ComboboxEmpty = (props: { class?: string; children?: JSX.Element }) => {
285
+ return (
286
+ <div class={cn("py-6 text-center text-sm text-muted-foreground", props.class)}>
287
+ {props.children || "No matching items found."}
288
+ </div>
289
+ );
290
+ };
291
+
292
+ /**
293
+ * Hidden native select element for form integrations.
294
+ */
295
+ export const ComboboxHiddenSelect = ComboboxPrimitive.HiddenSelect;
@@ -21,6 +21,10 @@ import { List, ListGroup, ListHeader, ListItem, type ListItemProps } from "../ui
21
21
  interface CommandContextValue {
22
22
  search: Accessor<string>;
23
23
  setSearch: (value: string) => void;
24
+ activeIndex: Accessor<number>;
25
+ setActiveIndex: (fn: (prev: number) => number) => void;
26
+ registerItemIndex: (element: HTMLElement) => number;
27
+ onItemSelect: (index: number, action?: () => void) => void;
24
28
  }
25
29
 
26
30
  const CommandContext = createContext<CommandContextValue>();
@@ -34,24 +38,96 @@ export const useCommand = () => {
34
38
  };
35
39
 
36
40
  /* --- Root Command Container --- */
37
- export interface CommandProps extends JSX.HTMLAttributes<HTMLDivElement> {
41
+ export interface CommandProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>, "children"> {
38
42
  class?: string;
43
+ children?: JSX.Element | ((ctx: CommandContextValue) => JSX.Element);
39
44
  }
40
45
 
41
46
  export const Command: Component<CommandProps> = (props) => {
42
47
  const [local, rest] = splitProps(props, ["class", "children"]);
43
48
  const [search, setSearch] = createSignal("");
49
+ const [activeIndex, setActiveIndex] = createSignal(0);
50
+
51
+ let containerRef: HTMLDivElement | undefined;
52
+ let itemsList: HTMLElement[] = [];
53
+
54
+ const registerItemIndex = (element: HTMLElement) => {
55
+ if (!itemsList.includes(element)) {
56
+ itemsList.push(element);
57
+ }
58
+ return itemsList.indexOf(element);
59
+ };
60
+
61
+ const onItemSelect = (index: number, action?: () => void) => {
62
+ setActiveIndex(index);
63
+ if (action) action();
64
+ };
65
+
66
+ const getVisibleItems = () => {
67
+ if (!containerRef) return [];
68
+ return Array.from(containerRef.querySelectorAll<HTMLElement>("[data-command-item]:not(.hidden)"));
69
+ };
70
+
71
+ const updateActiveAttribute = (index: number) => {
72
+ const visible = getVisibleItems();
73
+ visible.forEach((el, idx) => {
74
+ if (idx === index) {
75
+ el.setAttribute("aria-selected", "true");
76
+ el.scrollIntoView({ block: "nearest" });
77
+ } else {
78
+ el.removeAttribute("aria-selected");
79
+ }
80
+ });
81
+ };
82
+
83
+ const handleKeyDown = (e: KeyboardEvent) => {
84
+ const visible = getVisibleItems();
85
+ if (visible.length === 0) return;
86
+
87
+ if (e.key === "ArrowDown") {
88
+ e.preventDefault();
89
+ const next = (activeIndex() + 1) % visible.length;
90
+ setActiveIndex(next);
91
+ updateActiveAttribute(next);
92
+ } else if (e.key === "ArrowUp") {
93
+ e.preventDefault();
94
+ const prev = (activeIndex() - 1 + visible.length) % visible.length;
95
+ setActiveIndex(prev);
96
+ updateActiveAttribute(prev);
97
+ } else if (e.key === "Enter") {
98
+ e.preventDefault();
99
+ const current = visible[activeIndex()];
100
+ if (current) {
101
+ current.click();
102
+ }
103
+ }
104
+ };
105
+
106
+ const ctx: CommandContextValue = {
107
+ search,
108
+ setSearch: (val) => {
109
+ setSearch(val);
110
+ setActiveIndex(0);
111
+ },
112
+ activeIndex,
113
+ setActiveIndex: (fn) => setActiveIndex(fn),
114
+ registerItemIndex,
115
+ onItemSelect,
116
+ };
44
117
 
45
118
  return (
46
- <CommandContext.Provider value={{ search, setSearch }}>
119
+ <CommandContext.Provider value={ctx}>
47
120
  <div
121
+ ref={containerRef}
122
+ onKeyDown={handleKeyDown}
48
123
  class={cn(
49
- "flex flex-col w-full h-full rounded-lg bg-popover text-popover-foreground overflow-hidden border border-border shadow-md",
124
+ "flex flex-col w-full h-full rounded-lg bg-popover text-popover-foreground overflow-hidden border border-border shadow-md outline-none",
50
125
  local.class
51
126
  )}
127
+ tabIndex={0}
52
128
  {...rest}
53
129
  >
54
- {local.children}
130
+ {typeof local.children === "function" ? (local.children as any)(ctx) : local.children}
55
131
  </div>
56
132
  </CommandContext.Provider>
57
133
  );
@@ -62,7 +138,7 @@ export interface CommandDialogProps {
62
138
  open?: boolean;
63
139
  onOpenChange?: (open: boolean) => void;
64
140
  enableHotkey?: boolean;
65
- children?: JSX.Element;
141
+ children?: JSX.Element | ((ctx: CommandContextValue) => JSX.Element);
66
142
  class?: string;
67
143
  }
68
144
 
@@ -120,6 +196,7 @@ export const CommandInput: Component<CommandInputProps> = (props) => {
120
196
  </InputGroupAddon>
121
197
 
122
198
  <InputGroupInput
199
+ ref={(el) => setTimeout(() => el.focus(), 50)}
123
200
  value={search()}
124
201
  onInput={(e) => {
125
202
  setSearch(e.currentTarget.value);
@@ -200,6 +277,7 @@ export interface CommandItemProps extends ListItemProps {
200
277
  }
201
278
 
202
279
  export const CommandItem: Component<CommandItemProps> = (props) => {
280
+ let itemRef: HTMLDivElement | undefined;
203
281
  const [local, rest] = splitProps(props, [
204
282
  "title",
205
283
  "subtitle",
@@ -208,7 +286,7 @@ export const CommandItem: Component<CommandItemProps> = (props) => {
208
286
  "onClick",
209
287
  "class",
210
288
  ]);
211
- const { search } = useCommand();
289
+ const { search, activeIndex } = useCommand();
212
290
 
213
291
  /* Auto fuzzy-filter item based on search query */
214
292
  const matchesSearch = () => {
@@ -236,10 +314,15 @@ export const CommandItem: Component<CommandItemProps> = (props) => {
236
314
  return (
237
315
  <Show when={matchesSearch()}>
238
316
  <ListItem
317
+ ref={itemRef}
318
+ data-command-item="true"
239
319
  title={local.title}
240
320
  subtitle={local.subtitle}
241
321
  onClick={handleClick}
242
- class={local.class}
322
+ class={cn(
323
+ "aria-selected:bg-accent aria-selected:text-accent-foreground cursor-pointer transition-colors hover:bg-accent hover:text-accent-foreground",
324
+ local.class
325
+ )}
243
326
  {...rest}
244
327
  />
245
328
  </Show>
@@ -0,0 +1,105 @@
1
+ import { splitProps, type JSX, type ValidComponent } from "solid-js";
2
+ import * as HoverCardPrimitive from "@kobalte/core/hover-card";
3
+ import { cn } from "@/lib/cn";
4
+
5
+ export type HoverCardProps = HoverCardPrimitive.HoverCardRootProps;
6
+
7
+ /**
8
+ * Root HoverCard component built on Kobalte primitives.
9
+ * Controls hover open/close delays (default: 200ms / 150ms).
10
+ */
11
+ export const HoverCard = (props: HoverCardProps) => {
12
+ return (
13
+ <HoverCardPrimitive.Root
14
+ openDelay={200}
15
+ closeDelay={150}
16
+ {...props}
17
+ />
18
+ );
19
+ };
20
+
21
+ export type HoverCardTriggerProps<T extends ValidComponent = "a"> =
22
+ HoverCardPrimitive.HoverCardTriggerProps<T> &
23
+ JSX.AnchorHTMLAttributes<HTMLAnchorElement> & {
24
+ class?: string;
25
+ children?: JSX.Element;
26
+ };
27
+
28
+ /**
29
+ * HoverCard trigger handle (link, avatar, or text).
30
+ */
31
+ export const HoverCardTrigger = <T extends ValidComponent = "a">(
32
+ props: HoverCardTriggerProps<T>
33
+ ) => {
34
+ const [local, rest] = splitProps(props as HoverCardTriggerProps, [
35
+ "class",
36
+ "children",
37
+ ]);
38
+
39
+ return (
40
+ <HoverCardPrimitive.Trigger
41
+ class={cn(
42
+ "inline-flex items-center text-sm font-medium text-foreground underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2",
43
+ local.class
44
+ )}
45
+ {...(rest as any)}
46
+ >
47
+ {local.children}
48
+ </HoverCardPrimitive.Trigger>
49
+ );
50
+ };
51
+
52
+ export type HoverCardContentProps<T extends ValidComponent = "div"> =
53
+ HoverCardPrimitive.HoverCardContentProps<T> & {
54
+ class?: string;
55
+ children?: JSX.Element;
56
+ };
57
+
58
+ /**
59
+ * Floating card preview container rendered in a portal layer.
60
+ * Includes offset spacing from trigger and support for optional HoverCardArrow.
61
+ */
62
+ export const HoverCardContent = <T extends ValidComponent = "div">(
63
+ props: HoverCardContentProps<T>
64
+ ) => {
65
+ const [local, rest] = splitProps(props as HoverCardContentProps, [
66
+ "class",
67
+ "children",
68
+ ]);
69
+
70
+ return (
71
+ <HoverCardPrimitive.Portal>
72
+ <HoverCardPrimitive.Content
73
+ sideOffset={8}
74
+ class={cn(
75
+ "z-50 w-80 rounded-lg border border-border bg-popover p-4 text-popover-foreground shadow-xl outline-none transition-all data-expanded:animate-in data-closed:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-closed:zoom-out-95 data-expanded:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
76
+ local.class
77
+ )}
78
+ {...(rest as any)}
79
+ >
80
+ {local.children}
81
+ </HoverCardPrimitive.Content>
82
+ </HoverCardPrimitive.Portal>
83
+ );
84
+ };
85
+
86
+ export type HoverCardArrowProps<T extends ValidComponent = "div"> =
87
+ HoverCardPrimitive.HoverCardArrowProps<T> & {
88
+ class?: string;
89
+ };
90
+
91
+ /**
92
+ * Optional arrow pointer pointing to the trigger.
93
+ */
94
+ export const HoverCardArrow = <T extends ValidComponent = "div">(
95
+ props: HoverCardArrowProps<T>
96
+ ) => {
97
+ const [local, rest] = splitProps(props as HoverCardArrowProps, ["class"]);
98
+
99
+ return (
100
+ <HoverCardPrimitive.Arrow
101
+ class={cn("fill-popover stroke-border stroke-1", local.class)}
102
+ {...(rest as any)}
103
+ />
104
+ );
105
+ };