@nikala-ui/core 0.10.1 → 0.11.0-nightly.b01fa01

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 (106) hide show
  1. package/package.json +1 -1
  2. package/registry/banner.json +1 -1
  3. package/registry/bubble.json +18 -0
  4. package/registry/button-group.json +20 -0
  5. package/registry/button.json +1 -1
  6. package/registry/checkbox.json +4 -1
  7. package/registry/combobox.json +1 -1
  8. package/registry/command.json +3 -2
  9. package/registry/context-menu.json +1 -1
  10. package/registry/create-app-updater.json +13 -0
  11. package/registry/create-chat-scroll.json +13 -0
  12. package/registry/create-document-tabs.json +13 -0
  13. package/registry/create-drop-zone.json +13 -0
  14. package/registry/create-global-shortcut.json +13 -0
  15. package/registry/create-pagination.json +13 -0
  16. package/registry/create-tauri-window.json +13 -0
  17. package/registry/dialog.json +1 -1
  18. package/registry/dropdown-menu.json +3 -2
  19. package/registry/dropzone.json +21 -0
  20. package/registry/field.json +1 -1
  21. package/registry/footer.json +18 -0
  22. package/registry/forgot-password-01.json +28 -0
  23. package/registry/form-message.json +3 -2
  24. package/registry/hero-01.json +22 -0
  25. package/registry/icon-button.json +1 -1
  26. package/registry/index.json +432 -5
  27. package/registry/login-01.json +28 -0
  28. package/registry/marker.json +17 -0
  29. package/registry/marquee.json +17 -0
  30. package/registry/message.json +17 -0
  31. package/registry/navbar.json +19 -0
  32. package/registry/navigation-menu.json +22 -0
  33. package/registry/number-input.json +1 -1
  34. package/registry/otp-verification-01.json +26 -0
  35. package/registry/pagination.json +22 -0
  36. package/registry/popover.json +4 -1
  37. package/registry/rating.json +19 -0
  38. package/registry/register-01.json +31 -0
  39. package/registry/resizable.json +1 -1
  40. package/registry/review-card.json +23 -0
  41. package/registry/scroll-area.json +1 -1
  42. package/registry/select.json +3 -2
  43. package/registry/sheet.json +1 -1
  44. package/registry/sidebar.json +24 -0
  45. package/registry/spinner.json +1 -1
  46. package/registry/stat.json +19 -0
  47. package/registry/status.json +1 -1
  48. package/registry/switch.json +4 -1
  49. package/registry/table.json +17 -0
  50. package/registry/tabs.json +4 -1
  51. package/registry/theme-manager.json +4 -3
  52. package/registry/timeline.json +18 -0
  53. package/registry/titlebar-tabs.json +24 -0
  54. package/registry/titlebar.json +26 -0
  55. package/registry/toggle-group.json +21 -0
  56. package/registry/updater-modal.json +26 -0
  57. package/src/registry/blocks/forgot-password-01.tsx +141 -0
  58. package/src/registry/blocks/hero-01.tsx +28 -0
  59. package/src/registry/blocks/login-01.tsx +231 -0
  60. package/src/registry/blocks/otp-verification-01.tsx +170 -0
  61. package/src/registry/blocks/register-01.tsx +318 -0
  62. package/src/registry/components/ui/banner.tsx +0 -2
  63. package/src/registry/components/ui/bubble.tsx +142 -0
  64. package/src/registry/components/ui/button-group.tsx +42 -0
  65. package/src/registry/components/ui/button.tsx +1 -1
  66. package/src/registry/components/ui/checkbox.tsx +1 -1
  67. package/src/registry/components/ui/combobox.tsx +83 -18
  68. package/src/registry/components/ui/command.tsx +101 -105
  69. package/src/registry/components/ui/context-menu.tsx +67 -22
  70. package/src/registry/components/ui/dialog.tsx +42 -32
  71. package/src/registry/components/ui/dropdown-menu.tsx +69 -31
  72. package/src/registry/components/ui/dropzone.tsx +189 -0
  73. package/src/registry/components/ui/field.tsx +7 -3
  74. package/src/registry/components/ui/footer.tsx +247 -0
  75. package/src/registry/components/ui/form-message.tsx +2 -2
  76. package/src/registry/components/ui/icon-button.tsx +1 -1
  77. package/src/registry/components/ui/marker.tsx +102 -0
  78. package/src/registry/components/ui/marquee.tsx +118 -0
  79. package/src/registry/components/ui/message.tsx +168 -0
  80. package/src/registry/components/ui/navbar.tsx +375 -0
  81. package/src/registry/components/ui/navigation-menu.tsx +358 -0
  82. package/src/registry/components/ui/number-input.tsx +3 -3
  83. package/src/registry/components/ui/pagination.tsx +258 -0
  84. package/src/registry/components/ui/popover.tsx +1 -1
  85. package/src/registry/components/ui/rating.tsx +185 -0
  86. package/src/registry/components/ui/resizable.tsx +3 -1
  87. package/src/registry/components/ui/review-card.tsx +195 -0
  88. package/src/registry/components/ui/scroll-area.tsx +13 -3
  89. package/src/registry/components/ui/select.tsx +16 -19
  90. package/src/registry/components/ui/sheet.tsx +58 -53
  91. package/src/registry/components/ui/sidebar.tsx +692 -0
  92. package/src/registry/components/ui/spinner.tsx +1 -1
  93. package/src/registry/components/ui/stat.tsx +245 -0
  94. package/src/registry/components/ui/status.tsx +7 -5
  95. package/src/registry/components/ui/switch.tsx +1 -1
  96. package/src/registry/components/ui/table.tsx +160 -0
  97. package/src/registry/components/ui/tabs.tsx +1 -1
  98. package/src/registry/components/ui/theme-toggle.tsx +80 -79
  99. package/src/registry/components/ui/timeline.tsx +350 -0
  100. package/src/registry/components/ui/titlebar-tabs.tsx +412 -0
  101. package/src/registry/components/ui/titlebar.tsx +399 -0
  102. package/src/registry/components/ui/toggle-group.tsx +203 -0
  103. package/src/registry/components/ui/updater-modal.tsx +254 -0
  104. package/src/registry/index.ts +3 -3
  105. package/src/registry/metadata.ts +184 -5
  106. package/src/registry/providers/theme-transitions.ts +81 -50
@@ -1,44 +1,88 @@
1
1
  import { splitProps, type JSX, type ValidComponent } from "solid-js";
2
2
  import { Check, ChevronDown, X } from "lucide-solid";
3
- import { ScrollArea } from "./scroll-area";
3
+ import { ScrollArea } from "@/components/ui/scroll-area";
4
4
  import * as ComboboxPrimitive from "@kobalte/core/combobox";
5
5
  import { cn } from "@/lib/cn";
6
6
 
7
7
  export type ComboboxRootProps<Option = any, OptGroup = any, T extends ValidComponent = "div"> =
8
- ComboboxPrimitive.ComboboxRootProps<Option, OptGroup, T>;
8
+ ComboboxPrimitive.ComboboxRootProps<Option, OptGroup, T> & {
9
+ class?: string;
10
+ children?: JSX.Element;
11
+ triggerMode?: "input" | "focus" | "both" | "manual";
12
+ };
9
13
 
10
14
  /**
11
- * Root Combobox primitive component wrapper.
15
+ * Root Combobox component providing search, single/multi-selection, and group support.
16
+ * `triggerMode="focus"` or `triggerMode="both"` enables opening dropdown on input click/focus.
12
17
  */
13
18
  export const Combobox = <Option = any, OptGroup = any, T extends ValidComponent = "div">(
14
19
  props: ComboboxRootProps<Option, OptGroup, T>
15
20
  ) => {
16
- return <ComboboxPrimitive.Root {...props} />;
21
+ const [local, rest] = splitProps(props as ComboboxRootProps, ["class", "children", "triggerMode"]);
22
+
23
+ return (
24
+ <ComboboxPrimitive.Root
25
+ triggerMode={local.triggerMode ?? "input"}
26
+ class={cn("relative w-full", local.class)}
27
+ {...(rest as any)}
28
+ >
29
+ {local.children}
30
+ </ComboboxPrimitive.Root>
31
+ );
17
32
  };
18
33
 
19
34
  export type ComboboxControlProps<Option = any, T extends ValidComponent = "div"> =
20
35
  ComboboxPrimitive.ComboboxControlProps<Option, T> & {
21
36
  class?: string;
22
37
  children?: JSX.Element;
38
+ clearable?: boolean;
39
+ onClear?: () => void;
23
40
  };
24
41
 
25
42
  /**
26
- * Input container box supporting single or multi-select tokens.
43
+ * Input container for Combobox supporting search input, selected tags, and clear button.
27
44
  */
28
45
  export const ComboboxControl = <Option = any, T extends ValidComponent = "div">(
29
46
  props: ComboboxControlProps<Option, T>
30
47
  ) => {
31
- const [local, rest] = splitProps(props as ComboboxControlProps, ["class", "children"]);
48
+ const [local, rest] = splitProps(props as ComboboxControlProps, [
49
+ "class",
50
+ "children",
51
+ "clearable",
52
+ "onClear",
53
+ ]);
32
54
 
33
55
  return (
34
56
  <ComboboxPrimitive.Control
35
57
  class={cn(
36
- "flex min-h-9 w-full flex-wrap items-center gap-1.5 rounded-md border border-input bg-muted px-3 py-1.5 text-sm shadow-2xs ring-offset-background focus-within:ring-1 focus-within:ring-primary focus-within:border-primary disabled:cursor-not-allowed disabled:opacity-50 text-foreground cursor-text transition-colors",
58
+ "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",
37
59
  local.class
38
60
  )}
39
61
  {...(rest as any)}
40
62
  >
41
- {local.children}
63
+ <div class="flex flex-wrap items-center gap-1.5 flex-1 min-w-0">
64
+ {local.children}
65
+ </div>
66
+
67
+ <div class="flex items-center gap-1 shrink-0 self-center">
68
+ {local.clearable && (
69
+ <button
70
+ type="button"
71
+ tabIndex={-1}
72
+ onClick={(e) => {
73
+ e.stopPropagation();
74
+ if (local.onClear) local.onClear();
75
+ }}
76
+ class="rounded-sm p-0.5 opacity-60 hover:opacity-100 hover:bg-accent text-foreground transition-opacity focus:outline-none cursor-pointer"
77
+ aria-label="Clear selection"
78
+ >
79
+ <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
80
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
81
+ </svg>
82
+ </button>
83
+ )}
84
+ <ComboboxTrigger />
85
+ </div>
42
86
  </ComboboxPrimitive.Control>
43
87
  );
44
88
  };
@@ -50,7 +94,8 @@ export type ComboboxInputProps<T extends ValidComponent = "input"> =
50
94
  };
51
95
 
52
96
  /**
53
- * Filter search input field.
97
+ * Search input field embedded within ComboboxControl.
98
+ * Supports `openOnFocus` to automatically trigger the dropdown when focused or clicked.
54
99
  */
55
100
  export const ComboboxInput = <T extends ValidComponent = "input">(
56
101
  props: ComboboxInputProps<T>
@@ -135,10 +180,12 @@ export const ComboboxToken = <Option = any>(props: ComboboxTokenProps<Option>) =
135
180
  e.stopPropagation();
136
181
  if (local.onRemove) local.onRemove();
137
182
  }}
138
- class="rounded-xs opacity-70 hover:opacity-100 focus:outline-none cursor-pointer text-muted-foreground hover:text-foreground"
183
+ class="rounded-xs p-0.5 hover:bg-muted-foreground/20 text-muted-foreground hover:text-foreground transition-colors focus:outline-none cursor-pointer"
184
+ aria-label="Remove tag"
139
185
  >
140
- <X class="h-3 w-3" />
141
- <span class="sr-only">Remove</span>
186
+ <svg class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
187
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
188
+ </svg>
142
189
  </button>
143
190
  </span>
144
191
  );
@@ -191,17 +238,19 @@ export const ComboboxItem = <T extends ValidComponent = "li">(
191
238
  return (
192
239
  <ComboboxPrimitive.Item
193
240
  class={cn(
194
- "relative flex w-full cursor-pointer select-none items-center justify-between rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50 text-foreground transition-colors",
241
+ "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",
195
242
  local.class
196
243
  )}
197
- {...(rest as any)}
244
+ {...rest}
198
245
  >
199
- <ComboboxPrimitive.ItemLabel class="flex-1 truncate">
246
+ <ComboboxPrimitive.ItemIndicator class="absolute left-2 flex h-4 w-4 items-center justify-center text-primary">
247
+ <svg class="h-4 w-4 fill-none stroke-current stroke-2" viewBox="0 0 24 24">
248
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
249
+ </svg>
250
+ </ComboboxPrimitive.ItemIndicator>
251
+ <ComboboxPrimitive.ItemLabel class="flex items-center gap-2 w-full truncate">
200
252
  {local.children}
201
253
  </ComboboxPrimitive.ItemLabel>
202
- <ComboboxPrimitive.ItemIndicator class="ml-2 flex h-4 w-4 items-center justify-center text-primary">
203
- <Check class="h-4 w-4 stroke-2" />
204
- </ComboboxPrimitive.ItemIndicator>
205
254
  </ComboboxPrimitive.Item>
206
255
  );
207
256
  };
@@ -232,3 +281,19 @@ export const ComboboxGroup = <T extends ValidComponent = "li">(
232
281
  </ComboboxPrimitive.Section>
233
282
  );
234
283
  };
284
+
285
+ /**
286
+ * Empty state notice when no matching search items exist.
287
+ */
288
+ export const ComboboxEmpty = (props: { class?: string; children?: JSX.Element }) => {
289
+ return (
290
+ <div class={cn("py-6 text-center text-sm text-muted-foreground", props.class)}>
291
+ {props.children || "No matching items found."}
292
+ </div>
293
+ );
294
+ };
295
+
296
+ /**
297
+ * Hidden native select element for form integrations.
298
+ */
299
+ export const ComboboxHiddenSelect = ComboboxPrimitive.HiddenSelect;
@@ -9,14 +9,14 @@ import {
9
9
  type JSX,
10
10
  type Accessor,
11
11
  } from "solid-js";
12
- import { createKeybindings } from "@nikala-ui/hooks";
12
+ import { createKeybindings } from "@/hooks/create-keybindings";
13
+ import { Dialog } from "@kobalte/core/dialog";
13
14
  import { Search, ArrowUp, ArrowDown, CornerDownLeft } from "lucide-solid";
14
15
  import { cn } from "@/lib/cn";
15
- import { Kbd, KbdGroup } from "./kbd";
16
- import { InputGroup, InputGroupInput, InputGroupAddon } from "./input-group";
17
- import { List, ListGroup, ListHeader, ListItem, type ListItemProps } from "./list";
18
- import { Dialog, DialogOverlay, DialogContent } from "./dialog";
19
- import { ScrollArea } from "./scroll-area";
16
+ import { Kbd, KbdGroup } from "../ui/kbd";
17
+ import { InputGroup, InputGroupInput, InputGroupAddon } from "../ui/input-group";
18
+ import { List, ListGroup, ListHeader, ListItem, type ListItemProps } from "../ui/list";
19
+ import { ScrollArea } from "@/components/ui/scroll-area";
20
20
 
21
21
  /* --- Command Context State --- */
22
22
  interface CommandContextValue {
@@ -24,8 +24,8 @@ interface CommandContextValue {
24
24
  setSearch: (value: string) => void;
25
25
  activeIndex: Accessor<number>;
26
26
  setActiveIndex: (fn: (prev: number) => number) => void;
27
- registerItemIndex: () => number;
28
- onItemSelect: (fn: () => void) => void;
27
+ registerItemIndex: (element: HTMLElement) => number;
28
+ onItemSelect: (index: number, action?: () => void) => void;
29
29
  }
30
30
 
31
31
  const CommandContext = createContext<CommandContextValue>();
@@ -33,12 +33,12 @@ const CommandContext = createContext<CommandContextValue>();
33
33
  export const useCommand = () => {
34
34
  const ctx = useContext(CommandContext);
35
35
  if (!ctx) {
36
- throw new Error("useCommand must be used within a <Command /> root component.");
36
+ throw new Error("useCommand must be used within a <Command />");
37
37
  }
38
38
  return ctx;
39
39
  };
40
40
 
41
- /* --- Command Root --- */
41
+ /* --- Root Command Container --- */
42
42
  export interface CommandProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>, "children"> {
43
43
  class?: string;
44
44
  children?: JSX.Element | ((ctx: CommandContextValue) => JSX.Element);
@@ -48,32 +48,56 @@ export const Command: Component<CommandProps> = (props) => {
48
48
  const [local, rest] = splitProps(props, ["class", "children"]);
49
49
  const [search, setSearch] = createSignal("");
50
50
  const [activeIndex, setActiveIndex] = createSignal(0);
51
- let itemCounter = 0;
52
- const itemCallbacks: Record<number, () => void> = {};
51
+
53
52
  let containerRef: HTMLDivElement | undefined;
53
+ let itemsList: HTMLElement[] = [];
54
+
55
+ const registerItemIndex = (element: HTMLElement) => {
56
+ if (!itemsList.includes(element)) {
57
+ itemsList.push(element);
58
+ }
59
+ return itemsList.indexOf(element);
60
+ };
54
61
 
55
- const registerItemIndex = () => {
56
- const idx = itemCounter;
57
- itemCounter += 1;
58
- return idx;
62
+ const onItemSelect = (index: number, action?: () => void) => {
63
+ setActiveIndex(index);
64
+ if (action) action();
59
65
  };
60
66
 
61
- const onItemSelect = (fn: () => void) => {
62
- itemCallbacks[activeIndex()] = fn;
67
+ const getVisibleItems = () => {
68
+ if (!containerRef) return [];
69
+ return Array.from(containerRef.querySelectorAll<HTMLElement>("[data-command-item]:not(.hidden)"));
70
+ };
71
+
72
+ const updateActiveAttribute = (index: number) => {
73
+ const visible = getVisibleItems();
74
+ visible.forEach((el, idx) => {
75
+ if (idx === index) {
76
+ el.setAttribute("aria-selected", "true");
77
+ el.scrollIntoView({ block: "nearest" });
78
+ } else {
79
+ el.removeAttribute("aria-selected");
80
+ }
81
+ });
63
82
  };
64
83
 
65
84
  const handleKeyDown = (e: KeyboardEvent) => {
85
+ const visible = getVisibleItems();
86
+ if (visible.length === 0) return;
87
+
66
88
  if (e.key === "ArrowDown") {
67
89
  e.preventDefault();
68
- setActiveIndex((prev) => Math.min(prev + 1, Math.max(0, itemCounter - 1)));
90
+ const next = (activeIndex() + 1) % visible.length;
91
+ setActiveIndex(next);
92
+ updateActiveAttribute(next);
69
93
  } else if (e.key === "ArrowUp") {
70
94
  e.preventDefault();
71
- setActiveIndex((prev) => Math.max(prev - 1, 0));
95
+ const prev = (activeIndex() - 1 + visible.length) % visible.length;
96
+ setActiveIndex(prev);
97
+ updateActiveAttribute(prev);
72
98
  } else if (e.key === "Enter") {
73
99
  e.preventDefault();
74
- const fn = itemCallbacks[activeIndex()];
75
- if (fn) fn();
76
- const current = containerRef?.querySelector('[data-command-active="true"]') as HTMLElement;
100
+ const current = visible[activeIndex()];
77
101
  if (current) {
78
102
  current.click();
79
103
  }
@@ -144,12 +168,14 @@ export const CommandDialog: Component<CommandDialogProps> = (props) => {
144
168
 
145
169
  return (
146
170
  <Dialog open={isOpen()} onOpenChange={setOpen}>
147
- <DialogOverlay class="fixed inset-0 z-50 bg-black/60 backdrop-blur-xs data-expanded:animate-in data-closed:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0" />
148
- <div class="fixed inset-0 z-50 flex items-start justify-center pt-16 sm:pt-24 px-4">
149
- <DialogContent class="w-full max-w-xl rounded-lg border border-border bg-popover text-popover-foreground shadow-2xl outline-none 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 p-0">
150
- <Command class={props.class}>{props.children}</Command>
151
- </DialogContent>
152
- </div>
171
+ <Dialog.Portal>
172
+ <Dialog.Overlay class="fixed inset-0 z-50 bg-black/60 backdrop-blur-xs data-expanded:animate-in data-closed:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0" />
173
+ <div class="fixed inset-0 z-50 flex items-start justify-center pt-16 sm:pt-24 px-4">
174
+ <Dialog.Content class="w-full max-w-xl rounded-lg border border-border bg-popover text-popover-foreground shadow-2xl outline-none 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">
175
+ <Command class={props.class}>{props.children}</Command>
176
+ </Dialog.Content>
177
+ </div>
178
+ </Dialog.Portal>
153
179
  </Dialog>
154
180
  );
155
181
  };
@@ -257,118 +283,88 @@ export const CommandItem: Component<CommandItemProps> = (props) => {
257
283
  "title",
258
284
  "subtitle",
259
285
  "keywords",
260
- "disabled",
261
286
  "onSelect",
287
+ "onClick",
262
288
  "class",
263
- "children",
264
289
  ]);
290
+ const { search, activeIndex } = useCommand();
265
291
 
266
- const ctx = useCommand();
267
- const index = ctx.registerItemIndex();
268
-
269
- const isActive = () => ctx.activeIndex() === index;
270
-
271
- /* Automatic scroll into view when navigating with Arrow keys */
272
- const scrollIntoViewIfNeeded = () => {
273
- if (isActive() && itemRef) {
274
- itemRef.scrollIntoView({ block: "nearest", behavior: "smooth" });
275
- }
276
- };
277
-
278
- const isVisible = () => {
279
- const query = ctx.search().toLowerCase().trim();
292
+ /* Auto fuzzy-filter item based on search query */
293
+ const matchesSearch = () => {
294
+ const query = search().toLowerCase().trim();
280
295
  if (!query) return true;
281
296
 
282
- const titleStr = typeof local.title === "string" ? local.title.toLowerCase() : "";
283
- const subStr = typeof local.subtitle === "string" ? local.subtitle.toLowerCase() : "";
284
-
285
- if (titleStr.includes(query) || subStr.includes(query)) return true;
286
-
287
- if (local.keywords) {
288
- return local.keywords.some((k) => k.toLowerCase().includes(query));
289
- }
297
+ const titleMatch = local.title?.toLowerCase().includes(query);
298
+ const subtitleMatch = local.subtitle?.toLowerCase().includes(query);
299
+ const keywordMatch = local.keywords?.some((k) =>
300
+ k.toLowerCase().includes(query)
301
+ );
290
302
 
291
- return false;
303
+ return Boolean(titleMatch || subtitleMatch || keywordMatch);
292
304
  };
293
305
 
294
- const handleSelect = () => {
295
- if (local.disabled) return;
296
- if (local.onSelect) local.onSelect();
306
+ const handleClick = (e: MouseEvent) => {
307
+ if (typeof local.onSelect === "function") {
308
+ local.onSelect();
309
+ }
310
+ if (typeof local.onClick === "function") {
311
+ local.onClick(e as any);
312
+ }
297
313
  };
298
314
 
299
315
  return (
300
- <Show when={isVisible()}>
316
+ <Show when={matchesSearch()}>
301
317
  <ListItem
302
- ref={(el) => {
303
- itemRef = el;
304
- scrollIntoViewIfNeeded();
305
- }}
318
+ ref={itemRef}
319
+ data-command-item="true"
306
320
  title={local.title}
307
321
  subtitle={local.subtitle}
308
- data-command-active={isActive() ? "true" : "false"}
322
+ onClick={handleClick}
309
323
  class={cn(
310
- "relative flex cursor-pointer select-none items-center rounded-md px-2 py-1.5 text-sm outline-none transition-colors",
311
- isActive()
312
- ? "bg-accent text-accent-foreground font-medium"
313
- : "text-popover-foreground hover:bg-muted/50",
314
- local.disabled && "pointer-events-none opacity-50",
324
+ "aria-selected:bg-accent aria-selected:text-accent-foreground cursor-pointer transition-colors hover:bg-accent hover:text-accent-foreground",
315
325
  local.class
316
326
  )}
317
- onClick={handleSelect}
318
- onMouseEnter={() => ctx.setActiveIndex(() => index)}
319
327
  {...rest}
320
- >
321
- {local.children}
322
- </ListItem>
328
+ />
323
329
  </Show>
324
330
  );
325
331
  };
326
332
 
327
- /* --- Command Footer Indicator Bar --- */
333
+ /* --- Command Footer --- */
328
334
  export interface CommandFooterProps extends JSX.HTMLAttributes<HTMLDivElement> {
329
335
  class?: string;
330
336
  }
331
337
 
332
338
  export const CommandFooter: Component<CommandFooterProps> = (props) => {
333
- const [local, rest] = splitProps(props, ["class", "children"]);
339
+ const [local, rest] = splitProps(props, ["class"]);
334
340
 
335
341
  return (
336
342
  <div
337
343
  class={cn(
338
- "flex items-center justify-between border-t border-border px-3 py-2 text-xs text-muted-foreground bg-muted/40",
344
+ "flex items-center justify-between border-t border-border bg-muted/30 px-3 py-2 text-xs text-muted-foreground select-none",
339
345
  local.class
340
346
  )}
341
347
  {...rest}
342
348
  >
343
- {local.children || (
344
- <>
345
- <div class="flex items-center gap-2">
346
- <span class="inline-flex items-center gap-1">
347
- <KbdGroup>
348
- <Kbd size="sm">
349
- <ArrowUp class="w-2.5 h-2.5" />
350
- </Kbd>
351
- <Kbd size="sm">
352
- <ArrowDown class="w-2.5 h-2.5" />
353
- </Kbd>
354
- </KbdGroup>
355
- <span>Navigate</span>
356
- </span>
357
-
358
- <span class="inline-flex items-center gap-1">
359
- <Kbd size="sm">
360
- <CornerDownLeft class="w-2.5 h-2.5" />
361
- </Kbd>
362
- <span>Select</span>
363
- </span>
364
- </div>
365
-
366
- <span class="inline-flex items-center gap-1">
367
- <Kbd size="sm">ESC</Kbd>
368
- <span>Close</span>
369
- </span>
370
- </>
371
- )}
349
+ <div class="flex items-center gap-3">
350
+ <span class="flex items-center gap-1">
351
+ <KbdGroup>
352
+ <Kbd size="sm"><ArrowUp class="w-2.5 h-2.5" /></Kbd>
353
+ <Kbd size="sm"><ArrowDown class="w-2.5 h-2.5" /></Kbd>
354
+ </KbdGroup>
355
+ <span>Navigate</span>
356
+ </span>
357
+
358
+ <span class="flex items-center gap-1">
359
+ <Kbd size="sm"><CornerDownLeft class="w-2.5 h-2.5" /></Kbd>
360
+ <span>Select</span>
361
+ </span>
362
+ </div>
363
+
364
+ <span class="flex items-center gap-1">
365
+ <Kbd size="sm">Esc</Kbd>
366
+ <span>Close</span>
367
+ </span>
372
368
  </div>
373
369
  );
374
370
  };
@@ -1,21 +1,18 @@
1
1
  import { splitProps, type Component, type JSX, type ValidComponent } from "solid-js";
2
2
  import * as ContextMenuPrimitive from "@kobalte/core/context-menu";
3
3
  import type { PolymorphicProps } from "@kobalte/core/polymorphic";
4
- import { createClickOutside } from "@nikala-ui/hooks";
5
- import { ScrollArea } from "./scroll-area";
6
- import { Separator } from "./separator";
7
- import { Kbd } from "./kbd";
4
+ import { createClickOutside } from "@/hooks/create-click-outside";
5
+ import { ScrollArea } from "@/components/ui/scroll-area";
6
+ import { Separator } from "@/components/ui/separator";
7
+ import { Kbd } from "@/components/ui/kbd";
8
8
  import { cn } from "@/lib/cn";
9
9
 
10
- export type ContextMenuRootProps = ContextMenuPrimitive.ContextMenuRootProps;
11
-
12
- export const ContextMenu: Component<ContextMenuRootProps> = (props) => {
13
- return <ContextMenuPrimitive.Root {...props} />;
14
- };
15
-
10
+ export const ContextMenu = ContextMenuPrimitive.Root;
16
11
  export const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
17
12
  export const ContextMenuGroup = ContextMenuPrimitive.Group;
13
+ export const ContextMenuPortal = ContextMenuPrimitive.Portal;
18
14
  export const ContextMenuSub = ContextMenuPrimitive.Sub;
15
+ export const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
19
16
 
20
17
  export type ContextMenuSubTriggerProps<T extends ValidComponent = "div"> =
21
18
  ContextMenuPrimitive.ContextMenuSubTriggerProps<T> & {
@@ -32,7 +29,7 @@ export const ContextMenuSubTrigger = <T extends ValidComponent = "div">(
32
29
  return (
33
30
  <ContextMenuPrimitive.SubTrigger
34
31
  class={cn(
35
- "flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground text-foreground",
32
+ "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",
36
33
  local.inset && "pl-8",
37
34
  local.class
38
35
  )}
@@ -126,7 +123,7 @@ export const ContextMenuItem = <T extends ValidComponent = "div">(
126
123
  return (
127
124
  <ContextMenuPrimitive.Item
128
125
  class={cn(
129
- "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 text-foreground",
126
+ "relative flex cursor-default 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",
130
127
  local.inset && "pl-8",
131
128
  local.class
132
129
  )}
@@ -151,7 +148,7 @@ export const ContextMenuCheckboxItem = <T extends ValidComponent = "div">(
151
148
  <ContextMenuPrimitive.CheckboxItem
152
149
  checked={local.checked}
153
150
  class={cn(
154
- "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 text-foreground",
151
+ "relative flex cursor-default 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",
155
152
  local.class
156
153
  )}
157
154
  {...(rest as any)}
@@ -168,25 +165,73 @@ export const ContextMenuCheckboxItem = <T extends ValidComponent = "div">(
168
165
  );
169
166
  };
170
167
 
171
- export interface ContextMenuShortcutProps extends JSX.HTMLAttributes<HTMLSpanElement> {
168
+ export type ContextMenuRadioItemProps<T extends ValidComponent = "div"> =
169
+ ContextMenuPrimitive.ContextMenuRadioItemProps<T> & {
170
+ class?: string;
171
+ children?: JSX.Element;
172
+ };
173
+
174
+ export const ContextMenuRadioItem = <T extends ValidComponent = "div">(
175
+ props: PolymorphicProps<T, ContextMenuRadioItemProps<T>>
176
+ ) => {
177
+ const [local, rest] = splitProps(props as ContextMenuRadioItemProps, ["class", "children"]);
178
+
179
+ return (
180
+ <ContextMenuPrimitive.RadioItem
181
+ class={cn(
182
+ "relative flex cursor-default 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",
183
+ local.class
184
+ )}
185
+ {...(rest as any)}
186
+ >
187
+ <span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
188
+ <ContextMenuPrimitive.ItemIndicator>
189
+ <svg class="h-2 w-2 fill-current" viewBox="0 0 8 8">
190
+ <circle cx="4" cy="4" r="3" />
191
+ </svg>
192
+ </ContextMenuPrimitive.ItemIndicator>
193
+ </span>
194
+ {local.children}
195
+ </ContextMenuPrimitive.RadioItem>
196
+ );
197
+ };
198
+
199
+ export interface ContextMenuLabelProps {
172
200
  class?: string;
201
+ inset?: boolean;
173
202
  children?: JSX.Element;
174
203
  }
175
204
 
176
- export const ContextMenuShortcut: Component<ContextMenuShortcutProps> = (props) => {
177
- const [local, rest] = splitProps(props, ["class", "children"]);
205
+ export const ContextMenuLabel: Component<ContextMenuLabelProps> = (props) => {
206
+ const [local, rest] = splitProps(props, ["class", "inset", "children"]);
178
207
 
179
208
  return (
180
- <span class={cn("ml-auto text-xs tracking-widest text-muted-foreground", local.class)} {...rest}>
181
- <Kbd size="sm">{local.children}</Kbd>
182
- </span>
209
+ <ContextMenuPrimitive.GroupLabel
210
+ class={cn("px-2 py-1.5 text-sm font-semibold text-foreground", local.inset && "pl-8", local.class)}
211
+ {...rest}
212
+ >
213
+ {local.children}
214
+ </ContextMenuPrimitive.GroupLabel>
183
215
  );
184
216
  };
185
217
 
186
- export interface ContextMenuSeparatorProps {
218
+ export const ContextMenuSeparator: Component<{ class?: string }> = (props) => {
219
+ return <Separator class={cn("-mx-1 my-1", props.class)} />;
220
+ };
221
+
222
+ export interface ContextMenuShortcutProps {
187
223
  class?: string;
224
+ children?: JSX.Element;
188
225
  }
189
226
 
190
- export const ContextMenuSeparator: Component<ContextMenuSeparatorProps> = (props) => {
191
- return <Separator class={cn("-mx-1 my-1 h-px bg-border", props.class)} />;
227
+ export const ContextMenuShortcut: Component<ContextMenuShortcutProps> = (props) => {
228
+ const [local, rest] = splitProps(props, ["class", "children"]);
229
+
230
+ return (
231
+ <span class={cn("ml-auto pl-4 text-xs tracking-widest text-muted-foreground", local.class)} {...rest}>
232
+ <Kbd size="sm" variant="outline">
233
+ {local.children}
234
+ </Kbd>
235
+ </span>
236
+ );
192
237
  };