@nikala-ui/core 0.9.11 → 0.9.12
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/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/dialog.json +4 -1
- package/registry/dropdown-menu.json +4 -1
- package/registry/index.json +111 -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/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/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/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/theme-toggle.tsx +6 -4
- package/src/registry/components/ui/toggle.tsx +101 -0
- package/src/registry/metadata.ts +45 -1
|
@@ -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
|
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { splitProps, type Component, type JSX, type ValidComponent } from "solid-js";
|
|
2
|
+
import * as ContextMenuPrimitive from "@kobalte/core/context-menu";
|
|
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";
|
|
8
|
+
import { cn } from "@/lib/cn";
|
|
9
|
+
|
|
10
|
+
export type ContextMenuRootProps = ContextMenuPrimitive.ContextMenuRootProps;
|
|
11
|
+
|
|
12
|
+
export const ContextMenu: Component<ContextMenuRootProps> = (props) => {
|
|
13
|
+
return <ContextMenuPrimitive.Root {...props} />;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
|
|
17
|
+
export const ContextMenuGroup = ContextMenuPrimitive.Group;
|
|
18
|
+
export const ContextMenuSub = ContextMenuPrimitive.Sub;
|
|
19
|
+
|
|
20
|
+
export type ContextMenuSubTriggerProps<T extends ValidComponent = "div"> =
|
|
21
|
+
ContextMenuPrimitive.ContextMenuSubTriggerProps<T> & {
|
|
22
|
+
class?: string;
|
|
23
|
+
children?: JSX.Element;
|
|
24
|
+
inset?: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const ContextMenuSubTrigger = <T extends ValidComponent = "div">(
|
|
28
|
+
props: PolymorphicProps<T, ContextMenuSubTriggerProps<T>>
|
|
29
|
+
) => {
|
|
30
|
+
const [local, rest] = splitProps(props as ContextMenuSubTriggerProps, ["class", "children", "inset"]);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<ContextMenuPrimitive.SubTrigger
|
|
34
|
+
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",
|
|
36
|
+
local.inset && "pl-8",
|
|
37
|
+
local.class
|
|
38
|
+
)}
|
|
39
|
+
{...(rest as any)}
|
|
40
|
+
>
|
|
41
|
+
{local.children}
|
|
42
|
+
<svg class="ml-auto h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
43
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
|
|
44
|
+
</svg>
|
|
45
|
+
</ContextMenuPrimitive.SubTrigger>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type ContextMenuSubContentProps<T extends ValidComponent = "div"> =
|
|
50
|
+
ContextMenuPrimitive.ContextMenuSubContentProps<T> & {
|
|
51
|
+
class?: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const ContextMenuSubContent = <T extends ValidComponent = "div">(
|
|
55
|
+
props: PolymorphicProps<T, ContextMenuSubContentProps<T>>
|
|
56
|
+
) => {
|
|
57
|
+
const [local, rest] = splitProps(props as ContextMenuSubContentProps, ["class"]);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<ContextMenuPrimitive.Portal>
|
|
61
|
+
<ContextMenuPrimitive.SubContent
|
|
62
|
+
class={cn(
|
|
63
|
+
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md transition-all animate-in fade-in-80 slide-in-from-top-1",
|
|
64
|
+
local.class
|
|
65
|
+
)}
|
|
66
|
+
{...(rest as any)}
|
|
67
|
+
/>
|
|
68
|
+
</ContextMenuPrimitive.Portal>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type ContextMenuContentProps<T extends ValidComponent = "div"> =
|
|
73
|
+
ContextMenuPrimitive.ContextMenuContentProps<T> & {
|
|
74
|
+
class?: string;
|
|
75
|
+
ref?: (el: HTMLElement) => void;
|
|
76
|
+
children?: JSX.Element;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const ContextMenuContent = <T extends ValidComponent = "div">(
|
|
80
|
+
props: PolymorphicProps<T, ContextMenuContentProps<T>>
|
|
81
|
+
) => {
|
|
82
|
+
const [local, rest] = splitProps(props as ContextMenuContentProps, ["class", "ref", "children"]);
|
|
83
|
+
let menuRef: HTMLElement | undefined;
|
|
84
|
+
|
|
85
|
+
createClickOutside({
|
|
86
|
+
target: () => menuRef,
|
|
87
|
+
onInteractOutside: () => {
|
|
88
|
+
// Automatic portal dismiss on outside click
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<ContextMenuPrimitive.Portal>
|
|
94
|
+
<ContextMenuPrimitive.Content
|
|
95
|
+
ref={(el) => {
|
|
96
|
+
menuRef = el;
|
|
97
|
+
if (typeof local.ref === "function") local.ref(el);
|
|
98
|
+
}}
|
|
99
|
+
class={cn(
|
|
100
|
+
"z-50 min-w-[8rem] rounded-md border border-border bg-popover text-popover-foreground shadow-md transition-all animate-in fade-in-80 slide-in-from-top-1 max-h-72 flex flex-col",
|
|
101
|
+
local.class
|
|
102
|
+
)}
|
|
103
|
+
{...(rest as any)}
|
|
104
|
+
>
|
|
105
|
+
<ScrollArea class="max-h-72 w-full rounded-[inherit]">
|
|
106
|
+
<div class="p-1">
|
|
107
|
+
{local.children}
|
|
108
|
+
</div>
|
|
109
|
+
</ScrollArea>
|
|
110
|
+
</ContextMenuPrimitive.Content>
|
|
111
|
+
</ContextMenuPrimitive.Portal>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export type ContextMenuItemProps<T extends ValidComponent = "div"> =
|
|
116
|
+
ContextMenuPrimitive.ContextMenuItemProps<T> & {
|
|
117
|
+
class?: string;
|
|
118
|
+
inset?: boolean;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const ContextMenuItem = <T extends ValidComponent = "div">(
|
|
122
|
+
props: PolymorphicProps<T, ContextMenuItemProps<T>>
|
|
123
|
+
) => {
|
|
124
|
+
const [local, rest] = splitProps(props as ContextMenuItemProps, ["class", "inset"]);
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<ContextMenuPrimitive.Item
|
|
128
|
+
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",
|
|
130
|
+
local.inset && "pl-8",
|
|
131
|
+
local.class
|
|
132
|
+
)}
|
|
133
|
+
{...(rest as any)}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export type ContextMenuCheckboxItemProps<T extends ValidComponent = "div"> =
|
|
139
|
+
ContextMenuPrimitive.ContextMenuCheckboxItemProps<T> & {
|
|
140
|
+
class?: string;
|
|
141
|
+
children?: JSX.Element;
|
|
142
|
+
checked?: boolean;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const ContextMenuCheckboxItem = <T extends ValidComponent = "div">(
|
|
146
|
+
props: PolymorphicProps<T, ContextMenuCheckboxItemProps<T>>
|
|
147
|
+
) => {
|
|
148
|
+
const [local, rest] = splitProps(props as ContextMenuCheckboxItemProps, ["class", "children", "checked"]);
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<ContextMenuPrimitive.CheckboxItem
|
|
152
|
+
checked={local.checked}
|
|
153
|
+
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",
|
|
155
|
+
local.class
|
|
156
|
+
)}
|
|
157
|
+
{...(rest as any)}
|
|
158
|
+
>
|
|
159
|
+
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
160
|
+
<ContextMenuPrimitive.ItemIndicator>
|
|
161
|
+
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
162
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
|
163
|
+
</svg>
|
|
164
|
+
</ContextMenuPrimitive.ItemIndicator>
|
|
165
|
+
</span>
|
|
166
|
+
{local.children}
|
|
167
|
+
</ContextMenuPrimitive.CheckboxItem>
|
|
168
|
+
);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export interface ContextMenuShortcutProps extends JSX.HTMLAttributes<HTMLSpanElement> {
|
|
172
|
+
class?: string;
|
|
173
|
+
children?: JSX.Element;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export const ContextMenuShortcut: Component<ContextMenuShortcutProps> = (props) => {
|
|
177
|
+
const [local, rest] = splitProps(props, ["class", "children"]);
|
|
178
|
+
|
|
179
|
+
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>
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export interface ContextMenuSeparatorProps {
|
|
187
|
+
class?: string;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export const ContextMenuSeparator: Component<ContextMenuSeparatorProps> = (props) => {
|
|
191
|
+
return <Separator class={cn("-mx-1 my-1 h-px bg-border", props.class)} />;
|
|
192
|
+
};
|