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.
- package/.next/standalone/.github/workflows/publish.yml +37 -0
- package/.next/standalone/DESIGN.md +521 -0
- package/.next/standalone/README.md +283 -0
- package/.next/standalone/bin/wiki-viewer.js +158 -0
- package/.next/standalone/next.config.ts +11 -0
- package/.next/standalone/package.json +120 -0
- package/.next/standalone/pnpm-lock.yaml +5654 -0
- package/.next/standalone/pnpm-workspace.yaml +2 -0
- package/.next/standalone/postcss.config.mjs +2 -0
- package/.next/standalone/public/logo.svg +11 -0
- package/.next/standalone/public/public/logo.svg +11 -0
- package/.next/standalone/server.js +44 -0
- package/.next/standalone/src/app/api/app-proxy/[...path]/route.ts +247 -0
- package/.next/standalone/src/app/api/assets/[...path]/route.ts +70 -0
- package/.next/standalone/src/app/api/system/browse/route.ts +78 -0
- package/.next/standalone/src/app/api/system/clear-root/route.ts +9 -0
- package/.next/standalone/src/app/api/system/config/route.ts +10 -0
- package/.next/standalone/src/app/api/system/pins/route.ts +31 -0
- package/.next/standalone/src/app/api/system/reveal/route.ts +28 -0
- package/.next/standalone/src/app/api/system/root-status/route.ts +9 -0
- package/.next/standalone/src/app/api/system/set-root/route.ts +23 -0
- package/.next/standalone/src/app/api/upload/[...path]/route.ts +80 -0
- package/.next/standalone/src/app/api/wiki/app/route.ts +41 -0
- package/.next/standalone/src/app/api/wiki/content/route.ts +60 -0
- package/.next/standalone/src/app/api/wiki/file/route.ts +52 -0
- package/.next/standalone/src/app/api/wiki/folder/route.ts +26 -0
- package/.next/standalone/src/app/api/wiki/move/route.ts +45 -0
- package/.next/standalone/src/app/api/wiki/page/route.ts +84 -0
- package/.next/standalone/src/app/api/wiki/route.ts +98 -0
- package/.next/standalone/src/app/api/wiki/slugs/route.ts +50 -0
- package/.next/standalone/src/app/api/wiki/upload/route.ts +87 -0
- package/.next/standalone/src/app/api/wiki/watch/route.ts +86 -0
- package/.next/standalone/src/app/globals.css +716 -0
- package/.next/standalone/src/app/icon.svg +11 -0
- package/.next/standalone/src/app/layout.tsx +35 -0
- package/.next/standalone/src/app/page.tsx +1509 -0
- package/.next/standalone/src/components/confirm-dialog.tsx +56 -0
- package/.next/standalone/src/components/dir-picker.tsx +393 -0
- package/.next/standalone/src/components/editor/bubble-menu.tsx +286 -0
- package/.next/standalone/src/components/editor/callout-extension.ts +36 -0
- package/.next/standalone/src/components/editor/csv-viewer.tsx +335 -0
- package/.next/standalone/src/components/editor/editor-toolbar.tsx +564 -0
- package/.next/standalone/src/components/editor/editor.tsx +633 -0
- package/.next/standalone/src/components/editor/extensions/drag-handle.ts +325 -0
- package/.next/standalone/src/components/editor/extensions/heading-anchors.ts +55 -0
- package/.next/standalone/src/components/editor/extensions/resizable-image.tsx +159 -0
- package/.next/standalone/src/components/editor/extensions.ts +148 -0
- package/.next/standalone/src/components/editor/file-fallback-viewer.tsx +74 -0
- package/.next/standalone/src/components/editor/folder-index.tsx +235 -0
- package/.next/standalone/src/components/editor/google-doc-viewer.tsx +73 -0
- package/.next/standalone/src/components/editor/image-viewer.tsx +56 -0
- package/.next/standalone/src/components/editor/link-popover.tsx +111 -0
- package/.next/standalone/src/components/editor/media-popover.tsx +211 -0
- package/.next/standalone/src/components/editor/media-viewer.tsx +69 -0
- package/.next/standalone/src/components/editor/mermaid-viewer.tsx +285 -0
- package/.next/standalone/src/components/editor/node-app-viewer.tsx +269 -0
- package/.next/standalone/src/components/editor/notebook-viewer.tsx +370 -0
- package/.next/standalone/src/components/editor/office/docx-viewer.tsx +92 -0
- package/.next/standalone/src/components/editor/office/office-chrome.tsx +82 -0
- package/.next/standalone/src/components/editor/office/pptx-viewer.tsx +103 -0
- package/.next/standalone/src/components/editor/office/xlsx-viewer.tsx +116 -0
- package/.next/standalone/src/components/editor/pdf-viewer.tsx +31 -0
- package/.next/standalone/src/components/editor/slash-commands.tsx +454 -0
- package/.next/standalone/src/components/editor/source-viewer.tsx +212 -0
- package/.next/standalone/src/components/editor/table-menu.tsx +232 -0
- package/.next/standalone/src/components/editor/website-viewer.tsx +74 -0
- package/.next/standalone/src/components/editor/wiki-link-create-dialog.tsx +188 -0
- package/.next/standalone/src/components/editor/wiki-link-decorator.ts +79 -0
- package/.next/standalone/src/components/editor/wiki-link-extension.ts +120 -0
- package/.next/standalone/src/components/editor/wiki-link-picker.tsx +250 -0
- package/.next/standalone/src/components/layout/viewer-toolbar.tsx +54 -0
- package/.next/standalone/src/components/theme-provider.tsx +16 -0
- package/.next/standalone/src/components/theme-toggle.tsx +24 -0
- package/.next/standalone/src/components/ui/badge.tsx +35 -0
- package/.next/standalone/src/components/ui/button.tsx +56 -0
- package/.next/standalone/src/components/ui/card.tsx +63 -0
- package/.next/standalone/src/components/ui/collapsible.tsx +9 -0
- package/.next/standalone/src/components/ui/command.tsx +150 -0
- package/.next/standalone/src/components/ui/context-menu.tsx +191 -0
- package/.next/standalone/src/components/ui/dialog.tsx +119 -0
- package/.next/standalone/src/components/ui/dropdown-menu.tsx +194 -0
- package/.next/standalone/src/components/ui/input.tsx +22 -0
- package/.next/standalone/src/components/ui/label.tsx +25 -0
- package/.next/standalone/src/components/ui/popover.tsx +33 -0
- package/.next/standalone/src/components/ui/scroll-area.tsx +47 -0
- package/.next/standalone/src/components/ui/select.tsx +156 -0
- package/.next/standalone/src/components/ui/separator.tsx +30 -0
- package/.next/standalone/src/components/ui/sheet.tsx +140 -0
- package/.next/standalone/src/components/ui/skeleton.tsx +15 -0
- package/.next/standalone/src/components/ui/switch.tsx +28 -0
- package/.next/standalone/src/components/ui/tabs.tsx +54 -0
- package/.next/standalone/src/components/ui/textarea.tsx +21 -0
- package/.next/standalone/src/components/ui/tip.tsx +23 -0
- package/.next/standalone/src/components/ui/toggle-group.tsx +41 -0
- package/.next/standalone/src/components/ui/tooltip.tsx +27 -0
- package/.next/standalone/src/components/wiki/frontmatter-header.tsx +164 -0
- package/.next/standalone/src/lib/app-runner.ts +231 -0
- package/.next/standalone/src/lib/cabinets/tree.ts +75 -0
- package/.next/standalone/src/lib/config.ts +32 -0
- package/.next/standalone/src/lib/embeds/detect.ts +142 -0
- package/.next/standalone/src/lib/google/detect.ts +107 -0
- package/.next/standalone/src/lib/markdown/parse-frontmatter.ts +84 -0
- package/.next/standalone/src/lib/markdown/remark-wikilinks.ts +100 -0
- package/.next/standalone/src/lib/markdown/to-html.ts +168 -0
- package/.next/standalone/src/lib/markdown/to-markdown.ts +238 -0
- package/.next/standalone/src/lib/root-dir.ts +35 -0
- package/.next/standalone/src/lib/toast.ts +9 -0
- package/.next/standalone/src/lib/utils.ts +6 -0
- package/.next/standalone/src/lib/wiki-helpers.ts +36 -0
- package/.next/standalone/src/stores/ai-panel-store.ts +23 -0
- package/.next/standalone/src/stores/editor-store.ts +244 -0
- package/.next/standalone/src/stores/tree-store.ts +52 -0
- package/.next/standalone/src/stores/wiki-slugs-store.ts +74 -0
- package/.next/standalone/src/types/index.ts +47 -0
- package/.next/standalone/tailwind.config.ts +201 -0
- package/.next/standalone/tsconfig.json +41 -0
- package/README.md +283 -0
- package/bin/wiki-viewer.js +158 -0
- 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
|
+
}
|