@nikala-ui/core 0.9.11 → 0.10.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/package.json +1 -1
- package/registry/aspect-ratio.json +17 -0
- package/registry/button.json +4 -1
- package/registry/collapsible.json +18 -0
- package/registry/combobox.json +4 -1
- package/registry/command.json +3 -2
- package/registry/context-menu.json +24 -0
- package/registry/create-form.json +1 -1
- package/registry/dialog.json +4 -1
- package/registry/dropdown-menu.json +4 -1
- package/registry/empty.json +17 -0
- package/registry/field.json +20 -0
- package/registry/form-message.json +16 -0
- package/registry/form.json +17 -0
- package/registry/icon-button.json +20 -0
- package/registry/index.json +191 -1
- package/registry/number-input.json +24 -0
- package/registry/resizable.json +21 -0
- package/registry/scroll-area.json +21 -0
- package/registry/select.json +4 -1
- package/registry/sheet.json +4 -1
- package/registry/spinner.json +18 -0
- package/registry/status.json +18 -0
- package/registry/theme-manager.json +1 -1
- package/registry/toggle.json +18 -0
- package/src/registry/components/ui/aspect-ratio.tsx +31 -0
- package/src/registry/components/ui/button.tsx +18 -3
- package/src/registry/components/ui/collapsible.tsx +73 -0
- package/src/registry/components/ui/combobox.tsx +23 -84
- package/src/registry/components/ui/command.tsx +154 -66
- package/src/registry/components/ui/context-menu.tsx +192 -0
- package/src/registry/components/ui/dialog.tsx +39 -44
- package/src/registry/components/ui/dropdown-menu.tsx +40 -45
- package/src/registry/components/ui/empty.tsx +83 -0
- package/src/registry/components/ui/field.tsx +67 -0
- package/src/registry/components/ui/form-message.tsx +36 -0
- package/src/registry/components/ui/form.tsx +21 -0
- package/src/registry/components/ui/icon-button.tsx +34 -0
- package/src/registry/components/ui/number-input.tsx +150 -0
- package/src/registry/components/ui/resizable.tsx +193 -0
- package/src/registry/components/ui/scroll-area.tsx +218 -0
- package/src/registry/components/ui/select.tsx +22 -16
- package/src/registry/components/ui/sheet.tsx +62 -63
- package/src/registry/components/ui/spinner.tsx +41 -0
- package/src/registry/components/ui/status.tsx +119 -0
- package/src/registry/components/ui/theme-toggle.tsx +6 -4
- package/src/registry/components/ui/toggle.tsx +101 -0
- package/src/registry/metadata.ts +84 -2
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { splitProps, type Component, type JSX } from "solid-js";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface AspectRatioProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
ratio?: number;
|
|
6
|
+
class?: string;
|
|
7
|
+
children?: JSX.Element;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Nikala UI AspectRatio Component.
|
|
12
|
+
* Displays content within a specific aspect ratio (e.g. 16/9, 4/3, 1/1) using CSS aspect-ratio.
|
|
13
|
+
*/
|
|
14
|
+
export const AspectRatio: Component<AspectRatioProps> = (props) => {
|
|
15
|
+
const [local, rest] = splitProps(props, ["ratio", "class", "children", "style"]);
|
|
16
|
+
|
|
17
|
+
const computedRatio = () => (local.ratio !== undefined ? local.ratio : 16 / 9);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
class={cn("relative w-full overflow-hidden", local.class)}
|
|
22
|
+
style={{
|
|
23
|
+
"aspect-ratio": `${computedRatio()}`,
|
|
24
|
+
...(typeof local.style === "object" ? local.style : {}),
|
|
25
|
+
}}
|
|
26
|
+
{...rest}
|
|
27
|
+
>
|
|
28
|
+
{local.children}
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { splitProps, type Component, type JSX } from "solid-js";
|
|
1
|
+
import { Show, splitProps, type Component, type JSX } from "solid-js";
|
|
2
2
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
3
|
import { cn } from "@/lib/cn";
|
|
4
|
+
import { Spinner } from "./spinner";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Class variance authority configuration for button styling variants and sizes.
|
|
@@ -46,6 +47,8 @@ export const buttonVariants = cva(
|
|
|
46
47
|
export interface ButtonProps
|
|
47
48
|
extends JSX.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
48
49
|
VariantProps<typeof buttonVariants> {
|
|
50
|
+
/** Shows a loading spinner and prevents repeated clicks while active. */
|
|
51
|
+
loading?: boolean;
|
|
49
52
|
class?: string;
|
|
50
53
|
}
|
|
51
54
|
|
|
@@ -54,14 +57,26 @@ export interface ButtonProps
|
|
|
54
57
|
*/
|
|
55
58
|
export const Button: Component<ButtonProps> = (props) => {
|
|
56
59
|
// Use splitProps to preserve SolidJS reactivity for destructured props
|
|
57
|
-
const [local, rest] = splitProps(props, [
|
|
60
|
+
const [local, rest] = splitProps(props, [
|
|
61
|
+
"variant",
|
|
62
|
+
"size",
|
|
63
|
+
"class",
|
|
64
|
+
"children",
|
|
65
|
+
"loading",
|
|
66
|
+
"disabled",
|
|
67
|
+
]);
|
|
58
68
|
|
|
59
69
|
return (
|
|
60
70
|
<button
|
|
61
71
|
class={cn(buttonVariants({ variant: local.variant, size: local.size }), local.class)}
|
|
72
|
+
disabled={local.disabled || local.loading}
|
|
73
|
+
aria-busy={local.loading ? "true" : undefined}
|
|
62
74
|
{...rest}
|
|
63
75
|
>
|
|
76
|
+
<Show when={local.loading}>
|
|
77
|
+
<Spinner size="sm" class="text-current" />
|
|
78
|
+
</Show>
|
|
64
79
|
{local.children}
|
|
65
80
|
</button>
|
|
66
81
|
);
|
|
67
|
-
};
|
|
82
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { splitProps, type JSX, type ValidComponent } from "solid-js";
|
|
2
|
+
import * as CollapsiblePrimitive from "@kobalte/core/collapsible";
|
|
3
|
+
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
|
4
|
+
import { cn } from "@/lib/cn";
|
|
5
|
+
|
|
6
|
+
export type CollapsibleRootProps<T extends ValidComponent = "div"> =
|
|
7
|
+
CollapsiblePrimitive.CollapsibleRootProps<T> & {
|
|
8
|
+
class?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Root Collapsible component built on Kobalte primitives.
|
|
13
|
+
*/
|
|
14
|
+
export const Collapsible = <T extends ValidComponent = "div">(
|
|
15
|
+
props: PolymorphicProps<T, CollapsibleRootProps<T>>
|
|
16
|
+
) => {
|
|
17
|
+
const [local, rest] = splitProps(props as CollapsibleRootProps, ["class"]);
|
|
18
|
+
return (
|
|
19
|
+
<CollapsiblePrimitive.Root
|
|
20
|
+
class={cn("w-full", local.class)}
|
|
21
|
+
{...(rest as any)}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type CollapsibleTriggerProps<T extends ValidComponent = "button"> =
|
|
27
|
+
CollapsiblePrimitive.CollapsibleTriggerProps<T> & {
|
|
28
|
+
class?: string;
|
|
29
|
+
children?: JSX.Element;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Trigger element that toggles the Collapsible open/closed state.
|
|
34
|
+
*/
|
|
35
|
+
export const CollapsibleTrigger = <T extends ValidComponent = "button">(
|
|
36
|
+
props: PolymorphicProps<T, CollapsibleTriggerProps<T>>
|
|
37
|
+
) => {
|
|
38
|
+
const [local, rest] = splitProps(props as CollapsibleTriggerProps, ["class", "children"]);
|
|
39
|
+
return (
|
|
40
|
+
<CollapsiblePrimitive.Trigger
|
|
41
|
+
class={cn("flex w-full items-center justify-between cursor-pointer", local.class)}
|
|
42
|
+
{...rest}
|
|
43
|
+
>
|
|
44
|
+
{local.children}
|
|
45
|
+
</CollapsiblePrimitive.Trigger>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type CollapsibleContentProps<T extends ValidComponent = "div"> =
|
|
50
|
+
CollapsiblePrimitive.CollapsibleContentProps<T> & {
|
|
51
|
+
class?: string;
|
|
52
|
+
children?: JSX.Element;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Collapsible content panel revealed when opened.
|
|
57
|
+
*/
|
|
58
|
+
export const CollapsibleContent = <T extends ValidComponent = "div">(
|
|
59
|
+
props: PolymorphicProps<T, CollapsibleContentProps<T>>
|
|
60
|
+
) => {
|
|
61
|
+
const [local, rest] = splitProps(props as CollapsibleContentProps, ["class", "children"]);
|
|
62
|
+
return (
|
|
63
|
+
<CollapsiblePrimitive.Content
|
|
64
|
+
class={cn(
|
|
65
|
+
"overflow-hidden transition-all data-expanded:animate-collapsible-down data-closed:animate-collapsible-up",
|
|
66
|
+
local.class
|
|
67
|
+
)}
|
|
68
|
+
{...rest}
|
|
69
|
+
>
|
|
70
|
+
{local.children}
|
|
71
|
+
</CollapsiblePrimitive.Content>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
@@ -1,86 +1,44 @@
|
|
|
1
1
|
import { splitProps, type JSX, type ValidComponent } from "solid-js";
|
|
2
|
+
import { Check, ChevronDown, X } from "lucide-solid";
|
|
3
|
+
import { ScrollArea } from "./scroll-area";
|
|
2
4
|
import * as ComboboxPrimitive from "@kobalte/core/combobox";
|
|
3
5
|
import { cn } from "@/lib/cn";
|
|
4
6
|
|
|
5
7
|
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
|
-
};
|
|
8
|
+
ComboboxPrimitive.ComboboxRootProps<Option, OptGroup, T>;
|
|
11
9
|
|
|
12
10
|
/**
|
|
13
|
-
* Root Combobox component
|
|
14
|
-
* `triggerMode="focus"` or `triggerMode="both"` enables opening dropdown on input click/focus.
|
|
11
|
+
* Root Combobox primitive component wrapper.
|
|
15
12
|
*/
|
|
16
13
|
export const Combobox = <Option = any, OptGroup = any, T extends ValidComponent = "div">(
|
|
17
14
|
props: ComboboxRootProps<Option, OptGroup, T>
|
|
18
15
|
) => {
|
|
19
|
-
|
|
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
|
-
);
|
|
16
|
+
return <ComboboxPrimitive.Root {...props} />;
|
|
30
17
|
};
|
|
31
18
|
|
|
32
19
|
export type ComboboxControlProps<Option = any, T extends ValidComponent = "div"> =
|
|
33
20
|
ComboboxPrimitive.ComboboxControlProps<Option, T> & {
|
|
34
21
|
class?: string;
|
|
35
22
|
children?: JSX.Element;
|
|
36
|
-
clearable?: boolean;
|
|
37
|
-
onClear?: () => void;
|
|
38
23
|
};
|
|
39
24
|
|
|
40
25
|
/**
|
|
41
|
-
* Input container
|
|
26
|
+
* Input container box supporting single or multi-select tokens.
|
|
42
27
|
*/
|
|
43
28
|
export const ComboboxControl = <Option = any, T extends ValidComponent = "div">(
|
|
44
29
|
props: ComboboxControlProps<Option, T>
|
|
45
30
|
) => {
|
|
46
|
-
const [local, rest] = splitProps(props as ComboboxControlProps, [
|
|
47
|
-
"class",
|
|
48
|
-
"children",
|
|
49
|
-
"clearable",
|
|
50
|
-
"onClear",
|
|
51
|
-
]);
|
|
31
|
+
const [local, rest] = splitProps(props as ComboboxControlProps, ["class", "children"]);
|
|
52
32
|
|
|
53
33
|
return (
|
|
54
34
|
<ComboboxPrimitive.Control
|
|
55
35
|
class={cn(
|
|
56
|
-
"flex min-h-9 w-full flex-wrap items-center
|
|
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",
|
|
57
37
|
local.class
|
|
58
38
|
)}
|
|
59
39
|
{...(rest as any)}
|
|
60
40
|
>
|
|
61
|
-
|
|
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>
|
|
41
|
+
{local.children}
|
|
84
42
|
</ComboboxPrimitive.Control>
|
|
85
43
|
);
|
|
86
44
|
};
|
|
@@ -92,8 +50,7 @@ export type ComboboxInputProps<T extends ValidComponent = "input"> =
|
|
|
92
50
|
};
|
|
93
51
|
|
|
94
52
|
/**
|
|
95
|
-
*
|
|
96
|
-
* Supports `openOnFocus` to automatically trigger the dropdown when focused or clicked.
|
|
53
|
+
* Filter search input field.
|
|
97
54
|
*/
|
|
98
55
|
export const ComboboxInput = <T extends ValidComponent = "input">(
|
|
99
56
|
props: ComboboxInputProps<T>
|
|
@@ -178,12 +135,10 @@ export const ComboboxToken = <Option = any>(props: ComboboxTokenProps<Option>) =
|
|
|
178
135
|
e.stopPropagation();
|
|
179
136
|
if (local.onRemove) local.onRemove();
|
|
180
137
|
}}
|
|
181
|
-
class="rounded-xs
|
|
182
|
-
aria-label="Remove tag"
|
|
138
|
+
class="rounded-xs opacity-70 hover:opacity-100 focus:outline-none cursor-pointer text-muted-foreground hover:text-foreground"
|
|
183
139
|
>
|
|
184
|
-
<
|
|
185
|
-
|
|
186
|
-
</svg>
|
|
140
|
+
<X class="h-3 w-3" />
|
|
141
|
+
<span class="sr-only">Remove</span>
|
|
187
142
|
</button>
|
|
188
143
|
</span>
|
|
189
144
|
);
|
|
@@ -206,12 +161,14 @@ export const ComboboxContent = <T extends ValidComponent = "div">(
|
|
|
206
161
|
<ComboboxPrimitive.Portal>
|
|
207
162
|
<ComboboxPrimitive.Content
|
|
208
163
|
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-
|
|
164
|
+
"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-[expanded]:fade-in-0 data-closed:zoom-out-95 data-expanded:zoom-in-95 max-h-60",
|
|
210
165
|
local.class
|
|
211
166
|
)}
|
|
212
167
|
{...(rest as any)}
|
|
213
168
|
>
|
|
214
|
-
<
|
|
169
|
+
<ScrollArea class="max-h-60 w-full">
|
|
170
|
+
<ComboboxPrimitive.Listbox class="p-1 outline-none" />
|
|
171
|
+
</ScrollArea>
|
|
215
172
|
</ComboboxPrimitive.Content>
|
|
216
173
|
</ComboboxPrimitive.Portal>
|
|
217
174
|
);
|
|
@@ -234,19 +191,17 @@ export const ComboboxItem = <T extends ValidComponent = "li">(
|
|
|
234
191
|
return (
|
|
235
192
|
<ComboboxPrimitive.Item
|
|
236
193
|
class={cn(
|
|
237
|
-
"relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5
|
|
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",
|
|
238
195
|
local.class
|
|
239
196
|
)}
|
|
240
|
-
{...rest}
|
|
197
|
+
{...(rest as any)}
|
|
241
198
|
>
|
|
242
|
-
<ComboboxPrimitive.
|
|
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">
|
|
199
|
+
<ComboboxPrimitive.ItemLabel class="flex-1 truncate">
|
|
248
200
|
{local.children}
|
|
249
201
|
</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>
|
|
250
205
|
</ComboboxPrimitive.Item>
|
|
251
206
|
);
|
|
252
207
|
};
|
|
@@ -277,19 +232,3 @@ export const ComboboxGroup = <T extends ValidComponent = "li">(
|
|
|
277
232
|
</ComboboxPrimitive.Section>
|
|
278
233
|
);
|
|
279
234
|
};
|
|
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;
|
|
@@ -10,17 +10,22 @@ import {
|
|
|
10
10
|
type Accessor,
|
|
11
11
|
} from "solid-js";
|
|
12
12
|
import { createKeybindings } from "@nikala-ui/hooks";
|
|
13
|
-
import { Dialog } from "@kobalte/core/dialog";
|
|
14
13
|
import { Search, ArrowUp, ArrowDown, CornerDownLeft } from "lucide-solid";
|
|
15
14
|
import { cn } from "@/lib/cn";
|
|
16
|
-
import { Kbd, KbdGroup } from "
|
|
17
|
-
import { InputGroup, InputGroupInput, InputGroupAddon } from "
|
|
18
|
-
import { List, ListGroup, ListHeader, ListItem, type ListItemProps } from "
|
|
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";
|
|
19
20
|
|
|
20
21
|
/* --- Command Context State --- */
|
|
21
22
|
interface CommandContextValue {
|
|
22
23
|
search: Accessor<string>;
|
|
23
24
|
setSearch: (value: string) => void;
|
|
25
|
+
activeIndex: Accessor<number>;
|
|
26
|
+
setActiveIndex: (fn: (prev: number) => number) => void;
|
|
27
|
+
registerItemIndex: () => number;
|
|
28
|
+
onItemSelect: (fn: () => void) => void;
|
|
24
29
|
}
|
|
25
30
|
|
|
26
31
|
const CommandContext = createContext<CommandContextValue>();
|
|
@@ -28,30 +33,78 @@ const CommandContext = createContext<CommandContextValue>();
|
|
|
28
33
|
export const useCommand = () => {
|
|
29
34
|
const ctx = useContext(CommandContext);
|
|
30
35
|
if (!ctx) {
|
|
31
|
-
throw new Error("useCommand must be used within a <Command />");
|
|
36
|
+
throw new Error("useCommand must be used within a <Command /> root component.");
|
|
32
37
|
}
|
|
33
38
|
return ctx;
|
|
34
39
|
};
|
|
35
40
|
|
|
36
|
-
/* ---
|
|
37
|
-
export interface CommandProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
41
|
+
/* --- Command Root --- */
|
|
42
|
+
export interface CommandProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>, "children"> {
|
|
38
43
|
class?: string;
|
|
44
|
+
children?: JSX.Element | ((ctx: CommandContextValue) => JSX.Element);
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
export const Command: Component<CommandProps> = (props) => {
|
|
42
48
|
const [local, rest] = splitProps(props, ["class", "children"]);
|
|
43
49
|
const [search, setSearch] = createSignal("");
|
|
50
|
+
const [activeIndex, setActiveIndex] = createSignal(0);
|
|
51
|
+
let itemCounter = 0;
|
|
52
|
+
const itemCallbacks: Record<number, () => void> = {};
|
|
53
|
+
let containerRef: HTMLDivElement | undefined;
|
|
54
|
+
|
|
55
|
+
const registerItemIndex = () => {
|
|
56
|
+
const idx = itemCounter;
|
|
57
|
+
itemCounter += 1;
|
|
58
|
+
return idx;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const onItemSelect = (fn: () => void) => {
|
|
62
|
+
itemCallbacks[activeIndex()] = fn;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
66
|
+
if (e.key === "ArrowDown") {
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
setActiveIndex((prev) => Math.min(prev + 1, Math.max(0, itemCounter - 1)));
|
|
69
|
+
} else if (e.key === "ArrowUp") {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
setActiveIndex((prev) => Math.max(prev - 1, 0));
|
|
72
|
+
} else if (e.key === "Enter") {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
const fn = itemCallbacks[activeIndex()];
|
|
75
|
+
if (fn) fn();
|
|
76
|
+
const current = containerRef?.querySelector('[data-command-active="true"]') as HTMLElement;
|
|
77
|
+
if (current) {
|
|
78
|
+
current.click();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const ctx: CommandContextValue = {
|
|
84
|
+
search,
|
|
85
|
+
setSearch: (val) => {
|
|
86
|
+
setSearch(val);
|
|
87
|
+
setActiveIndex(0);
|
|
88
|
+
},
|
|
89
|
+
activeIndex,
|
|
90
|
+
setActiveIndex: (fn) => setActiveIndex(fn),
|
|
91
|
+
registerItemIndex,
|
|
92
|
+
onItemSelect,
|
|
93
|
+
};
|
|
44
94
|
|
|
45
95
|
return (
|
|
46
|
-
<CommandContext.Provider value={
|
|
96
|
+
<CommandContext.Provider value={ctx}>
|
|
47
97
|
<div
|
|
98
|
+
ref={containerRef}
|
|
99
|
+
onKeyDown={handleKeyDown}
|
|
48
100
|
class={cn(
|
|
49
|
-
"flex flex-col w-full h-full rounded-lg bg-popover text-popover-foreground overflow-hidden border border-border shadow-md",
|
|
101
|
+
"flex flex-col w-full h-full rounded-lg bg-popover text-popover-foreground overflow-hidden border border-border shadow-md outline-none",
|
|
50
102
|
local.class
|
|
51
103
|
)}
|
|
104
|
+
tabIndex={0}
|
|
52
105
|
{...rest}
|
|
53
106
|
>
|
|
54
|
-
{local.children}
|
|
107
|
+
{typeof local.children === "function" ? (local.children as any)(ctx) : local.children}
|
|
55
108
|
</div>
|
|
56
109
|
</CommandContext.Provider>
|
|
57
110
|
);
|
|
@@ -62,7 +115,7 @@ export interface CommandDialogProps {
|
|
|
62
115
|
open?: boolean;
|
|
63
116
|
onOpenChange?: (open: boolean) => void;
|
|
64
117
|
enableHotkey?: boolean;
|
|
65
|
-
children?: JSX.Element;
|
|
118
|
+
children?: JSX.Element | ((ctx: CommandContextValue) => JSX.Element);
|
|
66
119
|
class?: string;
|
|
67
120
|
}
|
|
68
121
|
|
|
@@ -91,14 +144,12 @@ export const CommandDialog: Component<CommandDialogProps> = (props) => {
|
|
|
91
144
|
|
|
92
145
|
return (
|
|
93
146
|
<Dialog open={isOpen()} onOpenChange={setOpen}>
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
<
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
</div>
|
|
101
|
-
</Dialog.Portal>
|
|
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>
|
|
102
153
|
</Dialog>
|
|
103
154
|
);
|
|
104
155
|
};
|
|
@@ -120,6 +171,7 @@ export const CommandInput: Component<CommandInputProps> = (props) => {
|
|
|
120
171
|
</InputGroupAddon>
|
|
121
172
|
|
|
122
173
|
<InputGroupInput
|
|
174
|
+
ref={(el) => setTimeout(() => el.focus(), 50)}
|
|
123
175
|
value={search()}
|
|
124
176
|
onInput={(e) => {
|
|
125
177
|
setSearch(e.currentTarget.value);
|
|
@@ -144,12 +196,12 @@ export const CommandList: Component<CommandListProps> = (props) => {
|
|
|
144
196
|
const [local, rest] = splitProps(props, ["class", "children"]);
|
|
145
197
|
|
|
146
198
|
return (
|
|
147
|
-
<
|
|
148
|
-
class={cn("max-h-82.5
|
|
199
|
+
<ScrollArea
|
|
200
|
+
class={cn("max-h-82.5 p-1.5", local.class)}
|
|
149
201
|
{...rest}
|
|
150
202
|
>
|
|
151
203
|
<List>{local.children}</List>
|
|
152
|
-
</
|
|
204
|
+
</ScrollArea>
|
|
153
205
|
);
|
|
154
206
|
};
|
|
155
207
|
|
|
@@ -200,87 +252,123 @@ export interface CommandItemProps extends ListItemProps {
|
|
|
200
252
|
}
|
|
201
253
|
|
|
202
254
|
export const CommandItem: Component<CommandItemProps> = (props) => {
|
|
255
|
+
let itemRef: HTMLDivElement | undefined;
|
|
203
256
|
const [local, rest] = splitProps(props, [
|
|
204
257
|
"title",
|
|
205
258
|
"subtitle",
|
|
206
259
|
"keywords",
|
|
260
|
+
"disabled",
|
|
207
261
|
"onSelect",
|
|
208
|
-
"onClick",
|
|
209
262
|
"class",
|
|
263
|
+
"children",
|
|
210
264
|
]);
|
|
211
|
-
const { search } = useCommand();
|
|
212
265
|
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
const query = search().toLowerCase().trim();
|
|
216
|
-
if (!query) return true;
|
|
266
|
+
const ctx = useCommand();
|
|
267
|
+
const index = ctx.registerItemIndex();
|
|
217
268
|
|
|
218
|
-
|
|
219
|
-
const subtitleMatch = local.subtitle?.toLowerCase().includes(query);
|
|
220
|
-
const keywordMatch = local.keywords?.some((k) =>
|
|
221
|
-
k.toLowerCase().includes(query)
|
|
222
|
-
);
|
|
269
|
+
const isActive = () => ctx.activeIndex() === index;
|
|
223
270
|
|
|
224
|
-
|
|
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
|
+
}
|
|
225
276
|
};
|
|
226
277
|
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
278
|
+
const isVisible = () => {
|
|
279
|
+
const query = ctx.search().toLowerCase().trim();
|
|
280
|
+
if (!query) return true;
|
|
281
|
+
|
|
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));
|
|
233
289
|
}
|
|
290
|
+
|
|
291
|
+
return false;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const handleSelect = () => {
|
|
295
|
+
if (local.disabled) return;
|
|
296
|
+
if (local.onSelect) local.onSelect();
|
|
234
297
|
};
|
|
235
298
|
|
|
236
299
|
return (
|
|
237
|
-
<Show when={
|
|
300
|
+
<Show when={isVisible()}>
|
|
238
301
|
<ListItem
|
|
302
|
+
ref={(el) => {
|
|
303
|
+
itemRef = el;
|
|
304
|
+
scrollIntoViewIfNeeded();
|
|
305
|
+
}}
|
|
239
306
|
title={local.title}
|
|
240
307
|
subtitle={local.subtitle}
|
|
241
|
-
|
|
242
|
-
class={
|
|
308
|
+
data-command-active={isActive() ? "true" : "false"}
|
|
309
|
+
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",
|
|
315
|
+
local.class
|
|
316
|
+
)}
|
|
317
|
+
onClick={handleSelect}
|
|
318
|
+
onMouseEnter={() => ctx.setActiveIndex(() => index)}
|
|
243
319
|
{...rest}
|
|
244
|
-
|
|
320
|
+
>
|
|
321
|
+
{local.children}
|
|
322
|
+
</ListItem>
|
|
245
323
|
</Show>
|
|
246
324
|
);
|
|
247
325
|
};
|
|
248
326
|
|
|
249
|
-
/* --- Command Footer --- */
|
|
327
|
+
/* --- Command Footer Indicator Bar --- */
|
|
250
328
|
export interface CommandFooterProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
251
329
|
class?: string;
|
|
252
330
|
}
|
|
253
331
|
|
|
254
332
|
export const CommandFooter: Component<CommandFooterProps> = (props) => {
|
|
255
|
-
const [local, rest] = splitProps(props, ["class"]);
|
|
333
|
+
const [local, rest] = splitProps(props, ["class", "children"]);
|
|
256
334
|
|
|
257
335
|
return (
|
|
258
336
|
<div
|
|
259
337
|
class={cn(
|
|
260
|
-
"flex items-center justify-between border-t border-border
|
|
338
|
+
"flex items-center justify-between border-t border-border px-3 py-2 text-xs text-muted-foreground bg-muted/40",
|
|
261
339
|
local.class
|
|
262
340
|
)}
|
|
263
341
|
{...rest}
|
|
264
342
|
>
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
<
|
|
268
|
-
<
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
+
)}
|
|
284
372
|
</div>
|
|
285
373
|
);
|
|
286
374
|
};
|