From 3c5856aecb19bed86959a7b5090a54d1ec1ee09a Mon Sep 17 00:00:00 2001 From: dtoro Date: Sun, 5 Apr 2026 11:01:48 +0200 Subject: [PATCH] feat: link resolver and spinner --- frontend/src/assets/loading.min.svg | 1 + .../components/browse/BrowseNodeWindow.tsx | 3 +- .../src/components/editor/PreviewPane.tsx | 12 +++++- frontend/src/components/shared/Loader.tsx | 27 ++++++++++++ frontend/src/index.css | 11 +++++ frontend/src/routes/BrowseView.tsx | 43 ++++++++++++++++--- frontend/src/routes/ComposeView.tsx | 3 +- 7 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 frontend/src/assets/loading.min.svg create mode 100644 frontend/src/components/shared/Loader.tsx diff --git a/frontend/src/assets/loading.min.svg b/frontend/src/assets/loading.min.svg new file mode 100644 index 0000000..1c52b7d --- /dev/null +++ b/frontend/src/assets/loading.min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/browse/BrowseNodeWindow.tsx b/frontend/src/components/browse/BrowseNodeWindow.tsx index e710f26..9ae36c7 100644 --- a/frontend/src/components/browse/BrowseNodeWindow.tsx +++ b/frontend/src/components/browse/BrowseNodeWindow.tsx @@ -1,4 +1,5 @@ import FloatingWindow from "@/components/shared/FloatingWindow"; +import Loader from "@/components/shared/Loader"; import type { ManagedWindow } from "@/hooks/useWindowManager"; import type { BrowseWinData } from "./types"; @@ -65,7 +66,7 @@ export default function BrowseNodeWindow({ > {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
onContentClick(e, win.id, d)}> - {d.pageLoading && Requesting page...} + {d.pageLoading &&
Requesting page...
} {d.pageError && {d.pageError}} {d.pageHtml &&
}
diff --git a/frontend/src/components/editor/PreviewPane.tsx b/frontend/src/components/editor/PreviewPane.tsx index d1efe1f..88f9559 100644 --- a/frontend/src/components/editor/PreviewPane.tsx +++ b/frontend/src/components/editor/PreviewPane.tsx @@ -1,6 +1,8 @@ +import { useCallback } from "react"; import { useEditorCtx } from "./EditorStoreContext"; import { renderMicron } from "./micronRenderer"; import { cn } from "@/lib/utils"; +import Loader from "@/components/shared/Loader"; type PreviewMode = "micron" | "raw" | "script"; @@ -13,6 +15,11 @@ export default function PreviewPane() { const isCompiling = useEditorCtx((s) => s.isCompiling); const compileError = useEditorCtx((s) => s.compileError); + const handlePreviewClick = useCallback((e: React.MouseEvent) => { + const anchor = (e.target as HTMLElement).closest("a"); + if (anchor) e.preventDefault(); + }, []); + const tabs: { value: PreviewMode; label: string; show: boolean }[] = [ { value: "micron", label: "Micron", show: true }, { value: "raw", label: "Raw", show: true }, @@ -28,7 +35,7 @@ export default function PreviewPane() { )} {isCompiling && ( - + )} {compileError && ( @@ -57,7 +64,8 @@ export default function PreviewPane() { ))}
-
+ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} +
{previewMode === "micron" ? ( compiledMicron ? (
+ ); +} diff --git a/frontend/src/index.css b/frontend/src/index.css index 94456e5..c70c75b 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -15,6 +15,17 @@ } } +@keyframes spin-slow { + 0% { transform: rotate(0deg); } + 60% { transform: rotate(380deg); } + 80% { transform: rotate(355deg); } + 100% { transform: rotate(360deg); } +} + +@utility animate-spin-slow { + animation: spin-slow 2s cubic-bezier(0.4, 0, 0.2, 1) infinite; +} + @theme inline { --font-heading: var(--font-mono); --font-sans: 'JetBrains Mono Variable', 'Courier New', monospace; diff --git a/frontend/src/routes/BrowseView.tsx b/frontend/src/routes/BrowseView.tsx index e62b7f8..f4d7c42 100644 --- a/frontend/src/routes/BrowseView.tsx +++ b/frontend/src/routes/BrowseView.tsx @@ -8,6 +8,7 @@ import { buildGraphArrays, SPACE_SIZE } from "@/components/browse/buildGraph"; import type { BrowseWinData, HistoryEntry } from "@/components/browse/types"; import BrowseNodeWindow from "@/components/browse/BrowseNodeWindow"; import BrowseSearchBar from "@/components/browse/BrowseSearchBar"; +import Loader from "@/components/shared/Loader"; // --------------------------------------------------------------------------- // Main component @@ -355,6 +356,13 @@ export default function BrowseView() { }, [openWindow, navigateTo]); // ── Handle micron link clicks via event delegation ── + // + // Micron link destinations come in several forms: + // /page.mu — same-node, absolute path + // page.mu — same-node, relative + // :/page/page.mu — NomadNet "request" link (: prefix + /page/ segment) + // <32-hex-hash>/page.mu — cross-node link + // nomadnetwork://... — already stripped by micron-parser's data-destination const handleContentClick = useCallback((e: React.MouseEvent, winId: string, data: BrowseWinData) => { const anchor = (e.target as HTMLElement).closest("a"); if (!anchor) return; @@ -363,11 +371,35 @@ export default function BrowseView() { const dest = anchor.getAttribute("data-destination") ?? anchor.getAttribute("href") ?? ""; if (!dest) return; - let path = dest.replace(/^nomadnetwork:\/\//, "").replace(/^\/+/, ""); - if (/^[0-9a-f]{32}$/i.test(path)) return; + let raw = dest + .replace(/^nomadnetwork:\/\//, "") // strip scheme if present + .replace(/^:/, "") // strip NomadNet request prefix + .replace(/^\/page\//, "") // strip /page/ path segment + .replace(/^\/+/, ""); // strip remaining leading slashes + + // A bare 32-char hex hash with no path — nothing to navigate to + if (/^[0-9a-f]{32}$/i.test(raw)) return; + + // If the destination starts with a 32-char hex hash followed by "/", + // it's a cross-node link: /page.mu → use that node's hash + let targetNode = data.node; + let path = raw; + const crossNodeMatch = raw.match(/^([0-9a-f]{32})\/(.+)$/i); + if (crossNodeMatch) { + const targetHash = crossNodeMatch[1]!; + path = crossNodeMatch[2]!; + // Strip /page/ from the path portion too + path = path.replace(/^\/page\//, "").replace(/^\/+/, ""); + const known = nodesMapRef.current.get(targetHash); + if (known) { + targetNode = known; + } + } + + if (!path || path === "/") return; if (!path.endsWith(".mu")) path += ".mu"; - navigateTo(winId, data.node, path, data); + navigateTo(winId, targetNode, path, data); }, [navigateTo]); const clearSearch = useCallback(() => { @@ -400,8 +432,9 @@ export default function BrowseView() { /> {nodes.length === 0 && ( -
- Listening for nodes on the Reticulum network... +
+ + Connecting...
)} diff --git a/frontend/src/routes/ComposeView.tsx b/frontend/src/routes/ComposeView.tsx index 490551a..64b0014 100644 --- a/frontend/src/routes/ComposeView.tsx +++ b/frontend/src/routes/ComposeView.tsx @@ -5,6 +5,7 @@ import { usePagesStore } from "@/stores/pagesStore"; import * as api from "@/api/client"; import { useKeyboardSave } from "@/hooks/useKeyboardSave"; import StatusBadge from "@/components/dashboard/StatusBadge"; +import Loader from "@/components/shared/Loader"; import { Button } from "@/components/ui/button"; import { Table, @@ -259,7 +260,7 @@ export default function ComposeView() { {/* File table */}
{isLoading ? ( -
Loading...
+
Loading...
) : (