feat: style

This commit is contained in:
2026-04-03 22:10:26 +02:00
parent 234081db09
commit b878068ab3
10 changed files with 191 additions and 74 deletions

View File

@@ -3,6 +3,7 @@ import AppShell from "./components/shared/AppShell";
import ComposeView from "./routes/ComposeView";
import BrowseView from "./routes/BrowseView";
import EditorView from "./routes/EditorView";
import SettingsView from "./routes/SettingsView";
export default function App() {
return (
@@ -10,6 +11,7 @@ export default function App() {
<Routes>
<Route path="/" element={<ComposeView />} />
<Route path="/browse" element={<BrowseView />} />
<Route path="/settings" element={<SettingsView />} />
<Route path="/editor/new" element={<EditorView />} />
<Route path="/editor/:name" element={<EditorView />} />
</Routes>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 496 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 496 KiB

After

Width:  |  Height:  |  Size: 154 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 3.4 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 552 KiB

View File

@@ -4,6 +4,7 @@ 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.
* Uses a ResizeObserver to keep horizontal position synced on window resize.
*/
export default function EditorPointer() {
const [y, setY] = useState<number | null>(null);
@@ -12,19 +13,58 @@ export default function EditorPointer() {
const currentRef = useRef(0);
const rafRef = useRef(0);
const activeRef = useRef(false);
const [clickKey, setClickKey] = useState(0);
const clickTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
const cmRef = useRef<Element | null>(null);
const [eyeOffset, setEyeOffset] = useState({ x: 0, y: 0 });
const eyeTargetRef = useRef({ x: 0, y: 0 });
const eyeCurrentRef = useRef({ x: 0, y: 0 });
useEffect(() => {
const ease = 0.09;
const eyeEase = 0.06;
const updateLeft = () => {
if (cmRef.current) {
setEditorLeft(cmRef.current.getBoundingClientRect().left);
}
};
const onMouseMove = (e: MouseEvent) => {
const pointerX = (cmRef.current?.getBoundingClientRect().left ?? 0) - 100;
const pointerY = currentRef.current;
const dx = e.clientX - pointerX;
const dy = e.clientY - pointerY;
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
const maxShift = 1.5;
eyeTargetRef.current = {
x: (dx / dist) * maxShift,
y: (dy / dist) * maxShift,
};
};
window.addEventListener("mousemove", onMouseMove);
const onEditorClick = () => {
clearTimeout(clickTimerRef.current);
setClickKey((k) => k + 1);
clickTimerRef.current = setTimeout(() => setClickKey(0), 200);
};
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);
// Find editor container and attach click listener lazily
if (!cmRef.current) {
const cm = document.querySelector(".cm-editor");
if (cm) {
cmRef.current = cm;
cm.addEventListener("mousedown", onEditorClick);
ro.observe(cm);
}
}
updateLeft();
if (!activeRef.current) {
currentRef.current = top;
@@ -43,15 +83,34 @@ export default function EditorPointer() {
}
setY(currentRef.current);
}
// Lerp eyes toward target
const ec = eyeCurrentRef.current;
const et = eyeTargetRef.current;
ec.x += (et.x - ec.x) * eyeEase;
ec.y += (et.y - ec.y) * eyeEase;
setEyeOffset({ x: ec.x, y: ec.y });
rafRef.current = requestAnimationFrame(tick);
};
window.addEventListener("cm-cursor-move", onCursorMove);
rafRef.current = requestAnimationFrame(tick);
// Keep left position updated on resize
const ro = new ResizeObserver(() => updateLeft());
const existingCm = document.querySelector(".cm-editor");
if (existingCm) {
cmRef.current = existingCm;
ro.observe(existingCm);
}
window.addEventListener("resize", updateLeft);
return () => {
window.removeEventListener("cm-cursor-move", onCursorMove);
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("resize", updateLeft);
cmRef.current?.removeEventListener("mousedown", onEditorClick);
cancelAnimationFrame(rafRef.current);
ro.disconnect();
};
}, []);
@@ -59,7 +118,8 @@ export default function EditorPointer() {
return (
<div
className="editor-pointer"
className={`editor-pointer${clickKey ? " editor-pointer-click" : ""}`}
key={clickKey}
style={{ top: y, left: editorLeft }}
>
<div
@@ -69,6 +129,18 @@ export default function EditorPointer() {
maskImage: `url(${pointerSvg})`,
}}
/>
<div className="editor-pointer-eye" style={{ top: 33, left: 134 }}>
<div
className="editor-pointer-iris"
style={{ transform: `translate(${eyeOffset.x}px, ${eyeOffset.y}px)` }}
/>
</div>
<div className="editor-pointer-eye" style={{ top: 29, left: 144 }}>
<div
className="editor-pointer-iris"
style={{ transform: `translate(${eyeOffset.x}px, ${eyeOffset.y}px)` }}
/>
</div>
</div>
);
}

View File

@@ -2,8 +2,7 @@ import { useEffect, useState, type ReactNode } from "react";
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 frameSvg from "@/assets/frame.themed.svg";
import NavMenu from "./NavMenu";
const TITLE = `
@@ -99,8 +98,6 @@ export default function AppShell({ children }: { children: ReactNode }) {
localStorage.setItem("micronomicon-theme", theme);
}, [theme]);
const frameSvg = theme === "azure" ? frameAzureSvg : frameTerraSvg;
const toggleTheme = () => setTheme(t => t === "terra" ? "azure" : "terra");
return (
@@ -109,13 +106,21 @@ export default function AppShell({ children }: { children: ReactNode }) {
<main className="flex-1 overflow-auto" data-scroll-root>
<div className="relative max-w-5xl min-w-5xl mx-auto min-h-full">
{/* Frame SVG — background */}
<img
key={theme}
src={frameSvg}
alt=""
<div
aria-hidden
className="absolute inset-0 w-full min-w-full pointer-events-none select-none"
style={{ zIndex: 0, objectFit: "fill" }}
style={{
zIndex: 0,
height: "1315px",
background: "var(--primary)",
WebkitMaskImage: `url(${frameSvg})`,
maskImage: `url(${frameSvg})`,
WebkitMaskSize: "cover",
maskSize: "cover",
WebkitMaskRepeat: "no-repeat",
maskRepeat: "no-repeat",
objectFit: "fill"
}}
/>
{/* Top hole — ASCII title */}
@@ -147,7 +152,7 @@ export default function AppShell({ children }: { children: ReactNode }) {
className="absolute z-20"
style={{ top: 200, left: -210 }}
>
<NavMenu />
<NavMenu theme={theme} onToggleTheme={toggleTheme} />
</div>
)}
@@ -155,14 +160,6 @@ export default function AppShell({ children }: { children: ReactNode }) {
</main>
<footer className="flex items-center justify-center gap-3 text-[10px] text-muted-foreground py-4 shrink-0 tracking-widest uppercase">
<span>Becoming with hubris · MMXXVI</span>
<span className="text-muted-foreground/50"></span>
<button
onClick={toggleTheme}
className="hover:text-foreground transition-colors cursor-pointer"
title={`Switch to ${theme === "terra" ? "Azure" : "Terracotta"} theme`}
>
{theme === "terra" ? "◐ AZURE" : "◑ TERRA"}
</button>
</footer>
</div>
<Toaster />

View File

@@ -5,6 +5,7 @@ import menuSvg from "@/assets/menu.min.svg";
const NAV_ITEMS = [
{ label: "Browse", path: "/browse" },
{ label: "Compose", path: "/" },
{ label: "Settings", path: "/settings" },
] as const;
/**
@@ -47,7 +48,12 @@ function useSmoothScroll(scrollSelector: string, ease = 0.08) {
return offset;
}
export default function NavMenu() {
interface NavMenuProps {
theme: "terra" | "azure";
onToggleTheme: () => void;
}
export default function NavMenu({ theme, onToggleTheme }: NavMenuProps) {
const navigate = useNavigate();
const location = useLocation();
const scrollY = useSmoothScroll("[data-scroll-root]", 0.07);
@@ -82,6 +88,15 @@ export default function NavMenu() {
);
})}
</nav>
{/* Theme toggle — bottom container */}
<button
onClick={onToggleTheme}
className="nav-menu-theme-toggle"
title={`Switch to ${theme === "terra" ? "Azure" : "Terracotta"} theme`}
>
{theme === "terra" ? "◐ AZURE" : "◑ TERRA"}
</button>
</div>
);
}

View File

@@ -351,8 +351,9 @@
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
padding-top: 168px;
height: 100%;
border: none;
}
@@ -380,6 +381,27 @@
background: var(--primary) / 0.08;
}
.nav-menu-theme-toggle {
position: absolute;
bottom: 97px;
left: 50%;
transform: translateX(-50%);
z-index: 1;
font-family: var(--font-mono);
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted-foreground);
background: transparent;
border: none;
cursor: pointer;
transition: color 0.2s ease;
}
.nav-menu-theme-toggle:hover {
color: var(--foreground);
}
/* ── Editor Pointer ── */
.editor-pointer {
@@ -391,6 +413,24 @@
margin-top: -20px;
}
.editor-pointer-click {
animation: pointer-click 200ms ease-out;
}
@keyframes pointer-click {
0% {
transform: translateX(-100%);
}
40% {
transform: translateX(-96%);
}
100% {
transform: translateX(-100%);
}
}
.editor-pointer-img {
width: 200px;
height: 130px;
@@ -403,4 +443,26 @@
-webkit-mask-position: center;
opacity: 1;
}
.editor-pointer-eye {
position: absolute;
width: 5px;
height: 5px;
border-radius: 50%;
background: black;
pointer-events: none;
}
.editor-pointer-iris {
position: absolute;
width: 2px;
height: 2px;
border-radius: 50%;
background: var(--primary);
top: 50%;
left: 50%;
margin-top: -1.5px;
margin-left: -1.5px;
transition: transform 0.15s ease-out;
}
}

View File

@@ -0,0 +1,7 @@
export default function SettingsView() {
return (
<div className="flex items-center justify-center h-full text-muted-foreground">
Settings
</div>
);
}