@allbluecn/web-app 0.4.9 → 0.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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
+ };
@@ -39,7 +39,8 @@ import { Badge } from "@/components/ui/badge"
39
39
  import { workspaceRoutes } from "@/config/sidebar-routes";
40
40
  import { notifications } from "@/config/sidebar-notifications";
41
41
  import type { Route } from "@/config/sidebar-routes";
42
- import { SidebarSearchTrigger } from "@/components/layout/sidebar-03/sidebar-search-trigger"
42
+ import { SidebarSearchTrigger } from "@/components/layout/sidebar-03/nav-search-trigger"
43
+ import { NavSecondary } from "@/components/layout/sidebar-03/nav-secondary"
43
44
  import { PluginNavItems } from '@/components/plugin-slots/plugin-nav-items'
44
45
 
45
46
  export function DashboardSidebar() {
@@ -101,6 +102,7 @@ export function DashboardSidebar() {
101
102
  <NavAnyHub />
102
103
  </SidebarContent>
103
104
  <SidebarFooter className="gap-2 px-2 pb-3">
105
+ <NavSecondary />
104
106
  <SidebarSearchTrigger />
105
107
  <UserMenu />
106
108
  </SidebarFooter>
@@ -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
- <span className="inline-flex items-center gap-0.5">
57
- <kbd className="rounded border border-border bg-muted/60 px-1 py-0.5 text-[10px] font-medium text-muted-foreground">
58
- ⌘K
59
- </kbd>
60
- </span>
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
  }
@@ -0,0 +1,93 @@
1
+ import {
2
+ SidebarMenu,
3
+ SidebarMenuButton,
4
+ SidebarMenuItem,
5
+ useSidebar,
6
+ } from "@/components/ui/sidebar"
7
+ import { BookOpen, ScrollText } from "lucide-react"
8
+ import { cn } from "@/lib/utils"
9
+ import { Link } from "@tanstack/react-router"
10
+ import { APP_VERSION_DISPLAY } from "@/lib/app-version"
11
+
12
+ const items = [
13
+ {
14
+ title: "使用文档",
15
+ Icon: BookOpen,
16
+ href: "/docs",
17
+ tooltip: "使用文档",
18
+ external: true,
19
+ },
20
+ {
21
+ title: "开发者日志",
22
+ Icon: ScrollText,
23
+ href: "/changelog",
24
+ tooltip: "开发者日志",
25
+ badge: APP_VERSION_DISPLAY,
26
+ },
27
+ ]
28
+
29
+ export function NavSecondary() {
30
+ const { state } = useSidebar()
31
+ const isCollapsed = state === "collapsed"
32
+
33
+ return (
34
+ <SidebarMenu className={cn("space-y-1", isCollapsed && "px-1")}>
35
+ {!isCollapsed && (
36
+ <div className="px-2 text-xs font-medium text-muted-foreground/70">
37
+ {/* 资源 */}
38
+ </div>
39
+ )}
40
+ {items.map((item) => (
41
+ <SidebarMenuItem key={item.title}>
42
+ <SidebarMenuButton
43
+ asChild
44
+ tooltip={item.tooltip}
45
+ className={cn(
46
+ "flex items-center rounded-lg px-2",
47
+ isCollapsed && "justify-center"
48
+ )}
49
+ >
50
+ {item.external ? (
51
+ <a
52
+ href={item.href}
53
+ className={cn(
54
+ "flex w-full items-center gap-3 text-muted-foreground hover:text-foreground",
55
+ isCollapsed && "justify-center -ml-1"
56
+ )}
57
+ target="_blank"
58
+ rel="noopener noreferrer"
59
+ style={{ width: "100%", height: "100%" }}
60
+ >
61
+ <item.Icon className={cn("text-muted-foreground", !isCollapsed && "size-3.5!")} />
62
+ {!isCollapsed && (
63
+ <span className="text-xs text-muted-foreground">{item.title}</span>
64
+ )}
65
+ </a>
66
+ ) : (
67
+ <Link
68
+ to={item.href}
69
+ className={cn(
70
+ "flex w-full items-center justify-between gap-3 text-muted-foreground hover:text-foreground",
71
+ isCollapsed && "justify-center -ml-1"
72
+ )}
73
+ style={{ width: "100%", height: "100%" }}
74
+ >
75
+ <div className="flex items-center gap-3">
76
+ <item.Icon className={cn("text-muted-foreground", !isCollapsed && "size-3.5!")} />
77
+ {!isCollapsed && (
78
+ <span className="text-xs text-muted-foreground">{item.title}</span>
79
+ )}
80
+ </div>
81
+ {!isCollapsed && item.badge && (
82
+ <code className="text-[10px] font-mono text-muted-foreground/60 bg-muted/50 px-1.5 py-0.5 rounded shrink-0">
83
+ {item.badge}
84
+ </code>
85
+ )}
86
+ </Link>
87
+ )}
88
+ </SidebarMenuButton>
89
+ </SidebarMenuItem>
90
+ ))}
91
+ </SidebarMenu>
92
+ )
93
+ }
@@ -0,0 +1,20 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ export const APP_VERSION = '0.5.0'
19
+ export const APP_VERSION_DISPLAY = `v${APP_VERSION}`
20
+ export const APP_FULL_VERSION = `AllBlue v${APP_VERSION}`
@@ -0,0 +1,14 @@
1
+ export interface SdkVersion {
2
+ name: string
3
+ version: string
4
+ lastUpdate: string
5
+ source: string
6
+ docsUrl: string | null
7
+ }
8
+
9
+ export interface SdkGroup {
10
+ id: 'tanstack' | 'allblue' | 'core'
11
+ label: string
12
+ icon: string
13
+ items: SdkVersion[]
14
+ }