feat: composer improvements
This commit is contained in:
@@ -178,11 +178,20 @@ function buildGraphArrays(
|
||||
// Browse window data
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface HistoryEntry {
|
||||
path: string;
|
||||
html: string | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface BrowseWinData {
|
||||
node: NetworkNode;
|
||||
pageHtml: string | null;
|
||||
pageLoading: boolean;
|
||||
pageError: string | null;
|
||||
currentPath: string;
|
||||
history: HistoryEntry[];
|
||||
historyIndex: number;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -485,15 +494,83 @@ export default function BrowseView() {
|
||||
return () => { unsub(); if (batchTimer) clearTimeout(batchTimer); flush(); };
|
||||
}, []);
|
||||
|
||||
// ── Navigation helpers ──
|
||||
const navigateTo = useCallback((winId: string, node: NetworkNode, path: string, prevData?: BrowseWinData) => {
|
||||
const loading: BrowseWinData = {
|
||||
node, pageHtml: null, pageLoading: true, pageError: null,
|
||||
currentPath: path,
|
||||
history: prevData?.history ?? [],
|
||||
historyIndex: prevData?.historyIndex ?? -1,
|
||||
};
|
||||
updateWindow(winId, { data: loading });
|
||||
|
||||
fetchRemotePage(node.hash, path)
|
||||
.then((res) => {
|
||||
const html = res.content ? renderMicron(res.content, true) : null;
|
||||
const error = res.content ? null : (res.error ?? "No content");
|
||||
const entry: HistoryEntry = { path, html, error };
|
||||
|
||||
// Build new history: truncate any forward entries, push new
|
||||
const prevHistory = loading.history.slice(0, loading.historyIndex + 1);
|
||||
const newHistory = [...prevHistory, entry];
|
||||
const newIndex = newHistory.length - 1;
|
||||
|
||||
updateWindow(winId, { data: { node, pageHtml: html, pageError: error, pageLoading: false, currentPath: path, history: newHistory, historyIndex: newIndex } });
|
||||
})
|
||||
.catch((e) => {
|
||||
const error = String(e);
|
||||
const entry: HistoryEntry = { path, html: null, error };
|
||||
const prevHistory = loading.history.slice(0, loading.historyIndex + 1);
|
||||
const newHistory = [...prevHistory, entry];
|
||||
const newIndex = newHistory.length - 1;
|
||||
updateWindow(winId, { data: { node, pageError: error, pageLoading: false, pageHtml: null, currentPath: path, history: newHistory, historyIndex: newIndex } });
|
||||
});
|
||||
}, [updateWindow]);
|
||||
|
||||
const navBack = useCallback((winId: string, data: BrowseWinData) => {
|
||||
const newIndex = data.historyIndex - 1;
|
||||
if (newIndex < 0) return;
|
||||
const entry = data.history[newIndex]!;
|
||||
updateWindow(winId, { data: { ...data, pageHtml: entry.html, pageError: entry.error, pageLoading: false, currentPath: entry.path, historyIndex: newIndex } });
|
||||
}, [updateWindow]);
|
||||
|
||||
const navForward = useCallback((winId: string, data: BrowseWinData) => {
|
||||
const newIndex = data.historyIndex + 1;
|
||||
if (newIndex >= data.history.length) return;
|
||||
const entry = data.history[newIndex]!;
|
||||
updateWindow(winId, { data: { ...data, pageHtml: entry.html, pageError: entry.error, pageLoading: false, currentPath: entry.path, historyIndex: newIndex } });
|
||||
}, [updateWindow]);
|
||||
|
||||
const navReload = useCallback((winId: string, data: BrowseWinData) => {
|
||||
navigateTo(winId, data.node, data.currentPath, { ...data, historyIndex: data.historyIndex - 1 });
|
||||
}, [navigateTo]);
|
||||
|
||||
// ── Node click ──
|
||||
const handleNodeClick = useCallback((node: NetworkNode) => {
|
||||
const id = node.hash;
|
||||
const data: BrowseWinData = { node, pageHtml: null, pageLoading: true, pageError: null };
|
||||
openWindow(id, data);
|
||||
fetchRemotePage(node.hash)
|
||||
.then((res) => updateWindow(id, { data: { node, pageHtml: res.content ? renderMicron(res.content, true) : null, pageError: res.content ? null : (res.error ?? "No content"), pageLoading: false } }))
|
||||
.catch((e) => updateWindow(id, { data: { node, pageError: String(e), pageLoading: false, pageHtml: null } }));
|
||||
}, [openWindow, updateWindow]);
|
||||
const initData: BrowseWinData = { node, pageHtml: null, pageLoading: true, pageError: null, currentPath: "index.mu", history: [], historyIndex: -1 };
|
||||
openWindow(id, initData);
|
||||
navigateTo(id, node, "index.mu");
|
||||
}, [openWindow, navigateTo]);
|
||||
|
||||
// ── Handle micron link clicks via event delegation ──
|
||||
const handleContentClick = useCallback((e: React.MouseEvent, winId: string, data: BrowseWinData) => {
|
||||
const anchor = (e.target as HTMLElement).closest("a");
|
||||
if (!anchor) return;
|
||||
e.preventDefault();
|
||||
|
||||
const dest = anchor.getAttribute("data-destination") ?? anchor.getAttribute("href") ?? "";
|
||||
if (!dest) return;
|
||||
|
||||
// Strip nomadnetwork:// prefix if present, normalize path
|
||||
let path = dest.replace(/^nomadnetwork:\/\//, "").replace(/^\/+/, "");
|
||||
// If it looks like a hash (hex, 32 chars), it's a node link — not a page path
|
||||
if (/^[0-9a-f]{32}$/i.test(path)) return;
|
||||
// Ensure .mu extension
|
||||
if (!path.endsWith(".mu")) path += ".mu";
|
||||
|
||||
navigateTo(winId, data.node, path, data);
|
||||
}, [navigateTo]);
|
||||
|
||||
const clearSearch = useCallback(() => {
|
||||
setFilter("");
|
||||
@@ -502,8 +579,6 @@ export default function BrowseView() {
|
||||
updateClusterLabels();
|
||||
}, [updateClusterLabels]);
|
||||
|
||||
const nodeCount = nodes.filter(n => n.type !== "interface").length;
|
||||
const ifaceCount = nodes.filter(n => n.type === "interface").length;
|
||||
|
||||
// ── Capture typing into search when no window is focused ──
|
||||
useEffect(() => { searchInputRef.current?.focus(); }, []);
|
||||
@@ -612,42 +687,63 @@ export default function BrowseView() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{windows.map((win) => (
|
||||
<FloatingWindow
|
||||
key={win.id}
|
||||
id={win.id}
|
||||
title={win.data.node.name}
|
||||
x={win.x} y={win.y} w={win.w} h={win.h}
|
||||
zIndex={win.zIndex}
|
||||
focused={focusedWinId === win.id}
|
||||
onUpdate={updateWindow}
|
||||
onClose={closeWindowById}
|
||||
onFocus={focusWindow}
|
||||
addressBar={
|
||||
<div className="flex items-center gap-2 px-3 py-1 border-b border-border shrink-0 bg-muted/15">
|
||||
<span className="text-[9px] text-muted-foreground uppercase tracking-wider shrink-0">addr</span>
|
||||
<div className="flex-1 flex items-center h-5 px-2 bg-background/60 border border-border rounded text-[10px] font-mono text-foreground/80 truncate">{win.data.node.hash}</div>
|
||||
<span className="flex items-center gap-1 text-[9px] uppercase tracking-wider shrink-0">
|
||||
{win.data.pageLoading ? <span className="text-muted-foreground animate-pulse">loading</span>
|
||||
: win.data.pageError ? <><span className="w-1.5 h-1.5 rounded-full bg-destructive inline-block" /><span className="text-destructive">error</span></>
|
||||
: win.data.pageHtml ? <><span className="w-1.5 h-1.5 rounded-full bg-green-500 inline-block" /><span className="text-muted-foreground">ok</span></> : null}
|
||||
</span>
|
||||
{windows.map((win) => {
|
||||
const d = win.data;
|
||||
const canBack = d.historyIndex > 0;
|
||||
const canFwd = d.historyIndex < d.history.length - 1;
|
||||
return (
|
||||
<FloatingWindow
|
||||
key={win.id}
|
||||
id={win.id}
|
||||
title={win.data.node.name}
|
||||
x={win.x} y={win.y} w={win.w} h={win.h}
|
||||
zIndex={win.zIndex}
|
||||
focused={focusedWinId === win.id}
|
||||
onUpdate={updateWindow}
|
||||
onClose={closeWindowById}
|
||||
onFocus={focusWindow}
|
||||
addressBar={
|
||||
<div className="flex items-center gap-1.5 px-2 py-1 border-b border-border shrink-0 bg-muted/15">
|
||||
{/* Nav buttons */}
|
||||
<button onClick={() => navBack(win.id, d)} disabled={!canBack || d.pageLoading}
|
||||
className="w-5 h-5 flex items-center justify-center rounded text-[10px] text-muted-foreground hover:text-foreground disabled:opacity-30 disabled:cursor-default transition-colors" title="Back">
|
||||
◀
|
||||
</button>
|
||||
<button onClick={() => navForward(win.id, d)} disabled={!canFwd || d.pageLoading}
|
||||
className="w-5 h-5 flex items-center justify-center rounded text-[10px] text-muted-foreground hover:text-foreground disabled:opacity-30 disabled:cursor-default transition-colors" title="Forward">
|
||||
▶
|
||||
</button>
|
||||
<button onClick={() => navReload(win.id, d)} disabled={d.pageLoading}
|
||||
className="w-5 h-5 flex items-center justify-center rounded text-[10px] text-muted-foreground hover:text-foreground disabled:opacity-30 disabled:cursor-default transition-colors" title="Reload">
|
||||
↻
|
||||
</button>
|
||||
{/* Address */}
|
||||
<div className="flex-1 flex items-center h-5 px-2 bg-background/60 border border-border rounded text-[10px] font-mono text-foreground/80 truncate">
|
||||
<span className="text-muted-foreground/60 truncate">{d.node.hash.slice(0, 12)}…/</span>{d.currentPath}
|
||||
</div>
|
||||
<span className="flex items-center gap-1 text-[9px] uppercase tracking-wider shrink-0">
|
||||
{d.pageLoading ? <span className="text-muted-foreground animate-pulse">loading</span>
|
||||
: d.pageError ? <><span className="w-1.5 h-1.5 rounded-full bg-destructive inline-block" /><span className="text-destructive">error</span></>
|
||||
: d.pageHtml ? <><span className="w-1.5 h-1.5 rounded-full bg-green-500 inline-block" /><span className="text-muted-foreground">ok</span></> : null}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
footer={
|
||||
<div className="flex items-center gap-3 px-3 py-1 border-t border-border shrink-0 bg-muted/15 rounded-b-lg">
|
||||
<span className="text-[9px] text-muted-foreground uppercase tracking-wider truncate">{d.node.type ?? "peer"}</span>
|
||||
{d.node.interface && <span className="text-[9px] text-muted-foreground uppercase tracking-wider truncate">via {d.node.interface}</span>}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<div className="p-3 h-full overflow-auto" onClick={(e) => handleContentClick(e, win.id, d)}>
|
||||
{d.pageLoading && <span className="text-muted-foreground text-xs animate-pulse">Requesting page...</span>}
|
||||
{d.pageError && <span className="text-destructive text-xs">{d.pageError}</span>}
|
||||
{d.pageHtml && <div className="font-mono text-[11px] leading-tight" dangerouslySetInnerHTML={{ __html: d.pageHtml }} />}
|
||||
</div>
|
||||
}
|
||||
footer={
|
||||
<div className="flex items-center gap-3 px-3 py-1 border-t border-border shrink-0 bg-muted/15 rounded-b-lg">
|
||||
<span className="text-[9px] text-muted-foreground uppercase tracking-wider truncate">{win.data.node.type ?? "peer"}</span>
|
||||
{win.data.node.interface && <span className="text-[9px] text-muted-foreground uppercase tracking-wider truncate">via {win.data.node.interface}</span>}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="p-3 h-full overflow-auto">
|
||||
{win.data.pageLoading && <span className="text-muted-foreground text-xs animate-pulse">Requesting page...</span>}
|
||||
{win.data.pageError && <span className="text-destructive text-xs">{win.data.pageError}</span>}
|
||||
{win.data.pageHtml && <div className="font-mono text-[11px] leading-tight" dangerouslySetInnerHTML={{ __html: win.data.pageHtml }} />}
|
||||
</div>
|
||||
</FloatingWindow>
|
||||
))}
|
||||
</FloatingWindow>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user