wiki-viewer 1.8.2 → 1.8.3
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/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +5 -5
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/app-proxy/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/browse/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/app/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/upload/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/signin/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/signin/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-inabx._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__00lep-u._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{0~w0_katex_dist_katex_mjs_04mhawr._.js → 0~w0_katex_dist_katex_mjs_0hyln.b._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__09~x2v~._.js → [root-of-the-server]__0du58sw._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_0pqaawz._.js +19 -19
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0shlang._.js → node_modules__pnpm_0.kyqfq._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_04n0h6.._.js → node_modules__pnpm_0.~goq~._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_011vy_q._.js → node_modules__pnpm_00y7foi._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0yiz.-t._.js → node_modules__pnpm_03.1jli._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_093dp2q._.js → node_modules__pnpm_03z3na-._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_01o_5qt._.js → node_modules__pnpm_05~w-ok._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0b8qx0g._.js → node_modules__pnpm_09d5yyu._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0wmx9kq._.js → node_modules__pnpm_09eo8jh._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0nw5ou2._.js → node_modules__pnpm_0adm8h6._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0bbc---._.js → node_modules__pnpm_0arbjru._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0t9jb7d._.js → node_modules__pnpm_0b5fla7._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_124u4o2._.js → node_modules__pnpm_0beormu._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_13tajgx._.js → node_modules__pnpm_0c7u76a._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_09ej62_._.js → node_modules__pnpm_0dbr7c2._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0s0ur~d._.js → node_modules__pnpm_0d~ida~._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_09ljbdz._.js → node_modules__pnpm_0elpfsb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0hlzy8f._.js → node_modules__pnpm_0fh.6ir._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_09~_ob2._.js → node_modules__pnpm_0fm6wcx._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0o.dn8o._.js → node_modules__pnpm_0iikbcy._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0hk6xys._.js → node_modules__pnpm_0mvih7b._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0a01ng7._.js → node_modules__pnpm_0qd2ovj._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0c.309.._.js → node_modules__pnpm_0s4yn43._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0sq3c2r._.js → node_modules__pnpm_0s8fvbn._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0xfx0sf._.js → node_modules__pnpm_0tq8zs6._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_11sut1u._.js → node_modules__pnpm_0vyv.lz._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0c8aunj._.js → node_modules__pnpm_0xa.ko1._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0.96tmo._.js → node_modules__pnpm_12.ymhl._.js} +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
- package/.next/standalone/.next/server/middleware-manifest.json +1 -1
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/static/chunks/{0vp-ahhagxptc.js → 06iw_iy5e90d4.js} +1 -1
- package/.next/standalone/.next/static/chunks/0d~o0h0ssgra7.js +129 -0
- package/.next/standalone/.next/static/chunks/167ph4xws_-61.css +2 -0
- package/.next/standalone/.next/static/chunks/{turbopack-12~z-mr7b9b50.js → turbopack-0lf4~b-ofzbm6.js} +1 -1
- package/.next/standalone/AGENTS.md +104 -0
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/src/app/api/wiki/app/route.ts +7 -4
- package/.next/standalone/src/app/page.tsx +580 -192
- package/.next/standalone/src/components/editor/backlinks-panel.tsx +152 -0
- package/.next/standalone/src/components/editor/document-outline.tsx +221 -0
- package/.next/standalone/src/components/editor/editor.tsx +145 -48
- package/.next/standalone/src/components/editor/node-app-viewer.tsx +65 -7
- package/.next/standalone/src/components/editor/slash-commands.tsx +216 -41
- package/.next/standalone/src/components/editor/source-viewer.tsx +29 -2
- package/.next/standalone/src/components/search/search-command-dialog.tsx +183 -46
- package/.next/standalone/src/lib/app-runner.ts +57 -16
- package/.next/standalone/src/stores/pin-store.ts +55 -0
- package/.next/standalone/src/stores/recent-store.ts +50 -0
- package/.next/standalone/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/.next/standalone/.next/static/chunks/0c_~zevf2n0r4.css +0 -2
- package/.next/standalone/.next/static/chunks/16r--8~rpehcz.js +0 -129
- package/.next/standalone/src/components/wiki/markdown-preview.tsx +0 -120
- /package/.next/standalone/.next/static/{t_k0ztuf0V-iA-kgeez18 → Pkj31aCfRVk0Zy2rKA1wf}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{t_k0ztuf0V-iA-kgeez18 → Pkj31aCfRVk0Zy2rKA1wf}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{t_k0ztuf0V-iA-kgeez18 → Pkj31aCfRVk0Zy2rKA1wf}/_ssgManifest.js +0 -0
|
@@ -213,6 +213,155 @@ const commands: SlashCommand[] = [
|
|
|
213
213
|
},
|
|
214
214
|
];
|
|
215
215
|
|
|
216
|
+
/** Small static preview rendered at the bottom of the menu for the focused command. */
|
|
217
|
+
function CommandPreview({ cmd }: { cmd: SlashCommand }) {
|
|
218
|
+
switch (cmd.label) {
|
|
219
|
+
case "Heading 1":
|
|
220
|
+
return (
|
|
221
|
+
<p className="font-bold text-[18px] leading-tight text-foreground/80 truncate">
|
|
222
|
+
Heading 1
|
|
223
|
+
</p>
|
|
224
|
+
);
|
|
225
|
+
case "Heading 2":
|
|
226
|
+
return (
|
|
227
|
+
<p className="font-semibold text-[14px] leading-tight text-foreground/80 truncate">
|
|
228
|
+
Heading 2
|
|
229
|
+
</p>
|
|
230
|
+
);
|
|
231
|
+
case "Heading 3":
|
|
232
|
+
return (
|
|
233
|
+
<p className="font-medium text-[12px] leading-tight text-foreground/80 truncate">
|
|
234
|
+
Heading 3
|
|
235
|
+
</p>
|
|
236
|
+
);
|
|
237
|
+
case "Text":
|
|
238
|
+
return (
|
|
239
|
+
<p className="text-[11px] text-foreground/60 leading-snug">
|
|
240
|
+
The quick brown fox jumps over the lazy dog.
|
|
241
|
+
</p>
|
|
242
|
+
);
|
|
243
|
+
case "Bullet List":
|
|
244
|
+
return (
|
|
245
|
+
<ul className="list-disc pl-4 text-[10px] text-foreground/60 space-y-0.5">
|
|
246
|
+
<li>First item</li>
|
|
247
|
+
<li>Second item</li>
|
|
248
|
+
<li>Third item</li>
|
|
249
|
+
</ul>
|
|
250
|
+
);
|
|
251
|
+
case "Numbered List":
|
|
252
|
+
return (
|
|
253
|
+
<ol className="list-decimal pl-4 text-[10px] text-foreground/60 space-y-0.5">
|
|
254
|
+
<li>First item</li>
|
|
255
|
+
<li>Second item</li>
|
|
256
|
+
<li>Third item</li>
|
|
257
|
+
</ol>
|
|
258
|
+
);
|
|
259
|
+
case "Checklist":
|
|
260
|
+
return (
|
|
261
|
+
<div className="space-y-0.5 text-[10px] text-foreground/60">
|
|
262
|
+
<div className="flex items-center gap-1.5">
|
|
263
|
+
<span className="w-3 h-3 border border-foreground/30 rounded-sm inline-block shrink-0" />
|
|
264
|
+
Pending task
|
|
265
|
+
</div>
|
|
266
|
+
<div className="flex items-center gap-1.5">
|
|
267
|
+
<span className="w-3 h-3 border border-foreground/30 rounded-sm bg-foreground/20 inline-block shrink-0" />
|
|
268
|
+
Done task
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
case "Code Block":
|
|
273
|
+
return (
|
|
274
|
+
<pre className="bg-muted rounded px-2 py-1 text-[10px] font-mono text-foreground/70 leading-snug">
|
|
275
|
+
<code>{"const x = 42;"}</code>
|
|
276
|
+
</pre>
|
|
277
|
+
);
|
|
278
|
+
case "Blockquote":
|
|
279
|
+
return (
|
|
280
|
+
<div className="border-l-2 border-foreground/30 pl-2 text-[10px] text-foreground/60 italic">
|
|
281
|
+
A thought worth quoting.
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
284
|
+
case "Divider":
|
|
285
|
+
return <hr className="border-foreground/20 w-full" />;
|
|
286
|
+
case "Table":
|
|
287
|
+
return (
|
|
288
|
+
<table className="text-[9px] border-collapse w-full">
|
|
289
|
+
<thead>
|
|
290
|
+
<tr>
|
|
291
|
+
{["Col A", "Col B", "Col C"].map((h) => (
|
|
292
|
+
<th
|
|
293
|
+
key={h}
|
|
294
|
+
className="border border-foreground/20 px-1.5 py-0.5 text-left font-semibold bg-muted/50 text-foreground/70"
|
|
295
|
+
>
|
|
296
|
+
{h}
|
|
297
|
+
</th>
|
|
298
|
+
))}
|
|
299
|
+
</tr>
|
|
300
|
+
</thead>
|
|
301
|
+
<tbody>
|
|
302
|
+
{[["1", "2", "3"], ["4", "5", "6"]].map((row, i) => (
|
|
303
|
+
<tr key={i}>
|
|
304
|
+
{row.map((cell, j) => (
|
|
305
|
+
<td
|
|
306
|
+
key={j}
|
|
307
|
+
className="border border-foreground/20 px-1.5 py-0.5 text-foreground/50"
|
|
308
|
+
>
|
|
309
|
+
{cell}
|
|
310
|
+
</td>
|
|
311
|
+
))}
|
|
312
|
+
</tr>
|
|
313
|
+
))}
|
|
314
|
+
</tbody>
|
|
315
|
+
</table>
|
|
316
|
+
);
|
|
317
|
+
case "Image":
|
|
318
|
+
return (
|
|
319
|
+
<div className="flex items-center gap-2 text-foreground/50">
|
|
320
|
+
<ImageIcon className="h-8 w-8 text-foreground/20" />
|
|
321
|
+
<span className="text-[10px]">image.png</span>
|
|
322
|
+
</div>
|
|
323
|
+
);
|
|
324
|
+
case "Video":
|
|
325
|
+
return (
|
|
326
|
+
<div className="flex items-center gap-2 text-foreground/50">
|
|
327
|
+
<Video className="h-8 w-8 text-foreground/20" />
|
|
328
|
+
<span className="text-[10px]">video.mp4</span>
|
|
329
|
+
</div>
|
|
330
|
+
);
|
|
331
|
+
case "File":
|
|
332
|
+
return (
|
|
333
|
+
<div className="flex items-center gap-2 text-foreground/50">
|
|
334
|
+
<File className="h-8 w-8 text-foreground/20" />
|
|
335
|
+
<span className="text-[10px]">attachment.pdf</span>
|
|
336
|
+
</div>
|
|
337
|
+
);
|
|
338
|
+
case "Callout":
|
|
339
|
+
return (
|
|
340
|
+
<div className="flex items-start gap-1.5 bg-blue-500/10 border border-blue-500/20 rounded px-2 py-1">
|
|
341
|
+
<Info className="h-3 w-3 text-blue-500 shrink-0 mt-0.5" />
|
|
342
|
+
<span className="text-[10px] text-foreground/60">Info callout</span>
|
|
343
|
+
</div>
|
|
344
|
+
);
|
|
345
|
+
case "Warning":
|
|
346
|
+
return (
|
|
347
|
+
<div className="flex items-start gap-1.5 bg-yellow-500/10 border border-yellow-500/20 rounded px-2 py-1">
|
|
348
|
+
<AlertTriangle className="h-3 w-3 text-yellow-500 shrink-0 mt-0.5" />
|
|
349
|
+
<span className="text-[10px] text-foreground/60">Warning callout</span>
|
|
350
|
+
</div>
|
|
351
|
+
);
|
|
352
|
+
case "Math":
|
|
353
|
+
return (
|
|
354
|
+
<p className="font-mono text-[13px] text-foreground/70">
|
|
355
|
+
E = mc²
|
|
356
|
+
</p>
|
|
357
|
+
);
|
|
358
|
+
default:
|
|
359
|
+
return (
|
|
360
|
+
<p className="text-[10px] text-muted-foreground">{cmd.description}</p>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
216
365
|
interface SlashCommandsProps {
|
|
217
366
|
editor: Editor | null;
|
|
218
367
|
}
|
|
@@ -396,56 +545,82 @@ export function SlashCommands({ editor }: SlashCommandsProps) {
|
|
|
396
545
|
];
|
|
397
546
|
|
|
398
547
|
const flatCommands: SlashCommand[] = filtered;
|
|
548
|
+
const focusedCmd = flatCommands[selectedIndex] ?? null;
|
|
399
549
|
|
|
400
550
|
return (
|
|
401
551
|
<>
|
|
402
552
|
{open && filtered.length > 0 && (
|
|
403
553
|
<div
|
|
404
554
|
ref={menuRef}
|
|
405
|
-
className="absolute z-50 w-[280px] bg-popover border border-border rounded-lg shadow-lg
|
|
555
|
+
className="absolute z-50 w-[280px] bg-popover border border-border rounded-lg shadow-lg overflow-hidden"
|
|
406
556
|
style={{ top: position.top, left: position.left }}
|
|
407
557
|
>
|
|
408
|
-
{
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
558
|
+
{/* Scrollable command list */}
|
|
559
|
+
<div className="max-h-[300px] overflow-y-auto py-1">
|
|
560
|
+
{order.map((group) => {
|
|
561
|
+
const items = byCategory.get(group.key);
|
|
562
|
+
if (!items || items.length === 0) return null;
|
|
563
|
+
return (
|
|
564
|
+
<div key={group.key}>
|
|
565
|
+
<div className="text-[9px] uppercase tracking-wider text-muted-foreground px-3 pt-2 pb-1">
|
|
566
|
+
{group.title}
|
|
567
|
+
</div>
|
|
568
|
+
{items.map((cmd) => {
|
|
569
|
+
const flatIndex = flatCommands.indexOf(cmd);
|
|
570
|
+
const Icon = cmd.icon;
|
|
571
|
+
const isFocused = flatIndex === selectedIndex;
|
|
572
|
+
return (
|
|
573
|
+
<button
|
|
574
|
+
key={cmd.label}
|
|
575
|
+
onMouseDown={(e) => {
|
|
576
|
+
e.preventDefault();
|
|
577
|
+
handleSelect(cmd);
|
|
578
|
+
}}
|
|
579
|
+
onMouseEnter={() => setSelectedIndex(flatIndex)}
|
|
580
|
+
className={cn(
|
|
581
|
+
"flex items-center gap-3 w-full px-3 py-1.5 text-left transition-colors",
|
|
582
|
+
isFocused
|
|
583
|
+
? "bg-accent text-accent-foreground ring-1 ring-inset ring-accent-foreground/10"
|
|
584
|
+
: "hover:bg-accent/50",
|
|
585
|
+
)}
|
|
586
|
+
>
|
|
587
|
+
<Icon
|
|
588
|
+
className={cn(
|
|
589
|
+
"h-4 w-4 shrink-0 transition-colors",
|
|
590
|
+
isFocused
|
|
591
|
+
? "text-accent-foreground"
|
|
592
|
+
: "text-muted-foreground",
|
|
593
|
+
)}
|
|
594
|
+
/>
|
|
595
|
+
<div className="min-w-0">
|
|
596
|
+
<p className="text-[12px] font-medium truncate">
|
|
597
|
+
{cmd.label}
|
|
598
|
+
</p>
|
|
599
|
+
<p
|
|
600
|
+
className={cn(
|
|
601
|
+
"text-[10px] truncate transition-colors",
|
|
602
|
+
isFocused
|
|
603
|
+
? "text-accent-foreground/70"
|
|
604
|
+
: "text-muted-foreground",
|
|
605
|
+
)}
|
|
606
|
+
>
|
|
607
|
+
{cmd.description}
|
|
608
|
+
</p>
|
|
609
|
+
</div>
|
|
610
|
+
</button>
|
|
611
|
+
);
|
|
612
|
+
})}
|
|
415
613
|
</div>
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
onMouseEnter={() => setSelectedIndex(flatIndex)}
|
|
427
|
-
className={cn(
|
|
428
|
-
"flex items-center gap-3 w-full px-3 py-1.5 text-left transition-colors",
|
|
429
|
-
flatIndex === selectedIndex
|
|
430
|
-
? "bg-accent text-accent-foreground"
|
|
431
|
-
: "hover:bg-accent/50",
|
|
432
|
-
)}
|
|
433
|
-
>
|
|
434
|
-
<Icon className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
435
|
-
<div className="min-w-0">
|
|
436
|
-
<p className="text-[12px] font-medium truncate">
|
|
437
|
-
{cmd.label}
|
|
438
|
-
</p>
|
|
439
|
-
<p className="text-[10px] text-muted-foreground truncate">
|
|
440
|
-
{cmd.description}
|
|
441
|
-
</p>
|
|
442
|
-
</div>
|
|
443
|
-
</button>
|
|
444
|
-
);
|
|
445
|
-
})}
|
|
446
|
-
</div>
|
|
447
|
-
);
|
|
448
|
-
})}
|
|
614
|
+
);
|
|
615
|
+
})}
|
|
616
|
+
</div>
|
|
617
|
+
|
|
618
|
+
{/* Preview pane for focused command */}
|
|
619
|
+
{focusedCmd && (
|
|
620
|
+
<div className="border-t border-border bg-muted/30 px-3 py-2 min-h-[52px] flex items-center">
|
|
621
|
+
<CommandPreview cmd={focusedCmd} />
|
|
622
|
+
</div>
|
|
623
|
+
)}
|
|
449
624
|
</div>
|
|
450
625
|
)}
|
|
451
626
|
{renderPopover()}
|
|
@@ -7,6 +7,23 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|
|
7
7
|
import { ViewerToolbar } from "@/components/layout/viewer-toolbar";
|
|
8
8
|
import { Button } from "@/components/ui/button";
|
|
9
9
|
import { withWs, wsFetch } from "@/lib/workspace-client";
|
|
10
|
+
import { FileFallbackViewer } from "@/components/editor/file-fallback-viewer";
|
|
11
|
+
|
|
12
|
+
// Heuristic binary sniff: a NUL byte never appears in UTF-8/UTF-16LE text we
|
|
13
|
+
// care about, and a high ratio of control chars (excluding tab/newline/CR)
|
|
14
|
+
// signals binary. Only inspect a prefix \u2014 enough to classify cheaply.
|
|
15
|
+
function looksBinary(bytes: Uint8Array): boolean {
|
|
16
|
+
const n = Math.min(bytes.length, 8192);
|
|
17
|
+
if (n === 0) return false;
|
|
18
|
+
let suspicious = 0;
|
|
19
|
+
for (let i = 0; i < n; i++) {
|
|
20
|
+
const b = bytes[i];
|
|
21
|
+
if (b === 0) return true; // NUL \u2192 definitely binary
|
|
22
|
+
// Allow tab(9), LF(10), CR(13); flag other C0 control chars.
|
|
23
|
+
if (b < 9 || (b > 13 && b < 32)) suspicious++;
|
|
24
|
+
}
|
|
25
|
+
return suspicious / n > 0.3;
|
|
26
|
+
}
|
|
10
27
|
|
|
11
28
|
interface SourceViewerProps {
|
|
12
29
|
path: string;
|
|
@@ -73,6 +90,7 @@ function formatBadge(filename: string): string {
|
|
|
73
90
|
|
|
74
91
|
export function SourceViewer({ path }: SourceViewerProps) {
|
|
75
92
|
const [content, setContent] = useState<string | null>(null);
|
|
93
|
+
const [binary, setBinary] = useState(false);
|
|
76
94
|
const [loading, setLoading] = useState(true);
|
|
77
95
|
const [wrap, setWrap] = useState(false);
|
|
78
96
|
const [copied, setCopied] = useState(false);
|
|
@@ -86,8 +104,12 @@ export function SourceViewer({ path }: SourceViewerProps) {
|
|
|
86
104
|
try {
|
|
87
105
|
const res = await wsFetch(assetUrl);
|
|
88
106
|
if (res.ok) {
|
|
89
|
-
const
|
|
90
|
-
|
|
107
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
108
|
+
if (looksBinary(bytes)) {
|
|
109
|
+
setBinary(true);
|
|
110
|
+
} else {
|
|
111
|
+
setContent(new TextDecoder("utf-8").decode(bytes));
|
|
112
|
+
}
|
|
91
113
|
}
|
|
92
114
|
} catch {
|
|
93
115
|
/* ignore */
|
|
@@ -129,6 +151,11 @@ export function SourceViewer({ path }: SourceViewerProps) {
|
|
|
129
151
|
setTimeout(() => setCopied(false), 2000);
|
|
130
152
|
};
|
|
131
153
|
|
|
154
|
+
// Detected binary at runtime \u2192 reuse the download/reveal fallback UI.
|
|
155
|
+
if (binary) {
|
|
156
|
+
return <FileFallbackViewer path={path} title={filename} />;
|
|
157
|
+
}
|
|
158
|
+
|
|
132
159
|
return (
|
|
133
160
|
<div className="flex-1 flex flex-col overflow-hidden">
|
|
134
161
|
<ViewerToolbar
|
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Command palette (cmd+k / ctrl+k) for full-text search.
|
|
4
|
+
* Command palette (cmd+k / ctrl+k) for full-text search + app actions.
|
|
5
5
|
* Mounts once in the app shell; visibility is driven by the search store.
|
|
6
|
+
*
|
|
7
|
+
* Action mode activates when the query is empty or starts with ">".
|
|
8
|
+
* New optional props let the parent wire page-level callbacks; they default
|
|
9
|
+
* to no-ops so this component compiles without any parent changes.
|
|
6
10
|
*/
|
|
7
11
|
import { useEffect } from "react";
|
|
12
|
+
import {
|
|
13
|
+
AlignCenter,
|
|
14
|
+
Bot,
|
|
15
|
+
Copy,
|
|
16
|
+
FilePlus,
|
|
17
|
+
Moon,
|
|
18
|
+
PanelLeft,
|
|
19
|
+
Sun,
|
|
20
|
+
} from "lucide-react";
|
|
21
|
+
import { useTheme } from "next-themes";
|
|
8
22
|
import {
|
|
9
23
|
CommandDialog,
|
|
10
24
|
CommandEmpty,
|
|
@@ -12,15 +26,33 @@ import {
|
|
|
12
26
|
CommandInput,
|
|
13
27
|
CommandItem,
|
|
14
28
|
CommandList,
|
|
29
|
+
CommandSeparator,
|
|
15
30
|
} from "@/components/ui/command";
|
|
16
31
|
import { useSearchStore } from "@/stores/search-store";
|
|
32
|
+
import { useAIPanelStore } from "@/stores/ai-panel-store";
|
|
33
|
+
import {
|
|
34
|
+
useViewWidthStore,
|
|
35
|
+
VIEW_WIDTH_LABEL,
|
|
36
|
+
VIEW_WIDTH_ORDER,
|
|
37
|
+
} from "@/stores/view-width-store";
|
|
17
38
|
import { SnippetText } from "./snippet-text";
|
|
18
39
|
|
|
40
|
+
export interface SearchCommandDialogProps {
|
|
41
|
+
onOpenFile: (relPath: string) => void;
|
|
42
|
+
/** Wire to page-level sidebar toggle. Defaults to no-op. */
|
|
43
|
+
onToggleSidebar?: () => void;
|
|
44
|
+
/** Wire to page-level new-file action. Defaults to no-op. */
|
|
45
|
+
onNewFile?: () => void;
|
|
46
|
+
/** Wire to page-level copy-path action. Defaults to no-op. */
|
|
47
|
+
onCopyPath?: () => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
19
50
|
export function SearchCommandDialog({
|
|
20
51
|
onOpenFile,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
52
|
+
onToggleSidebar = () => undefined,
|
|
53
|
+
onNewFile = () => undefined,
|
|
54
|
+
onCopyPath = () => undefined,
|
|
55
|
+
}: SearchCommandDialogProps) {
|
|
24
56
|
const open = useSearchStore((s) => s.open);
|
|
25
57
|
const setOpen = useSearchStore((s) => s.setOpen);
|
|
26
58
|
const query = useSearchStore((s) => s.query);
|
|
@@ -30,6 +62,18 @@ export function SearchCommandDialog({
|
|
|
30
62
|
const truncated = useSearchStore((s) => s.truncated);
|
|
31
63
|
const search = useSearchStore((s) => s.search);
|
|
32
64
|
|
|
65
|
+
const { resolvedTheme, setTheme } = useTheme();
|
|
66
|
+
const toggleAIPanel = useAIPanelStore((s) => s.toggle);
|
|
67
|
+
const viewWidth = useViewWidthStore((s) => s.width);
|
|
68
|
+
const cycleViewWidth = useViewWidthStore((s) => s.cycle);
|
|
69
|
+
|
|
70
|
+
// Action mode: empty query or ">" prefix (VS Code convention).
|
|
71
|
+
const trimmed = query.trimStart();
|
|
72
|
+
const isActionMode = trimmed === "" || trimmed.startsWith(">");
|
|
73
|
+
|
|
74
|
+
// The bare text to actually search files (strip leading ">").
|
|
75
|
+
const fileQuery = isActionMode ? "" : query;
|
|
76
|
+
|
|
33
77
|
// Global cmd+k / ctrl+k toggle.
|
|
34
78
|
useEffect(() => {
|
|
35
79
|
function onKey(e: KeyboardEvent) {
|
|
@@ -42,67 +86,160 @@ export function SearchCommandDialog({
|
|
|
42
86
|
return () => window.removeEventListener("keydown", onKey);
|
|
43
87
|
}, [open, setOpen]);
|
|
44
88
|
|
|
45
|
-
// Debounced search
|
|
89
|
+
// Debounced search — only fires when NOT in action mode.
|
|
46
90
|
useEffect(() => {
|
|
47
91
|
if (!open) return;
|
|
92
|
+
if (isActionMode) return;
|
|
48
93
|
const id = setTimeout(() => {
|
|
49
|
-
void search(
|
|
94
|
+
void search(fileQuery);
|
|
50
95
|
}, 120);
|
|
51
96
|
return () => clearTimeout(id);
|
|
52
|
-
}, [
|
|
97
|
+
}, [fileQuery, isActionMode, open, search]);
|
|
98
|
+
|
|
99
|
+
// Helper: run action then close.
|
|
100
|
+
function runAction(fn: () => void) {
|
|
101
|
+
fn();
|
|
102
|
+
setOpen(false);
|
|
103
|
+
setQuery("");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Next view-width label for display.
|
|
107
|
+
const nextWidthIdx =
|
|
108
|
+
(VIEW_WIDTH_ORDER.indexOf(viewWidth) + 1) % VIEW_WIDTH_ORDER.length;
|
|
109
|
+
const nextWidthLabel = VIEW_WIDTH_LABEL[VIEW_WIDTH_ORDER[nextWidthIdx]];
|
|
53
110
|
|
|
54
111
|
return (
|
|
55
112
|
<CommandDialog
|
|
56
113
|
open={open}
|
|
57
|
-
onOpenChange={
|
|
114
|
+
onOpenChange={(v) => {
|
|
115
|
+
setOpen(v);
|
|
116
|
+
if (!v) setQuery("");
|
|
117
|
+
}}
|
|
58
118
|
// cmdk's built-in fuzzy filter would re-rank our BM25 results; disable it.
|
|
59
119
|
shouldFilter={false}
|
|
60
120
|
>
|
|
61
121
|
<CommandInput
|
|
62
|
-
placeholder="Search files…"
|
|
122
|
+
placeholder="Search files… ( > for actions )"
|
|
63
123
|
value={query}
|
|
64
124
|
onValueChange={setQuery}
|
|
65
125
|
/>
|
|
66
126
|
<CommandList>
|
|
67
|
-
{
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
127
|
+
{/* ── Action mode ─────────────────────────────────────── */}
|
|
128
|
+
{isActionMode && (
|
|
129
|
+
<CommandGroup heading="Actions">
|
|
130
|
+
<CommandItem
|
|
131
|
+
value="toggle-dark-mode"
|
|
132
|
+
onSelect={() =>
|
|
133
|
+
runAction(() =>
|
|
134
|
+
setTheme(resolvedTheme === "dark" ? "light" : "dark"),
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
>
|
|
138
|
+
{resolvedTheme === "dark" ? (
|
|
139
|
+
<Sun className="mr-2 h-4 w-4" />
|
|
140
|
+
) : (
|
|
141
|
+
<Moon className="mr-2 h-4 w-4" />
|
|
142
|
+
)}
|
|
143
|
+
{resolvedTheme === "dark"
|
|
144
|
+
? "Switch to light mode"
|
|
145
|
+
: "Switch to dark mode"}
|
|
146
|
+
</CommandItem>
|
|
147
|
+
|
|
148
|
+
<CommandItem
|
|
149
|
+
value="toggle-ai-panel"
|
|
150
|
+
onSelect={() => runAction(toggleAIPanel)}
|
|
151
|
+
>
|
|
152
|
+
<Bot className="mr-2 h-4 w-4" />
|
|
153
|
+
Toggle AI panel
|
|
154
|
+
</CommandItem>
|
|
155
|
+
|
|
156
|
+
<CommandItem
|
|
157
|
+
value="toggle-sidebar"
|
|
158
|
+
onSelect={() => runAction(onToggleSidebar)}
|
|
159
|
+
>
|
|
160
|
+
<PanelLeft className="mr-2 h-4 w-4" />
|
|
161
|
+
Toggle sidebar
|
|
162
|
+
</CommandItem>
|
|
163
|
+
|
|
164
|
+
<CommandItem
|
|
165
|
+
value="new-file"
|
|
166
|
+
onSelect={() => runAction(onNewFile)}
|
|
167
|
+
>
|
|
168
|
+
<FilePlus className="mr-2 h-4 w-4" />
|
|
169
|
+
New file
|
|
170
|
+
</CommandItem>
|
|
171
|
+
|
|
172
|
+
<CommandItem
|
|
173
|
+
value="copy-path"
|
|
174
|
+
onSelect={() => runAction(onCopyPath)}
|
|
175
|
+
>
|
|
176
|
+
<Copy className="mr-2 h-4 w-4" />
|
|
177
|
+
Copy current file path
|
|
178
|
+
</CommandItem>
|
|
179
|
+
|
|
180
|
+
<CommandItem
|
|
181
|
+
value="cycle-view-width"
|
|
182
|
+
onSelect={() => runAction(cycleViewWidth)}
|
|
183
|
+
>
|
|
184
|
+
<AlignCenter className="mr-2 h-4 w-4" />
|
|
185
|
+
<span>
|
|
186
|
+
Change view width{" "}
|
|
187
|
+
<span className="text-muted-foreground">
|
|
188
|
+
({VIEW_WIDTH_LABEL[viewWidth]} → {nextWidthLabel})
|
|
189
|
+
</span>
|
|
190
|
+
</span>
|
|
191
|
+
</CommandItem>
|
|
104
192
|
</CommandGroup>
|
|
105
193
|
)}
|
|
194
|
+
|
|
195
|
+
{/* ── File-search mode ────────────────────────────────── */}
|
|
196
|
+
{!isActionMode && (
|
|
197
|
+
<>
|
|
198
|
+
{loading && (
|
|
199
|
+
<div className="px-3 py-2 text-xs text-muted-foreground">
|
|
200
|
+
Searching…
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
203
|
+
{!loading && fileQuery.trim() !== "" && results.length === 0 && (
|
|
204
|
+
<CommandEmpty>No matches.</CommandEmpty>
|
|
205
|
+
)}
|
|
206
|
+
{results.length > 0 && (
|
|
207
|
+
<>
|
|
208
|
+
<CommandSeparator />
|
|
209
|
+
<CommandGroup
|
|
210
|
+
heading={
|
|
211
|
+
truncated
|
|
212
|
+
? "Top results (refine query for more)"
|
|
213
|
+
: "Results"
|
|
214
|
+
}
|
|
215
|
+
>
|
|
216
|
+
{results.map((r) => (
|
|
217
|
+
<CommandItem
|
|
218
|
+
key={r.path}
|
|
219
|
+
value={r.path}
|
|
220
|
+
onSelect={() => {
|
|
221
|
+
onOpenFile(r.path);
|
|
222
|
+
setOpen(false);
|
|
223
|
+
setQuery("");
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
226
|
+
<div className="flex min-w-0 flex-col gap-0.5">
|
|
227
|
+
<span className="truncate font-mono text-xs">
|
|
228
|
+
{r.path}
|
|
229
|
+
</span>
|
|
230
|
+
{r.snippet && (
|
|
231
|
+
<span className="truncate text-xs text-muted-foreground">
|
|
232
|
+
<SnippetText html={r.snippet} />
|
|
233
|
+
</span>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
</CommandItem>
|
|
237
|
+
))}
|
|
238
|
+
</CommandGroup>
|
|
239
|
+
</>
|
|
240
|
+
)}
|
|
241
|
+
</>
|
|
242
|
+
)}
|
|
106
243
|
</CommandList>
|
|
107
244
|
</CommandDialog>
|
|
108
245
|
);
|