@allbluecn/web-app 0.4.9 → 0.4.10
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 +9 -7
- package/src/components/{command-palette.tsx → command/command-palette.tsx} +95 -47
- package/src/components/hextaui/command-menu.tsx +460 -0
- package/src/components/layout/sidebar-03/sidebar-search-trigger.tsx +21 -7
- package/src/lib/commands/registry.ts +2 -2
- package/src/routes/_nav/route.lazy.tsx +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@allbluecn/web-app",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.10",
|
|
4
4
|
"license": "AGPL-3.0-or-later",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -27,9 +27,11 @@
|
|
|
27
27
|
"@iconify/react": "^6.0.2",
|
|
28
28
|
"@lobehub/icons": "^5.10.0",
|
|
29
29
|
"@prisma/client": "^7.8.0",
|
|
30
|
+
"@radix-ui/react-dialog": "^1.1.23",
|
|
30
31
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
31
32
|
"@radix-ui/react-popover": "^1.1.15",
|
|
32
33
|
"@radix-ui/react-slot": "^1.2.4",
|
|
34
|
+
"@radix-ui/react-visually-hidden": "^1.2.11",
|
|
33
35
|
"@tanstack/ai": "^0.43.0",
|
|
34
36
|
"@tanstack/ai-anthropic": "^0.16.3",
|
|
35
37
|
"@tanstack/ai-gemini": "^0.20.1",
|
|
@@ -131,14 +133,14 @@
|
|
|
131
133
|
"three": "^0.180.0",
|
|
132
134
|
"tw-animate-css": "^1.4.0",
|
|
133
135
|
"zod": "^4.4.3",
|
|
134
|
-
"@allbluecn/database": "0.4.
|
|
136
|
+
"@allbluecn/database": "0.4.10",
|
|
135
137
|
"@allbluecn/kernel": "0.3.0",
|
|
136
|
-
"@allbluecn/ai-gateway": "0.4.
|
|
137
|
-
"@allbluecn/plugins-core": "0.4.
|
|
138
|
+
"@allbluecn/ai-gateway": "0.4.10",
|
|
139
|
+
"@allbluecn/plugins-core": "0.4.10",
|
|
138
140
|
"@allbluecn/prototype": "0.1.1",
|
|
139
|
-
"@allbluecn/
|
|
140
|
-
"@allbluecn/
|
|
141
|
-
"@allbluecn/ui": "0.4.
|
|
141
|
+
"@allbluecn/shared": "0.4.10",
|
|
142
|
+
"@allbluecn/tiptap": "0.4.10",
|
|
143
|
+
"@allbluecn/ui": "0.4.10"
|
|
142
144
|
},
|
|
143
145
|
"devDependencies": {
|
|
144
146
|
"@babel/parser": "^8.0.4",
|
|
@@ -21,15 +21,15 @@ import { useHotkey, useHotkeySequences } from "@tanstack/react-hotkeys"
|
|
|
21
21
|
import type { HotkeySequence } from "@tanstack/hotkeys"
|
|
22
22
|
import { useTranslation } from "react-i18next"
|
|
23
23
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
} from "@/components/
|
|
24
|
+
CommandMenu,
|
|
25
|
+
CommandMenuContent,
|
|
26
|
+
CommandMenuInput,
|
|
27
|
+
CommandMenuList,
|
|
28
|
+
CommandMenuEmpty,
|
|
29
|
+
CommandMenuGroup,
|
|
30
|
+
CommandMenuItem,
|
|
31
|
+
useCommandMenu,
|
|
32
|
+
} from "@/components/hextaui/command-menu"
|
|
33
33
|
import { CreatePRDDialog } from "@allbluecn/plugins-core/prd/components/dialog/create-prd-dialog"
|
|
34
34
|
import { useCommands, type ResolvedCommand } from "@/hooks/use-commands"
|
|
35
35
|
import {
|
|
@@ -54,8 +54,8 @@ import {
|
|
|
54
54
|
ShoppingCart,
|
|
55
55
|
Building2,
|
|
56
56
|
Settings,
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
SunMoon,
|
|
58
|
+
LogOut,
|
|
59
59
|
} from "lucide-react"
|
|
60
60
|
import type { LucideIcon } from "lucide-react"
|
|
61
61
|
|
|
@@ -76,8 +76,8 @@ const PALETTE_ICON_MAP: Record<string, LucideIcon> = {
|
|
|
76
76
|
cart: ShoppingCart,
|
|
77
77
|
building2: Building2,
|
|
78
78
|
settings: Settings,
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
sunMoon: SunMoon,
|
|
80
|
+
logOut: LogOut,
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
export function CommandPalette() {
|
|
@@ -100,15 +100,12 @@ export function CommandPalette() {
|
|
|
100
100
|
},
|
|
101
101
|
})
|
|
102
102
|
|
|
103
|
-
const handleClose = () => setCommandPaletteOpen(false)
|
|
104
|
-
|
|
105
103
|
const run = (cmd: ResolvedCommand) => {
|
|
106
|
-
|
|
104
|
+
setCommandPaletteOpen(false)
|
|
107
105
|
execute(cmd)
|
|
108
106
|
}
|
|
109
107
|
|
|
110
108
|
useHotkey("Mod+K", toggleCommandPalette, { ignoreInputs: false })
|
|
111
|
-
|
|
112
109
|
useHotkey("Mod+Shift+L", () => setTheme(theme === "dark" ? "light" : "dark"), {
|
|
113
110
|
ignoreInputs: false,
|
|
114
111
|
})
|
|
@@ -123,50 +120,101 @@ export function CommandPalette() {
|
|
|
123
120
|
})),
|
|
124
121
|
)
|
|
125
122
|
|
|
123
|
+
return (
|
|
124
|
+
<>
|
|
125
|
+
<CommandMenu
|
|
126
|
+
open={open}
|
|
127
|
+
onOpenChange={(next) => {
|
|
128
|
+
if (next) toggleCommandPalette()
|
|
129
|
+
else setCommandPaletteOpen(false)
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
<CommandMenuContent className="w-205">
|
|
133
|
+
<CommandMenuInput
|
|
134
|
+
placeholder={t("command.searchPlaceholder")}
|
|
135
|
+
/>
|
|
136
|
+
<CommandMenuList maxHeight="500px">
|
|
137
|
+
<CommandPaletteList commands={commands} onRun={run} t={t} />
|
|
138
|
+
</CommandMenuList>
|
|
139
|
+
</CommandMenuContent>
|
|
140
|
+
</CommandMenu>
|
|
141
|
+
<CreatePRDDialog open={prdDialogOpen} onOpenChange={setPrdDialogOpen} />
|
|
142
|
+
</>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function resolveShortcut(shortcut: string[]): string {
|
|
147
|
+
const isMac = typeof navigator !== "undefined"
|
|
148
|
+
&& (navigator.platform.toUpperCase().includes("MAC")
|
|
149
|
+
|| navigator.userAgent.toUpperCase().includes("MAC"))
|
|
150
|
+
return shortcut.map((k) => {
|
|
151
|
+
const upper = k.toUpperCase()
|
|
152
|
+
if (upper === "MOD") return isMac ? "CMD" : "CTRL"
|
|
153
|
+
return upper
|
|
154
|
+
}).join("+")
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function CommandPaletteList({
|
|
158
|
+
commands,
|
|
159
|
+
onRun,
|
|
160
|
+
t,
|
|
161
|
+
}: {
|
|
162
|
+
commands: ResolvedCommand[]
|
|
163
|
+
onRun: (cmd: ResolvedCommand) => void
|
|
164
|
+
t: (key: string) => string
|
|
165
|
+
}) {
|
|
166
|
+
const { value } = useCommandMenu()
|
|
167
|
+
|
|
126
168
|
const navCommands = commands.filter((c) => c.group === "navigation")
|
|
127
169
|
const actionCommands = commands.filter((c) => c.group === "action")
|
|
128
170
|
|
|
171
|
+
const filterCommands = (cmds: ResolvedCommand[]): ResolvedCommand[] => {
|
|
172
|
+
if (!value) return cmds
|
|
173
|
+
const lower = value.toLowerCase()
|
|
174
|
+
return cmds.filter(
|
|
175
|
+
(c) =>
|
|
176
|
+
c.title.toLowerCase().includes(lower) ||
|
|
177
|
+
(c.keywords ?? []).some((k) => k.toLowerCase().includes(lower)),
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const filteredNav = filterCommands(navCommands)
|
|
182
|
+
const filteredAction = filterCommands(actionCommands)
|
|
183
|
+
|
|
184
|
+
if (filteredNav.length === 0 && filteredAction.length === 0) {
|
|
185
|
+
return <CommandMenuEmpty>{t("command.empty")}</CommandMenuEmpty>
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let index = 0
|
|
189
|
+
|
|
129
190
|
const renderItem = (cmd: ResolvedCommand) => {
|
|
130
191
|
const Icon = cmd.icon ? PALETTE_ICON_MAP[cmd.icon] : undefined
|
|
192
|
+
const idx = index++
|
|
131
193
|
return (
|
|
132
|
-
<
|
|
194
|
+
<CommandMenuItem
|
|
133
195
|
key={cmd.id}
|
|
134
|
-
|
|
135
|
-
onSelect={() =>
|
|
196
|
+
index={idx}
|
|
197
|
+
onSelect={() => onRun(cmd)}
|
|
198
|
+
shortcut={cmd.shortcut ? resolveShortcut(cmd.shortcut) : undefined}
|
|
199
|
+
icon={Icon && <Icon className="size-4 shrink-0" />}
|
|
136
200
|
>
|
|
137
|
-
{Icon && <Icon className="size-4 shrink-0" />}
|
|
138
201
|
<span className="truncate">{cmd.title}</span>
|
|
139
|
-
|
|
140
|
-
<CommandShortcut>{cmd.shortcut.join(" → ")}</CommandShortcut>
|
|
141
|
-
)}
|
|
142
|
-
</CommandItem>
|
|
202
|
+
</CommandMenuItem>
|
|
143
203
|
)
|
|
144
204
|
}
|
|
145
205
|
|
|
146
206
|
return (
|
|
147
207
|
<>
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
<CommandList className="max-h-100">
|
|
159
|
-
<CommandEmpty>{t("command.empty")}</CommandEmpty>
|
|
160
|
-
<CommandGroup heading={t("command.groupNavigation")}>
|
|
161
|
-
{navCommands.map(renderItem)}
|
|
162
|
-
</CommandGroup>
|
|
163
|
-
<CommandGroup heading={t("command.groupAction")}>
|
|
164
|
-
{actionCommands.map(renderItem)}
|
|
165
|
-
</CommandGroup>
|
|
166
|
-
</CommandList>
|
|
167
|
-
</Command>
|
|
168
|
-
</CommandDialog>
|
|
169
|
-
<CreatePRDDialog open={prdDialogOpen} onOpenChange={setPrdDialogOpen} />
|
|
208
|
+
{filteredNav.length > 0 && (
|
|
209
|
+
<CommandMenuGroup heading={t("command.groupNavigation")}>
|
|
210
|
+
{filteredNav.map(renderItem)}
|
|
211
|
+
</CommandMenuGroup>
|
|
212
|
+
)}
|
|
213
|
+
{filteredAction.length > 0 && (
|
|
214
|
+
<CommandMenuGroup heading={t("command.groupAction")}>
|
|
215
|
+
{filteredAction.map(renderItem)}
|
|
216
|
+
</CommandMenuGroup>
|
|
217
|
+
)}
|
|
170
218
|
</>
|
|
171
219
|
)
|
|
172
220
|
}
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
4
|
+
import * as VisuallyHidden from "@radix-ui/react-visually-hidden";
|
|
5
|
+
import { Search, X, Command, Option, ArrowBigUp, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Delete, Space } from "lucide-react";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
import { cn } from "@/lib/utils";
|
|
8
|
+
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
9
|
+
|
|
10
|
+
interface CommandMenuContextType {
|
|
11
|
+
value: string;
|
|
12
|
+
setValue: (value: string) => void;
|
|
13
|
+
selectedIndex: number;
|
|
14
|
+
setSelectedIndex: (index: number) => void;
|
|
15
|
+
scrollType?: "auto" | "always" | "scroll" | "hover";
|
|
16
|
+
scrollHideDelay?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const CommandMenuContext = React.createContext<
|
|
20
|
+
CommandMenuContextType | undefined
|
|
21
|
+
>(undefined);
|
|
22
|
+
|
|
23
|
+
const CommandMenuProvider: React.FC<{
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
value: string;
|
|
26
|
+
setValue: (value: string) => void;
|
|
27
|
+
selectedIndex: number;
|
|
28
|
+
setSelectedIndex: (index: number) => void;
|
|
29
|
+
scrollType?: "auto" | "always" | "scroll" | "hover";
|
|
30
|
+
scrollHideDelay?: number;
|
|
31
|
+
}> = ({
|
|
32
|
+
children,
|
|
33
|
+
value,
|
|
34
|
+
setValue,
|
|
35
|
+
selectedIndex,
|
|
36
|
+
setSelectedIndex,
|
|
37
|
+
scrollType,
|
|
38
|
+
scrollHideDelay,
|
|
39
|
+
}) => (
|
|
40
|
+
<CommandMenuContext.Provider
|
|
41
|
+
value={{
|
|
42
|
+
value,
|
|
43
|
+
setValue,
|
|
44
|
+
selectedIndex,
|
|
45
|
+
setSelectedIndex,
|
|
46
|
+
scrollType,
|
|
47
|
+
scrollHideDelay,
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
{children}
|
|
51
|
+
</CommandMenuContext.Provider>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const useCommandMenu = () => {
|
|
55
|
+
const context = React.useContext(CommandMenuContext);
|
|
56
|
+
if (!context)
|
|
57
|
+
throw new Error("useCommandMenu must be used within CommandMenuProvider");
|
|
58
|
+
return context;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const CommandMenu = DialogPrimitive.Root;
|
|
62
|
+
const CommandMenuTrigger = DialogPrimitive.Trigger;
|
|
63
|
+
const CommandMenuPortal = DialogPrimitive.Portal;
|
|
64
|
+
const CommandMenuClose = DialogPrimitive.Close;
|
|
65
|
+
|
|
66
|
+
const CommandMenuTitle = React.forwardRef<
|
|
67
|
+
React.ComponentRef<typeof DialogPrimitive.Title>,
|
|
68
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
|
69
|
+
>(({ className, ...props }, ref) => (
|
|
70
|
+
<DialogPrimitive.Title
|
|
71
|
+
className={cn(
|
|
72
|
+
"font-semibold text-foreground text-lg leading-none tracking-tight",
|
|
73
|
+
className
|
|
74
|
+
)}
|
|
75
|
+
ref={ref}
|
|
76
|
+
{...props}
|
|
77
|
+
/>
|
|
78
|
+
));
|
|
79
|
+
CommandMenuTitle.displayName = "CommandMenuTitle";
|
|
80
|
+
|
|
81
|
+
const CommandMenuDescription = React.forwardRef<
|
|
82
|
+
React.ComponentRef<typeof DialogPrimitive.Description>,
|
|
83
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
|
84
|
+
>(({ className, ...props }, ref) => (
|
|
85
|
+
<DialogPrimitive.Description
|
|
86
|
+
className={cn("text-muted-foreground text-sm", className)}
|
|
87
|
+
ref={ref}
|
|
88
|
+
{...props}
|
|
89
|
+
/>
|
|
90
|
+
));
|
|
91
|
+
CommandMenuDescription.displayName = "CommandMenuDescription";
|
|
92
|
+
|
|
93
|
+
const CommandMenuOverlay = React.forwardRef<
|
|
94
|
+
React.ComponentRef<typeof DialogPrimitive.Overlay>,
|
|
95
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
|
96
|
+
>(({ className, ...props }, ref) => (
|
|
97
|
+
<DialogPrimitive.Overlay
|
|
98
|
+
className={cn(
|
|
99
|
+
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-[state=closed]:animate-out data-[state=open]:animate-in",
|
|
100
|
+
className
|
|
101
|
+
)}
|
|
102
|
+
ref={ref}
|
|
103
|
+
{...props}
|
|
104
|
+
/>
|
|
105
|
+
));
|
|
106
|
+
CommandMenuOverlay.displayName = "CommandMenuOverlay";
|
|
107
|
+
|
|
108
|
+
const CommandMenuContent = React.forwardRef<
|
|
109
|
+
React.ComponentRef<typeof DialogPrimitive.Content>,
|
|
110
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
|
111
|
+
showShortcut?: boolean;
|
|
112
|
+
scrollType?: "auto" | "always" | "scroll" | "hover";
|
|
113
|
+
scrollHideDelay?: number;
|
|
114
|
+
}
|
|
115
|
+
>(
|
|
116
|
+
(
|
|
117
|
+
{
|
|
118
|
+
className,
|
|
119
|
+
children,
|
|
120
|
+
showShortcut = true,
|
|
121
|
+
scrollType = "hover",
|
|
122
|
+
scrollHideDelay = 600,
|
|
123
|
+
...props
|
|
124
|
+
},
|
|
125
|
+
ref
|
|
126
|
+
) => {
|
|
127
|
+
const [value, setValue] = React.useState("");
|
|
128
|
+
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
129
|
+
|
|
130
|
+
React.useEffect(() => {
|
|
131
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
132
|
+
if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Enter") {
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
137
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
138
|
+
}, []);
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<CommandMenuPortal>
|
|
142
|
+
<CommandMenuOverlay />
|
|
143
|
+
<DialogPrimitive.Content asChild ref={ref} {...props}>
|
|
144
|
+
<div
|
|
145
|
+
className={cn(
|
|
146
|
+
"fixed top-[30%] left-1/2 z-50 w-auto max-w-none -translate-x-1/2 -translate-y-1/2",
|
|
147
|
+
"rounded-xl border border-border bg-background",
|
|
148
|
+
"overflow-hidden",
|
|
149
|
+
"motion-safe:duration-200 motion-reduce:animate-none",
|
|
150
|
+
className
|
|
151
|
+
)}
|
|
152
|
+
>
|
|
153
|
+
<CommandMenuProvider
|
|
154
|
+
scrollHideDelay={scrollHideDelay}
|
|
155
|
+
scrollType={scrollType}
|
|
156
|
+
selectedIndex={selectedIndex}
|
|
157
|
+
setSelectedIndex={setSelectedIndex}
|
|
158
|
+
setValue={setValue}
|
|
159
|
+
value={value}
|
|
160
|
+
>
|
|
161
|
+
<VisuallyHidden.Root>
|
|
162
|
+
<CommandMenuTitle>Command Menu</CommandMenuTitle>
|
|
163
|
+
</VisuallyHidden.Root>
|
|
164
|
+
{children}
|
|
165
|
+
<CommandMenuClose className="absolute top-3 right-3 rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
|
|
166
|
+
<X size={14} />
|
|
167
|
+
<span className="sr-only">Close</span>
|
|
168
|
+
</CommandMenuClose>
|
|
169
|
+
{showShortcut && (
|
|
170
|
+
<div className="absolute top-3 right-12 flex h-6 items-center justify-center gap-1">
|
|
171
|
+
<kbd
|
|
172
|
+
data-slot="kbd"
|
|
173
|
+
className="inline-flex items-center justify-center rounded px-1.5 py-1 font-mono text-[10px] font-medium text-muted-foreground bg-muted h-[22px] min-w-[22px] leading-none"
|
|
174
|
+
>
|
|
175
|
+
<Command size={10} />
|
|
176
|
+
</kbd>
|
|
177
|
+
<kbd
|
|
178
|
+
data-slot="kbd"
|
|
179
|
+
className="inline-flex items-center justify-center rounded px-1.5 py-1 font-mono text-[10px] font-medium text-muted-foreground bg-muted h-[22px] min-w-[22px] leading-none"
|
|
180
|
+
>
|
|
181
|
+
<span className="text-xs font-medium">K</span>
|
|
182
|
+
</kbd>
|
|
183
|
+
<span className="text-xs text-muted-foreground">/</span>
|
|
184
|
+
<kbd
|
|
185
|
+
data-slot="kbd"
|
|
186
|
+
className="inline-flex items-center justify-center rounded px-1.5 py-1 font-mono text-[10px] font-medium text-muted-foreground bg-muted h-[22px] min-w-[22px] leading-none"
|
|
187
|
+
>
|
|
188
|
+
<span className="text-xs font-medium">ESC</span>
|
|
189
|
+
</kbd>
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
</CommandMenuProvider>
|
|
193
|
+
</div>
|
|
194
|
+
</DialogPrimitive.Content>
|
|
195
|
+
</CommandMenuPortal>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
CommandMenuContent.displayName = "CommandMenuContent";
|
|
200
|
+
|
|
201
|
+
const CommandMenuInput = React.forwardRef<
|
|
202
|
+
HTMLInputElement,
|
|
203
|
+
React.InputHTMLAttributes<HTMLInputElement> & { placeholder?: string }
|
|
204
|
+
>(
|
|
205
|
+
(
|
|
206
|
+
{ className, placeholder = "Type a command or search...", ...props },
|
|
207
|
+
ref
|
|
208
|
+
) => {
|
|
209
|
+
const { value, setValue } = useCommandMenu();
|
|
210
|
+
return (
|
|
211
|
+
<div className="flex items-center gap-2 border-border border-b px-3 py-0">
|
|
212
|
+
<Search className="size-4 shrink-0 text-muted-foreground" />
|
|
213
|
+
<input
|
|
214
|
+
className={cn(
|
|
215
|
+
"h-12 w-full rounded-none border-0 bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
|
216
|
+
className
|
|
217
|
+
)}
|
|
218
|
+
onChange={(e) => setValue(e.target.value)}
|
|
219
|
+
placeholder={placeholder}
|
|
220
|
+
ref={ref}
|
|
221
|
+
value={value}
|
|
222
|
+
{...props}
|
|
223
|
+
/>
|
|
224
|
+
</div>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
CommandMenuInput.displayName = "CommandMenuInput";
|
|
229
|
+
|
|
230
|
+
const CommandMenuList = React.forwardRef<
|
|
231
|
+
HTMLDivElement,
|
|
232
|
+
React.HTMLAttributes<HTMLDivElement> & { maxHeight?: string }
|
|
233
|
+
>(({ className, children, maxHeight = "300px", ...props }, ref) => {
|
|
234
|
+
const {
|
|
235
|
+
selectedIndex,
|
|
236
|
+
setSelectedIndex,
|
|
237
|
+
scrollType = "hover",
|
|
238
|
+
scrollHideDelay = 600,
|
|
239
|
+
} = useCommandMenu();
|
|
240
|
+
|
|
241
|
+
React.useEffect(() => {
|
|
242
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
243
|
+
const items = document.querySelectorAll("[data-command-item]");
|
|
244
|
+
const maxIndex = items.length - 1;
|
|
245
|
+
if (e.key === "ArrowDown") {
|
|
246
|
+
e.preventDefault();
|
|
247
|
+
const newIndex = Math.min(selectedIndex + 1, maxIndex);
|
|
248
|
+
setSelectedIndex(newIndex);
|
|
249
|
+
(items[newIndex] as HTMLElement | undefined)?.scrollIntoView({
|
|
250
|
+
block: "nearest",
|
|
251
|
+
behavior: "smooth",
|
|
252
|
+
});
|
|
253
|
+
} else if (e.key === "ArrowUp") {
|
|
254
|
+
e.preventDefault();
|
|
255
|
+
const newIndex = Math.max(selectedIndex - 1, 0);
|
|
256
|
+
setSelectedIndex(newIndex);
|
|
257
|
+
(items[newIndex] as HTMLElement | undefined)?.scrollIntoView({
|
|
258
|
+
block: "nearest",
|
|
259
|
+
behavior: "smooth",
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
264
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
265
|
+
}, [selectedIndex, setSelectedIndex]);
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<div className="p-1" ref={ref} {...props}>
|
|
269
|
+
<ScrollArea
|
|
270
|
+
className={cn("w-full", className)}
|
|
271
|
+
scrollHideDelay={scrollHideDelay}
|
|
272
|
+
style={{ height: maxHeight }}
|
|
273
|
+
type={scrollType}
|
|
274
|
+
>
|
|
275
|
+
<div className="flex flex-col gap-0.5 p-0.5">{children}</div>
|
|
276
|
+
</ScrollArea>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
CommandMenuList.displayName = "CommandMenuList";
|
|
281
|
+
|
|
282
|
+
const CommandMenuGroup = React.forwardRef<
|
|
283
|
+
HTMLDivElement,
|
|
284
|
+
React.HTMLAttributes<HTMLDivElement> & { heading?: string }
|
|
285
|
+
>(({ className, children, heading, ...props }, ref) => (
|
|
286
|
+
<div className={cn("", className)} ref={ref} {...props}>
|
|
287
|
+
{heading && (
|
|
288
|
+
<div className="px-2 py-1.5 font-medium text-muted-foreground text-xs uppercase tracking-wider">
|
|
289
|
+
{heading}
|
|
290
|
+
</div>
|
|
291
|
+
)}
|
|
292
|
+
{children}
|
|
293
|
+
</div>
|
|
294
|
+
));
|
|
295
|
+
CommandMenuGroup.displayName = "CommandMenuGroup";
|
|
296
|
+
|
|
297
|
+
const CommandMenuItem = React.forwardRef<
|
|
298
|
+
HTMLDivElement,
|
|
299
|
+
React.HTMLAttributes<HTMLDivElement> & {
|
|
300
|
+
onSelect?: () => void;
|
|
301
|
+
disabled?: boolean;
|
|
302
|
+
shortcut?: string;
|
|
303
|
+
icon?: React.ReactNode;
|
|
304
|
+
index?: number;
|
|
305
|
+
}
|
|
306
|
+
>(
|
|
307
|
+
(
|
|
308
|
+
{
|
|
309
|
+
className,
|
|
310
|
+
children,
|
|
311
|
+
onSelect,
|
|
312
|
+
disabled = false,
|
|
313
|
+
shortcut,
|
|
314
|
+
icon,
|
|
315
|
+
index = 0,
|
|
316
|
+
...props
|
|
317
|
+
},
|
|
318
|
+
ref
|
|
319
|
+
) => {
|
|
320
|
+
const { selectedIndex, setSelectedIndex } = useCommandMenu();
|
|
321
|
+
const isSelected = selectedIndex === index;
|
|
322
|
+
|
|
323
|
+
const handleSelect = React.useCallback(() => {
|
|
324
|
+
if (!disabled && onSelect) onSelect();
|
|
325
|
+
}, [disabled, onSelect]);
|
|
326
|
+
|
|
327
|
+
React.useEffect(() => {
|
|
328
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
329
|
+
if (e.key === "Enter" && isSelected) {
|
|
330
|
+
e.preventDefault();
|
|
331
|
+
handleSelect();
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
335
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
336
|
+
}, [isSelected, handleSelect]);
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
<div
|
|
340
|
+
className={cn(
|
|
341
|
+
"relative flex cursor-default select-none items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none transition-colors",
|
|
342
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
343
|
+
isSelected && "bg-accent text-accent-foreground",
|
|
344
|
+
disabled && "pointer-events-none opacity-50",
|
|
345
|
+
className
|
|
346
|
+
)}
|
|
347
|
+
data-command-item
|
|
348
|
+
onClick={handleSelect}
|
|
349
|
+
onMouseEnter={() => setSelectedIndex(index)}
|
|
350
|
+
ref={ref}
|
|
351
|
+
{...props}
|
|
352
|
+
>
|
|
353
|
+
{icon && (
|
|
354
|
+
<div className="flex size-4 items-center justify-center">{icon}</div>
|
|
355
|
+
)}
|
|
356
|
+
<div className="flex-1">{children}</div>
|
|
357
|
+
{shortcut && (
|
|
358
|
+
<div className="ml-auto flex items-center gap-1">
|
|
359
|
+
{shortcut.split("+").map((key, i) => (
|
|
360
|
+
<React.Fragment key={`${key}-${i}`}>
|
|
361
|
+
{i > 0 && (
|
|
362
|
+
<span className="text-muted-foreground text-xs">+</span>
|
|
363
|
+
)}
|
|
364
|
+
<kbd
|
|
365
|
+
data-slot="kbd"
|
|
366
|
+
className={cn(
|
|
367
|
+
"inline-flex items-center justify-center rounded px-1.5 py-1 font-mono text-[10px] font-medium text-muted-foreground bg-muted h-[22px] min-w-[22px] leading-none"
|
|
368
|
+
)}
|
|
369
|
+
>
|
|
370
|
+
{key === "CMD" || key === "⌘" ? (
|
|
371
|
+
<Command size={10} />
|
|
372
|
+
) : key === "OPTION" || key === "ALT" ? (
|
|
373
|
+
<Option size={14} />
|
|
374
|
+
) : key === "SHIFT" ? (
|
|
375
|
+
<ArrowBigUp size={13} />
|
|
376
|
+
) : key === "CTRL" ? (
|
|
377
|
+
<span className="text-xs font-medium">CTRL</span>
|
|
378
|
+
) : key === "ESC" || key === "ESCAPE" ? (
|
|
379
|
+
<span className="text-xs font-medium">ESC</span>
|
|
380
|
+
) : key === "DELETE" || key === "BACKSPACE" ? (
|
|
381
|
+
<Delete size={14} />
|
|
382
|
+
) : key === "ARROWUP" ? (
|
|
383
|
+
<ArrowUp size={14} />
|
|
384
|
+
) : key === "ARROWDOWN" ? (
|
|
385
|
+
<ArrowDown size={14} />
|
|
386
|
+
) : key === "ARROWLEFT" ? (
|
|
387
|
+
<ArrowLeft size={14} />
|
|
388
|
+
) : key === "ARROWRIGHT" ? (
|
|
389
|
+
<ArrowRight size={14} />
|
|
390
|
+
) : key === "SPACE" ? (
|
|
391
|
+
<Space size={14} />
|
|
392
|
+
) : (
|
|
393
|
+
<span className="text-xs font-medium">{key}</span>
|
|
394
|
+
)}
|
|
395
|
+
</kbd>
|
|
396
|
+
</React.Fragment>
|
|
397
|
+
))}
|
|
398
|
+
</div>
|
|
399
|
+
)}
|
|
400
|
+
</div>
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
);
|
|
404
|
+
CommandMenuItem.displayName = "CommandMenuItem";
|
|
405
|
+
|
|
406
|
+
const CommandMenuSeparator = React.forwardRef<
|
|
407
|
+
HTMLDivElement,
|
|
408
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
409
|
+
>(({ className, ...props }, ref) => (
|
|
410
|
+
<div
|
|
411
|
+
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
|
412
|
+
ref={ref}
|
|
413
|
+
{...props}
|
|
414
|
+
/>
|
|
415
|
+
));
|
|
416
|
+
CommandMenuSeparator.displayName = "CommandMenuSeparator";
|
|
417
|
+
|
|
418
|
+
const CommandMenuEmpty = React.forwardRef<
|
|
419
|
+
HTMLDivElement,
|
|
420
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
421
|
+
>(({ className, children = "No results found.", ...props }, ref) => (
|
|
422
|
+
<div
|
|
423
|
+
className={cn("py-6 text-center text-muted-foreground text-sm", className)}
|
|
424
|
+
ref={ref}
|
|
425
|
+
{...props}
|
|
426
|
+
>
|
|
427
|
+
{children}
|
|
428
|
+
</div>
|
|
429
|
+
));
|
|
430
|
+
CommandMenuEmpty.displayName = "CommandMenuEmpty";
|
|
431
|
+
|
|
432
|
+
export const useCommandMenuShortcut = (callback: () => void) => {
|
|
433
|
+
React.useEffect(() => {
|
|
434
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
435
|
+
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
|
436
|
+
e.preventDefault();
|
|
437
|
+
callback();
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
441
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
442
|
+
}, [callback]);
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
export {
|
|
446
|
+
CommandMenu,
|
|
447
|
+
CommandMenuTrigger,
|
|
448
|
+
CommandMenuContent,
|
|
449
|
+
CommandMenuTitle,
|
|
450
|
+
CommandMenuDescription,
|
|
451
|
+
CommandMenuInput,
|
|
452
|
+
CommandMenuList,
|
|
453
|
+
CommandMenuEmpty,
|
|
454
|
+
CommandMenuGroup,
|
|
455
|
+
CommandMenuItem,
|
|
456
|
+
CommandMenuSeparator,
|
|
457
|
+
CommandMenuClose,
|
|
458
|
+
CommandMenuProvider,
|
|
459
|
+
useCommandMenu,
|
|
460
|
+
};
|
|
@@ -16,11 +16,16 @@
|
|
|
16
16
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
import { useTranslation } from "react-i18next"
|
|
19
|
-
import { Search } from "lucide-react"
|
|
19
|
+
import { Search, Command } from "lucide-react"
|
|
20
20
|
import { useSidebar } from "@/components/ui/sidebar"
|
|
21
21
|
import { cn } from "@/lib/utils"
|
|
22
22
|
import { setCommandPaletteOpen } from "@/lib/commands/command-palette-state"
|
|
23
23
|
|
|
24
|
+
const isMac =
|
|
25
|
+
typeof navigator !== "undefined" &&
|
|
26
|
+
(navigator.platform.toUpperCase().includes("MAC") ||
|
|
27
|
+
navigator.userAgent.toUpperCase().includes("MAC"))
|
|
28
|
+
|
|
24
29
|
export function SidebarSearchTrigger() {
|
|
25
30
|
const { t } = useTranslation("common")
|
|
26
31
|
const { state } = useSidebar()
|
|
@@ -50,14 +55,23 @@ export function SidebarSearchTrigger() {
|
|
|
50
55
|
)}
|
|
51
56
|
>
|
|
52
57
|
<Search className="size-4 shrink-0" />
|
|
53
|
-
<span className="flex-1 truncate text-left">
|
|
58
|
+
<span className="flex-1 truncate text-left text-xs">
|
|
54
59
|
{t("command.searchPlaceholder")}
|
|
55
60
|
</span>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
<span className="inline-flex items-center gap-1 rounded border border-border bg-muted/60 px-1.5 py-0.5">
|
|
62
|
+
<kbd
|
|
63
|
+
data-slot="kbd"
|
|
64
|
+
className="inline-flex items-center justify-center"
|
|
65
|
+
>
|
|
66
|
+
{isMac ? <Command size={10} /> : <span className="text-[10px] font-medium text-muted-foreground">Ctrl</span>}
|
|
67
|
+
</kbd>
|
|
68
|
+
<kbd
|
|
69
|
+
data-slot="kbd"
|
|
70
|
+
className="inline-flex items-center justify-center"
|
|
71
|
+
>
|
|
72
|
+
<span className="text-[11px] font-medium text-muted-foreground">K</span>
|
|
73
|
+
</kbd>
|
|
74
|
+
</span>
|
|
61
75
|
</button>
|
|
62
76
|
)
|
|
63
77
|
}
|
|
@@ -38,8 +38,8 @@ export const COMMAND_REGISTRY: CommandDefinition[] = [
|
|
|
38
38
|
{ id: "create-prd", titleKey: "command.createPrd", icon: "file", group: "action", action: { kind: "callback", id: "create-prd" }, keywords: ["新建", "创建", "PRD", "xj"], shortcut: ["N", "P"] },
|
|
39
39
|
{ id: "create-conversation", titleKey: "command.createConversation", icon: "message", group: "action", action: { kind: "callback", id: "create-conversation" }, keywords: ["新建对话", "新对话", "xjdh"], shortcut: ["N", "C"] },
|
|
40
40
|
{ id: "create-story", titleKey: "command.createStory", icon: "clipboardList", group: "action", action: { kind: "callback", id: "create-story" }, keywords: ["新建需求", "xjxq"], shortcut: ["N", "S"] },
|
|
41
|
-
{ id: "toggle-theme", titleKey: "command.toggleTheme", icon: "
|
|
42
|
-
{ id: "sign-out", titleKey: "command.signOut", icon: "
|
|
41
|
+
{ id: "toggle-theme", titleKey: "command.toggleTheme", icon: "sunMoon", group: "action", action: { kind: "callback", id: "toggle-theme" }, keywords: ["切换主题", "暗黑", "主题", "qhzt"], shortcut: ["Mod", "Shift", "L"] },
|
|
42
|
+
{ id: "sign-out", titleKey: "command.signOut", icon: "logOut", group: "action", action: { kind: "callback", id: "sign-out" }, keywords: ["退出登录", "登出", "tcdl"] },
|
|
43
43
|
] as const
|
|
44
44
|
|
|
45
45
|
export function getSequencePairs(registry: CommandDefinition[]): string[] {
|
|
@@ -20,7 +20,7 @@ import Sidebar03 from '@/components/layout/sidebar-03'
|
|
|
20
20
|
import { GlobalChatPanel } from '@/components/ai/global-chat-panel'
|
|
21
21
|
import { TimezoneBootstrap } from '@/components/shared/timezone-bootstrap'
|
|
22
22
|
import { SubscriptionProvider } from '@/components/billing/subscription-provider'
|
|
23
|
-
import { CommandPalette } from '@/components/command-palette'
|
|
23
|
+
import { CommandPalette } from '@/components/command/command-palette'
|
|
24
24
|
import type { Session } from '@/lib/auth/auth-client'
|
|
25
25
|
|
|
26
26
|
export const Route = createLazyFileRoute('/_nav')({
|