From 0e7e435edd884f7caa8917957d5cb14faf1facd9 Mon Sep 17 00:00:00 2001 From: dtoro Date: Fri, 3 Apr 2026 16:44:52 +0200 Subject: [PATCH] feat: menu and ponters --- frontend/src/assets/pointer.min.svg | 1 + frontend/src/components/editor/EditorPane.tsx | 13 +++ .../src/components/editor/EditorPointer.tsx | 74 +++++++++++++ .../src/components/editor/oneDarkTheme.ts | 5 +- frontend/src/components/shared/AppShell.tsx | 18 +++- frontend/src/components/shared/NavMenu.tsx | 87 +++++++++++++++ frontend/src/index.css | 100 ++++++++++++++++-- frontend/src/routes/EditorView.tsx | 2 + 8 files changed, 290 insertions(+), 10 deletions(-) create mode 100644 frontend/src/assets/pointer.min.svg create mode 100644 frontend/src/components/editor/EditorPointer.tsx create mode 100644 frontend/src/components/shared/NavMenu.tsx diff --git a/frontend/src/assets/pointer.min.svg b/frontend/src/assets/pointer.min.svg new file mode 100644 index 0000000..edc54d9 --- /dev/null +++ b/frontend/src/assets/pointer.min.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/editor/EditorPane.tsx b/frontend/src/components/editor/EditorPane.tsx index 013866b..9ac4e3a 100644 --- a/frontend/src/components/editor/EditorPane.tsx +++ b/frontend/src/components/editor/EditorPane.tsx @@ -31,11 +31,24 @@ export default function EditorPane({ value, onChange, extensions = [] }: Props) if (update.docChanged) { onChangeRef.current(update.state.doc.toString()); } + if (update.selectionSet || update.docChanged) { + const pos = update.state.selection.main.head; + const coords = update.view.coordsAtPos(pos); + if (coords) { + window.dispatchEvent( + new CustomEvent("cm-cursor-move", { detail: { top: coords.top, left: coords.left } }), + ); + } + } }), EditorView.theme({ "&": { height: "100%", fontSize: "14px" }, ".cm-scroller": { overflow: "auto" }, ".cm-content": { fontFamily: "monospace", padding: "16px" }, + ".cm-cursor, .cm-cursor-primary": { + borderLeftColor: "var(--primary)", + borderLeftWidth: "0.5em", + }, }), ...extensions, ], diff --git a/frontend/src/components/editor/EditorPointer.tsx b/frontend/src/components/editor/EditorPointer.tsx new file mode 100644 index 0000000..48de75b --- /dev/null +++ b/frontend/src/components/editor/EditorPointer.tsx @@ -0,0 +1,74 @@ +import { useEffect, useRef, useState } from "react"; +import pointerSvg from "@/assets/pointer.min.svg"; + +/** + * Floating pointer that tracks the CodeMirror cursor with smooth lerp animation. + * Positions itself right next to the left border of the editor container. + */ +export default function EditorPointer() { + const [y, setY] = useState(null); + const [editorLeft, setEditorLeft] = useState(null); + const targetRef = useRef(0); + const currentRef = useRef(0); + const rafRef = useRef(0); + const activeRef = useRef(false); + + useEffect(() => { + const ease = 0.09; + + const onCursorMove = (e: Event) => { + const { top } = (e as CustomEvent).detail; + targetRef.current = top; + + // Find editor container left edge once (or when it changes) + const cm = document.querySelector(".cm-editor"); + if (cm) { + setEditorLeft(cm.getBoundingClientRect().left); + } + + if (!activeRef.current) { + currentRef.current = top; + setY(top); + activeRef.current = true; + } + }; + + const tick = () => { + if (activeRef.current) { + const diff = targetRef.current - currentRef.current; + if (Math.abs(diff) < 0.5) { + currentRef.current = targetRef.current; + } else { + currentRef.current += diff * ease; + } + setY(currentRef.current); + } + rafRef.current = requestAnimationFrame(tick); + }; + + window.addEventListener("cm-cursor-move", onCursorMove); + rafRef.current = requestAnimationFrame(tick); + + return () => { + window.removeEventListener("cm-cursor-move", onCursorMove); + cancelAnimationFrame(rafRef.current); + }; + }, []); + + if (y === null || editorLeft === null) return null; + + return ( +
+
+
+ ); +} diff --git a/frontend/src/components/editor/oneDarkTheme.ts b/frontend/src/components/editor/oneDarkTheme.ts index 570fdce..ee7dbc6 100644 --- a/frontend/src/components/editor/oneDarkTheme.ts +++ b/frontend/src/components/editor/oneDarkTheme.ts @@ -6,8 +6,9 @@ export const oneDark = EditorView.theme( backgroundColor: "#0d1117", color: "#c9d1d9", }, - ".cm-cursor": { - borderLeftColor: "#c9d1d9", + ".cm-cursor, .cm-cursor-primary": { + borderLeftColor: "var(--primary)", + borderLeftWidth: "0.5em", }, ".cm-selectionBackground, &.cm-focused .cm-selectionBackground": { backgroundColor: "#264f78", diff --git a/frontend/src/components/shared/AppShell.tsx b/frontend/src/components/shared/AppShell.tsx index c0b9414..4fb5ed8 100644 --- a/frontend/src/components/shared/AppShell.tsx +++ b/frontend/src/components/shared/AppShell.tsx @@ -1,9 +1,10 @@ import { useEffect, useState, type ReactNode } from "react"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useLocation } from "react-router-dom"; import { TooltipProvider } from "@/components/ui/tooltip"; import { Toaster } from "@/components/ui/sonner"; import frameTerraSvg from "@/assets/frame.themed.svg"; import frameAzureSvg from "@/assets/frame.azure.svg"; +import NavMenu from "./NavMenu"; const TITLE = ` @@ -82,7 +83,9 @@ function RomanClock() { export default function AppShell({ children }: { children: ReactNode }) { const navigate = useNavigate(); + const location = useLocation(); const [theme, setTheme] = useState(getStoredTheme); + const isEditor = location.pathname.startsWith("/editor"); useEffect(() => { const root = document.documentElement; @@ -103,7 +106,7 @@ export default function AppShell({ children }: { children: ReactNode }) { return (
-
+
{/* Frame SVG — background */} {children}
+ + {/* Nav menu — anchored below the frame, left side (hidden on editor) */} + {!isEditor && ( +
+ +
+ )} +