+ {/* 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...
) : (