wiki-viewer 1.0.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.
Files changed (119) hide show
  1. package/.next/standalone/.github/workflows/publish.yml +37 -0
  2. package/.next/standalone/DESIGN.md +521 -0
  3. package/.next/standalone/README.md +283 -0
  4. package/.next/standalone/bin/wiki-viewer.js +158 -0
  5. package/.next/standalone/next.config.ts +11 -0
  6. package/.next/standalone/package.json +120 -0
  7. package/.next/standalone/pnpm-lock.yaml +5654 -0
  8. package/.next/standalone/pnpm-workspace.yaml +2 -0
  9. package/.next/standalone/postcss.config.mjs +2 -0
  10. package/.next/standalone/public/logo.svg +11 -0
  11. package/.next/standalone/public/public/logo.svg +11 -0
  12. package/.next/standalone/server.js +44 -0
  13. package/.next/standalone/src/app/api/app-proxy/[...path]/route.ts +247 -0
  14. package/.next/standalone/src/app/api/assets/[...path]/route.ts +70 -0
  15. package/.next/standalone/src/app/api/system/browse/route.ts +78 -0
  16. package/.next/standalone/src/app/api/system/clear-root/route.ts +9 -0
  17. package/.next/standalone/src/app/api/system/config/route.ts +10 -0
  18. package/.next/standalone/src/app/api/system/pins/route.ts +31 -0
  19. package/.next/standalone/src/app/api/system/reveal/route.ts +28 -0
  20. package/.next/standalone/src/app/api/system/root-status/route.ts +9 -0
  21. package/.next/standalone/src/app/api/system/set-root/route.ts +23 -0
  22. package/.next/standalone/src/app/api/upload/[...path]/route.ts +80 -0
  23. package/.next/standalone/src/app/api/wiki/app/route.ts +41 -0
  24. package/.next/standalone/src/app/api/wiki/content/route.ts +60 -0
  25. package/.next/standalone/src/app/api/wiki/file/route.ts +52 -0
  26. package/.next/standalone/src/app/api/wiki/folder/route.ts +26 -0
  27. package/.next/standalone/src/app/api/wiki/move/route.ts +45 -0
  28. package/.next/standalone/src/app/api/wiki/page/route.ts +84 -0
  29. package/.next/standalone/src/app/api/wiki/route.ts +98 -0
  30. package/.next/standalone/src/app/api/wiki/slugs/route.ts +50 -0
  31. package/.next/standalone/src/app/api/wiki/upload/route.ts +87 -0
  32. package/.next/standalone/src/app/api/wiki/watch/route.ts +86 -0
  33. package/.next/standalone/src/app/globals.css +716 -0
  34. package/.next/standalone/src/app/icon.svg +11 -0
  35. package/.next/standalone/src/app/layout.tsx +35 -0
  36. package/.next/standalone/src/app/page.tsx +1509 -0
  37. package/.next/standalone/src/components/confirm-dialog.tsx +56 -0
  38. package/.next/standalone/src/components/dir-picker.tsx +393 -0
  39. package/.next/standalone/src/components/editor/bubble-menu.tsx +286 -0
  40. package/.next/standalone/src/components/editor/callout-extension.ts +36 -0
  41. package/.next/standalone/src/components/editor/csv-viewer.tsx +335 -0
  42. package/.next/standalone/src/components/editor/editor-toolbar.tsx +564 -0
  43. package/.next/standalone/src/components/editor/editor.tsx +633 -0
  44. package/.next/standalone/src/components/editor/extensions/drag-handle.ts +325 -0
  45. package/.next/standalone/src/components/editor/extensions/heading-anchors.ts +55 -0
  46. package/.next/standalone/src/components/editor/extensions/resizable-image.tsx +159 -0
  47. package/.next/standalone/src/components/editor/extensions.ts +148 -0
  48. package/.next/standalone/src/components/editor/file-fallback-viewer.tsx +74 -0
  49. package/.next/standalone/src/components/editor/folder-index.tsx +235 -0
  50. package/.next/standalone/src/components/editor/google-doc-viewer.tsx +73 -0
  51. package/.next/standalone/src/components/editor/image-viewer.tsx +56 -0
  52. package/.next/standalone/src/components/editor/link-popover.tsx +111 -0
  53. package/.next/standalone/src/components/editor/media-popover.tsx +211 -0
  54. package/.next/standalone/src/components/editor/media-viewer.tsx +69 -0
  55. package/.next/standalone/src/components/editor/mermaid-viewer.tsx +285 -0
  56. package/.next/standalone/src/components/editor/node-app-viewer.tsx +269 -0
  57. package/.next/standalone/src/components/editor/notebook-viewer.tsx +370 -0
  58. package/.next/standalone/src/components/editor/office/docx-viewer.tsx +92 -0
  59. package/.next/standalone/src/components/editor/office/office-chrome.tsx +82 -0
  60. package/.next/standalone/src/components/editor/office/pptx-viewer.tsx +103 -0
  61. package/.next/standalone/src/components/editor/office/xlsx-viewer.tsx +116 -0
  62. package/.next/standalone/src/components/editor/pdf-viewer.tsx +31 -0
  63. package/.next/standalone/src/components/editor/slash-commands.tsx +454 -0
  64. package/.next/standalone/src/components/editor/source-viewer.tsx +212 -0
  65. package/.next/standalone/src/components/editor/table-menu.tsx +232 -0
  66. package/.next/standalone/src/components/editor/website-viewer.tsx +74 -0
  67. package/.next/standalone/src/components/editor/wiki-link-create-dialog.tsx +188 -0
  68. package/.next/standalone/src/components/editor/wiki-link-decorator.ts +79 -0
  69. package/.next/standalone/src/components/editor/wiki-link-extension.ts +120 -0
  70. package/.next/standalone/src/components/editor/wiki-link-picker.tsx +250 -0
  71. package/.next/standalone/src/components/layout/viewer-toolbar.tsx +54 -0
  72. package/.next/standalone/src/components/theme-provider.tsx +16 -0
  73. package/.next/standalone/src/components/theme-toggle.tsx +24 -0
  74. package/.next/standalone/src/components/ui/badge.tsx +35 -0
  75. package/.next/standalone/src/components/ui/button.tsx +56 -0
  76. package/.next/standalone/src/components/ui/card.tsx +63 -0
  77. package/.next/standalone/src/components/ui/collapsible.tsx +9 -0
  78. package/.next/standalone/src/components/ui/command.tsx +150 -0
  79. package/.next/standalone/src/components/ui/context-menu.tsx +191 -0
  80. package/.next/standalone/src/components/ui/dialog.tsx +119 -0
  81. package/.next/standalone/src/components/ui/dropdown-menu.tsx +194 -0
  82. package/.next/standalone/src/components/ui/input.tsx +22 -0
  83. package/.next/standalone/src/components/ui/label.tsx +25 -0
  84. package/.next/standalone/src/components/ui/popover.tsx +33 -0
  85. package/.next/standalone/src/components/ui/scroll-area.tsx +47 -0
  86. package/.next/standalone/src/components/ui/select.tsx +156 -0
  87. package/.next/standalone/src/components/ui/separator.tsx +30 -0
  88. package/.next/standalone/src/components/ui/sheet.tsx +140 -0
  89. package/.next/standalone/src/components/ui/skeleton.tsx +15 -0
  90. package/.next/standalone/src/components/ui/switch.tsx +28 -0
  91. package/.next/standalone/src/components/ui/tabs.tsx +54 -0
  92. package/.next/standalone/src/components/ui/textarea.tsx +21 -0
  93. package/.next/standalone/src/components/ui/tip.tsx +23 -0
  94. package/.next/standalone/src/components/ui/toggle-group.tsx +41 -0
  95. package/.next/standalone/src/components/ui/tooltip.tsx +27 -0
  96. package/.next/standalone/src/components/wiki/frontmatter-header.tsx +164 -0
  97. package/.next/standalone/src/lib/app-runner.ts +231 -0
  98. package/.next/standalone/src/lib/cabinets/tree.ts +75 -0
  99. package/.next/standalone/src/lib/config.ts +32 -0
  100. package/.next/standalone/src/lib/embeds/detect.ts +142 -0
  101. package/.next/standalone/src/lib/google/detect.ts +107 -0
  102. package/.next/standalone/src/lib/markdown/parse-frontmatter.ts +84 -0
  103. package/.next/standalone/src/lib/markdown/remark-wikilinks.ts +100 -0
  104. package/.next/standalone/src/lib/markdown/to-html.ts +168 -0
  105. package/.next/standalone/src/lib/markdown/to-markdown.ts +238 -0
  106. package/.next/standalone/src/lib/root-dir.ts +35 -0
  107. package/.next/standalone/src/lib/toast.ts +9 -0
  108. package/.next/standalone/src/lib/utils.ts +6 -0
  109. package/.next/standalone/src/lib/wiki-helpers.ts +36 -0
  110. package/.next/standalone/src/stores/ai-panel-store.ts +23 -0
  111. package/.next/standalone/src/stores/editor-store.ts +244 -0
  112. package/.next/standalone/src/stores/tree-store.ts +52 -0
  113. package/.next/standalone/src/stores/wiki-slugs-store.ts +74 -0
  114. package/.next/standalone/src/types/index.ts +47 -0
  115. package/.next/standalone/tailwind.config.ts +201 -0
  116. package/.next/standalone/tsconfig.json +41 -0
  117. package/README.md +283 -0
  118. package/bin/wiki-viewer.js +158 -0
  119. package/package.json +120 -0
@@ -0,0 +1,56 @@
1
+ "use client";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogFooter,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ } from "@/components/ui/dialog";
12
+
13
+ interface ConfirmDialogProps {
14
+ open: boolean;
15
+ onOpenChange: (open: boolean) => void;
16
+ title: string;
17
+ description: string;
18
+ confirmLabel?: string;
19
+ onConfirm: () => void;
20
+ variant?: "destructive" | "default";
21
+ }
22
+
23
+ export function ConfirmDialog({
24
+ open,
25
+ onOpenChange,
26
+ title,
27
+ description,
28
+ confirmLabel = "Delete",
29
+ onConfirm,
30
+ variant = "destructive",
31
+ }: ConfirmDialogProps) {
32
+ return (
33
+ <Dialog open={open} onOpenChange={onOpenChange}>
34
+ <DialogContent className="sm:max-w-sm">
35
+ <DialogHeader>
36
+ <DialogTitle>{title}</DialogTitle>
37
+ <DialogDescription>{description}</DialogDescription>
38
+ </DialogHeader>
39
+ <DialogFooter className="gap-2 sm:gap-0">
40
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
41
+ Cancel
42
+ </Button>
43
+ <Button
44
+ variant={variant}
45
+ onClick={() => {
46
+ onConfirm();
47
+ onOpenChange(false);
48
+ }}
49
+ >
50
+ {confirmLabel}
51
+ </Button>
52
+ </DialogFooter>
53
+ </DialogContent>
54
+ </Dialog>
55
+ );
56
+ }
@@ -0,0 +1,393 @@
1
+ "use client";
2
+
3
+ import {
4
+ ChevronRight,
5
+ Folder,
6
+ FolderOpen,
7
+ HardDrive,
8
+ Home,
9
+ Loader2,
10
+ Pin,
11
+ PinOff,
12
+ X,
13
+ } from "lucide-react";
14
+ import { useCallback, useEffect, useRef, useState } from "react";
15
+ import { Button } from "@/components/ui/button";
16
+ import { cn } from "@/lib/utils";
17
+
18
+ interface Entry {
19
+ name: string;
20
+ path: string;
21
+ accessible: boolean;
22
+ }
23
+
24
+ interface BrowseResult {
25
+ path: string;
26
+ parent: string | null;
27
+ entries: Entry[];
28
+ shortcuts: Array<{ label: string; path: string }>;
29
+ }
30
+
31
+ interface Props {
32
+ onSelect: (path: string) => void;
33
+ }
34
+
35
+ export function DirPicker({ onSelect }: Props) {
36
+ const [data, setData] = useState<BrowseResult | null>(null);
37
+ const [loading, setLoading] = useState(true);
38
+ const [error, setError] = useState<string | null>(null);
39
+ const [pathInput, setPathInput] = useState("");
40
+ const [selecting, setSelecting] = useState(false);
41
+ const [pins, setPins] = useState<string[]>([]);
42
+ const [pinLoading, setPinLoading] = useState(false);
43
+ const inputRef = useRef<HTMLInputElement>(null);
44
+ const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
45
+
46
+ const navigate = useCallback(async (dir: string, updateInput = true) => {
47
+ if (debounceRef.current) {
48
+ clearTimeout(debounceRef.current);
49
+ debounceRef.current = null;
50
+ }
51
+ setLoading(true);
52
+ setError(null);
53
+ try {
54
+ const res = await fetch(
55
+ `/api/system/browse?path=${encodeURIComponent(dir)}`,
56
+ );
57
+ if (!res.ok) {
58
+ const e: { error?: string } = await res.json();
59
+ setError(e.error ?? "Cannot open directory");
60
+ return;
61
+ }
62
+ const result: BrowseResult = await res.json();
63
+ setData(result);
64
+ if (updateInput) setPathInput(result.path);
65
+ } catch {
66
+ setError("Network error");
67
+ } finally {
68
+ setLoading(false);
69
+ }
70
+ }, []);
71
+
72
+ // On mount: load config (last opened + pins), then navigate
73
+ useEffect(() => {
74
+ let cancelled = false;
75
+ (async () => {
76
+ try {
77
+ const res = await fetch("/api/system/config");
78
+ if (res.ok && !cancelled) {
79
+ const cfg: { pinnedPaths: string[]; lastOpenedPath: string | null } =
80
+ await res.json();
81
+ setPins(cfg.pinnedPaths ?? []);
82
+ await navigate(cfg.lastOpenedPath ?? "");
83
+ return;
84
+ }
85
+ } catch {
86
+ /* fallthrough to default */
87
+ }
88
+ if (!cancelled) await navigate("");
89
+ })();
90
+ return () => { cancelled = true; };
91
+ // eslint-disable-next-line react-hooks/exhaustive-deps
92
+ }, []);
93
+
94
+ const handleSelect = async (dir?: string) => {
95
+ const target = dir ?? data?.path;
96
+ if (!target) return;
97
+ setSelecting(true);
98
+ try {
99
+ const res = await fetch("/api/system/set-root", {
100
+ method: "POST",
101
+ headers: { "Content-Type": "application/json" },
102
+ body: JSON.stringify({ path: target }),
103
+ });
104
+ if (!res.ok) {
105
+ const e: { error?: string } = await res.json();
106
+ setError(e.error ?? "Cannot use that directory");
107
+ return;
108
+ }
109
+ onSelect(target);
110
+ } catch {
111
+ setError("Network error");
112
+ } finally {
113
+ setSelecting(false);
114
+ }
115
+ };
116
+
117
+ const handlePathKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
118
+ if (e.key === "Enter") {
119
+ e.preventDefault();
120
+ if (debounceRef.current) {
121
+ clearTimeout(debounceRef.current);
122
+ debounceRef.current = null;
123
+ }
124
+ navigate(pathInput.trim());
125
+ }
126
+ if (e.key === "Escape") {
127
+ if (debounceRef.current) {
128
+ clearTimeout(debounceRef.current);
129
+ debounceRef.current = null;
130
+ }
131
+ setPathInput(data?.path ?? "");
132
+ inputRef.current?.blur();
133
+ }
134
+ };
135
+
136
+ const isPinned = data ? pins.includes(data.path) : false;
137
+
138
+ const togglePin = async () => {
139
+ if (!data) return;
140
+ setPinLoading(true);
141
+ try {
142
+ const action = isPinned ? "unpin" : "pin";
143
+ const res = await fetch("/api/system/pins", {
144
+ method: "POST",
145
+ headers: { "Content-Type": "application/json" },
146
+ body: JSON.stringify({ path: data.path, action }),
147
+ });
148
+ if (res.ok) {
149
+ setPins((prev) =>
150
+ action === "pin"
151
+ ? [...prev, data.path]
152
+ : prev.filter((p) => p !== data.path),
153
+ );
154
+ }
155
+ } catch {
156
+ /* ignore */
157
+ } finally {
158
+ setPinLoading(false);
159
+ }
160
+ };
161
+
162
+ const removePin = async (p: string) => {
163
+ try {
164
+ const res = await fetch("/api/system/pins", {
165
+ method: "POST",
166
+ headers: { "Content-Type": "application/json" },
167
+ body: JSON.stringify({ path: p, action: "unpin" }),
168
+ });
169
+ if (res.ok) setPins((prev) => prev.filter((x) => x !== p));
170
+ } catch {
171
+ /* ignore */
172
+ }
173
+ };
174
+
175
+ // Build breadcrumb segments from path
176
+ const breadcrumbs = (data?.path ?? "")
177
+ .split(/[/\\]/)
178
+ .filter(Boolean)
179
+ .reduce<Array<{ label: string; path: string }>>(
180
+ (acc, seg) => {
181
+ const prev = acc[acc.length - 1]?.path ?? "/";
182
+ acc.push({ label: seg, path: `${prev === "/" ? "" : prev}/${seg}` });
183
+ return acc;
184
+ },
185
+ [{ label: "/", path: "/" }],
186
+ );
187
+
188
+ return (
189
+ <div className="flex flex-1 items-center justify-center bg-background p-4">
190
+ <div className="flex w-full max-w-xl flex-col gap-4">
191
+ {/* Header */}
192
+ <div className="text-center space-y-1">
193
+ <div className="flex items-center justify-center gap-2 mb-1">
194
+ <img src="/logo.svg" alt="Wiki Viewer" className="h-8 w-8" />
195
+ <span className="text-xl font-semibold tracking-tight">Wiki Viewer</span>
196
+ </div>
197
+ <h1 className="text-xl font-medium">Choose a directory</h1>
198
+ <p className="text-sm text-muted-foreground">
199
+ Select the folder wiki-viewer should serve.
200
+ <br />
201
+ This directory lives on the server — use the browser below.
202
+ </p>
203
+ </div>
204
+
205
+ {/* Main browser card */}
206
+ <div className="rounded-lg border bg-card shadow-sm overflow-hidden">
207
+ {/* Path input bar — always visible, reactive with browser */}
208
+ <div className="flex items-center gap-2 border-b px-3 py-2 bg-muted">
209
+ <input
210
+ ref={inputRef}
211
+ className="flex-1 bg-transparent text-sm outline-none font-mono min-w-0"
212
+ value={pathInput}
213
+ onChange={(e) => {
214
+ const val = e.target.value;
215
+ setPathInput(val);
216
+ if (debounceRef.current) clearTimeout(debounceRef.current);
217
+ debounceRef.current = setTimeout(() => {
218
+ debounceRef.current = null;
219
+ navigate(val.trim(), false);
220
+ }, 500);
221
+ }}
222
+ onKeyDown={handlePathKeyDown}
223
+ spellCheck={false}
224
+ placeholder="Enter a path…"
225
+ />
226
+
227
+ </div>
228
+
229
+ {/* Breadcrumb bar (click to navigate) */}
230
+ <div className="flex items-center gap-1 border-b px-3 py-1.5 bg-muted/50 overflow-x-auto shrink-0 min-h-[32px]">
231
+ {breadcrumbs.map((seg, i) => (
232
+ <span key={seg.path} className="flex items-center gap-1 shrink-0">
233
+ {i > 0 && (
234
+ <ChevronRight className="h-3 w-3 text-muted-foreground/50" />
235
+ )}
236
+ <button
237
+ type="button"
238
+ className="text-xs hover:text-foreground text-muted-foreground transition-colors rounded px-0.5"
239
+ onClick={() => navigate(seg.path)}
240
+ >
241
+ {i === 0 ? (
242
+ <HardDrive className="h-3.5 w-3.5" />
243
+ ) : (
244
+ seg.label
245
+ )}
246
+ </button>
247
+ </span>
248
+ ))}
249
+ </div>
250
+
251
+ {/* Directory list */}
252
+ <div className="max-h-60 overflow-y-auto">
253
+ {loading ? (
254
+ <div className="flex justify-center py-8">
255
+ <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
256
+ </div>
257
+ ) : error ? (
258
+ <div className="px-4 py-3 text-sm text-destructive">{error}</div>
259
+ ) : (
260
+ <>
261
+ {/* Parent directory */}
262
+ {data?.parent && (
263
+ <button
264
+ type="button"
265
+ className="flex w-full items-center gap-2.5 px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors"
266
+ onClick={() => navigate(data.parent!)}
267
+ >
268
+ <Folder className="h-4 w-4 shrink-0" />
269
+ <span className="font-mono text-xs">..</span>
270
+ </button>
271
+ )}
272
+
273
+ {data?.entries.length === 0 && (
274
+ <p className="px-4 py-3 text-sm text-muted-foreground/60">
275
+ No subdirectories
276
+ </p>
277
+ )}
278
+
279
+ {data?.entries.map((entry) => (
280
+ <button
281
+ type="button"
282
+ key={entry.path}
283
+ className="flex w-full items-center gap-2.5 px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground transition-colors"
284
+ onClick={() => navigate(entry.path)}
285
+ >
286
+ <Folder className="h-4 w-4 shrink-0 text-warning" />
287
+ <span className="truncate">{entry.name}</span>
288
+ </button>
289
+ ))}
290
+ </>
291
+ )}
292
+ </div>
293
+
294
+ {/* Select footer */}
295
+ <div className="border-t px-3 py-2 flex items-center justify-between gap-2 bg-muted">
296
+ <button
297
+ type="button"
298
+ className={cn(
299
+ "flex items-center gap-1.5 text-xs transition-colors rounded px-1.5 py-1",
300
+ isPinned
301
+ ? "text-foreground hover:text-destructive"
302
+ : "text-muted-foreground hover:text-foreground",
303
+ )}
304
+ title={isPinned ? "Unpin this path" : "Pin this path for quick access"}
305
+ onClick={togglePin}
306
+ disabled={pinLoading || !data}
307
+ >
308
+ {pinLoading ? (
309
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
310
+ ) : isPinned ? (
311
+ <PinOff className="h-3.5 w-3.5" />
312
+ ) : (
313
+ <Pin className="h-3.5 w-3.5" />
314
+ )}
315
+ {isPinned ? "Unpin" : "Pin"}
316
+ </button>
317
+ <Button
318
+ size="sm"
319
+ className="shrink-0 gap-1.5"
320
+ disabled={!data || selecting}
321
+ onClick={() => handleSelect()}
322
+ >
323
+ {selecting ? (
324
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
325
+ ) : (
326
+ <FolderOpen className="h-3.5 w-3.5" />
327
+ )}
328
+ Select
329
+ </Button>
330
+ </div>
331
+ </div>
332
+
333
+ {/* Pinned paths */}
334
+ {pins.length > 0 && (
335
+ <div className="rounded-lg border bg-card shadow-sm overflow-hidden">
336
+ <div className="px-3 py-2 border-b bg-muted">
337
+ <span className="text-xs font-medium text-muted-foreground flex items-center gap-1.5">
338
+ <Pin className="h-3 w-3" /> Pinned
339
+ </span>
340
+ </div>
341
+ <div className="max-h-40 overflow-y-auto">
342
+ {pins.map((p) => (
343
+ <div
344
+ key={p}
345
+ className="flex items-center gap-2 px-3 py-1.5 hover:bg-accent group"
346
+ >
347
+ <button
348
+ type="button"
349
+ className="flex-1 flex items-center gap-2 text-sm text-left min-w-0"
350
+ onClick={() => navigate(p)}
351
+ >
352
+ <Folder className="h-4 w-4 shrink-0 text-warning" />
353
+ <span className="truncate font-mono text-xs">{p}</span>
354
+ </button>
355
+ <button
356
+ type="button"
357
+ className="shrink-0 text-muted-foreground/50 hover:text-destructive opacity-0 group-hover:opacity-100 transition-all"
358
+ onClick={() => removePin(p)}
359
+ title="Remove pin"
360
+ >
361
+ <X className="h-3.5 w-3.5" />
362
+ </button>
363
+ </div>
364
+ ))}
365
+ </div>
366
+ </div>
367
+ )}
368
+
369
+ {/* Shortcuts */}
370
+ {data?.shortcuts && data.shortcuts.length > 0 && (
371
+ <div className="flex flex-wrap gap-1.5 justify-center">
372
+ {data.shortcuts.map((s) => (
373
+ <Button
374
+ key={s.path}
375
+ variant="outline"
376
+ size="sm"
377
+ className="h-7 gap-1.5 text-xs"
378
+ onClick={() => navigate(s.path)}
379
+ >
380
+ {s.label === "Home" ? (
381
+ <Home className="h-3 w-3" />
382
+ ) : (
383
+ <HardDrive className="h-3 w-3" />
384
+ )}
385
+ {s.label}
386
+ </Button>
387
+ ))}
388
+ </div>
389
+ )}
390
+ </div>
391
+ </div>
392
+ );
393
+ }