From 620415c824f3e1e2621a18de3561fbb1516b710f Mon Sep 17 00:00:00 2001 From: Parker TenBroeck <51721964+ParkerTenBroeck@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:32:28 -0500 Subject: [PATCH] semi finalized theming --- web/root/index.html | 42 +++++++-- web/root/src/controls.ts | 116 +++++++++++++++++++++++++ web/root/src/editor.ts | 4 +- web/root/src/main.ts | 5 +- web/root/src/splitters.ts | 6 +- web/root/src/theme.ts | 75 +++++++++++++++++ web/root/src/visualizer.ts | 88 +++++++++---------- web/root/style/controls.scss | 99 ++++++++++++++++++++++ web/root/style/editor.scss | 135 +++++++++++++++-------------- web/root/style/loading.scss | 60 ++++++------- web/root/style/style.scss | 56 +++++++----- web/root/style/terminal.scss | 58 ++++++------- web/root/style/themes.scss | 159 +++++++++++++++++++++++++++++++++++ web/root/style/tooltip.scss | 49 ++++++----- 14 files changed, 728 insertions(+), 224 deletions(-) create mode 100644 web/root/src/controls.ts create mode 100644 web/root/src/theme.ts create mode 100644 web/root/style/controls.scss create mode 100644 web/root/style/themes.scss diff --git a/web/root/index.html b/web/root/index.html index 0a4c747..efb9044 100644 --- a/web/root/index.html +++ b/web/root/index.html @@ -5,7 +5,7 @@ Automata - + @@ -28,20 +28,48 @@
- meow +
-
-
- - +
+
+ + + + + + + + + + + +
+
+
-
+
diff --git a/web/root/src/controls.ts b/web/root/src/controls.ts new file mode 100644 index 0000000..c4d2441 --- /dev/null +++ b/web/root/src/controls.ts @@ -0,0 +1,116 @@ +import {nodes, edges, network} from "./visualizer.ts" + +const togglePhysicsBtn = document.getElementById("togglePhysics") as HTMLButtonElement; +const resetLayoutBtn = document.getElementById("resetLayout") as HTMLButtonElement; +const playPauseBtn = document.getElementById("playPause") as HTMLButtonElement; +const stepBtn = document.getElementById("step") as HTMLButtonElement; +const speedSlider = document.getElementById("speed") as HTMLInputElement; +const speedLabel = document.getElementById("speedLabel") as HTMLSpanElement; +const resetSimBtn = document.getElementById("resetSim") as HTMLButtonElement; + + +function stepSimulation(): void { + console.log("step"); +} + +function resetSimulation(): void { + console.log("reset"); +} + +// ---- Physics toggle (styled label) ---- +function setPhysicsButtonUI(enabled: boolean) { + togglePhysicsBtn.classList.toggle("active", enabled); + togglePhysicsBtn.textContent = enabled ? "Physics: ON" : "Physics: OFF"; +} + +togglePhysicsBtn.onclick = () => { + const enabled = !togglePhysicsBtn.classList.contains("active"); + setPhysicsButtonUI(enabled); + network.setOptions({ physics: { enabled } }); +}; + +setPhysicsButtonUI(togglePhysicsBtn.classList.contains("active")); + +resetLayoutBtn.onclick = () => { + try { + nodes.forEach((n) => { + n.physics = true; + n.x = undefined; + n.y = undefined; + }); + network.setData({ nodes, edges }); + } catch { + // Last resort + network.setData({ nodes, edges }); + } + + // If physics button is OFF, keep it OFF (don’t surprise the user) + const physicsEnabled = togglePhysicsBtn.classList.contains("active"); + network.setOptions({ physics: { enabled: physicsEnabled } }); +}; + +// ---- Play/Pause + Speed ---- +let running = false; +let timer: number | null = null; + +// speed slider is "steps per second" +function getStepsPerSecond() { + return Math.max(1, Math.min(60, Number(speedSlider.value) || 10)); +} +function updateSpeedUI() { + speedLabel.textContent = `${getStepsPerSecond()}×`; +} +updateSpeedUI(); + +speedSlider.addEventListener("input", () => { + updateSpeedUI(); + if (running) restartTimer(); +}); + +function stopTimer() { + if (timer !== null) { + clearInterval(timer); + timer = null; + } +} + +function restartTimer() { + stopTimer(); + const sps = getStepsPerSecond(); + const intervalMs = Math.round(1000 / sps); + + timer = globalThis.window.setInterval(() => { + // If your step can throw, keep the interval alive: + try { stepSimulation(); } catch (e) { console.error(e); } + }, intervalMs); +} + +function setRunning(on: boolean) { + running = on; + playPauseBtn.textContent = running ? "⏸ Pause" : "▶ Play"; + playPauseBtn.classList.toggle("btn-primary", !running); + playPauseBtn.classList.toggle("btn-secondary", running); + + // Disable step while running (optional, but feels nice) + stepBtn.disabled = running; + + if (running) restartTimer(); + else stopTimer(); +} + +playPauseBtn.onclick = () => setRunning(!running); + +stepBtn.onclick = () => { + stepSimulation(); +}; + +resetSimBtn.onclick = () => { + // Stop if running + if (running) setRunning(false); + + // Reset + resetSimulation(); + + // Optional: re-enable Step after reset + stepBtn.disabled = false; +}; \ No newline at end of file diff --git a/web/root/src/editor.ts b/web/root/src/editor.ts index 7c96d26..27fdd82 100644 --- a/web/root/src/editor.ts +++ b/web/root/src/editor.ts @@ -8,13 +8,13 @@ import { ViewPlugin, lineNumbers, highlightActiveLineGutter, + highlightActiveLine } from "npm:@codemirror/view"; import { EditorState, StateField, Text } from "npm:@codemirror/state"; import { defaultKeymap, history, historyKeymap } from "npm:@codemirror/commands"; import { bracketMatching, indentOnInput } from "npm:@codemirror/language"; import { closeBrackets } from "npm:@codemirror/autocomplete"; -import { oneDark } from "npm:@codemirror/theme-one-dark"; import wasm from "./wasm.ts" @@ -292,9 +292,9 @@ const state = EditorState.create({ history(), indentOnInput(), bracketMatching(), + highlightActiveLine(), closeBrackets(), keymap.of([...defaultKeymap, ...historyKeymap]), - oneDark, analysisField, diagHover, diff --git a/web/root/src/main.ts b/web/root/src/main.ts index 26a43e6..7ab398b 100644 --- a/web/root/src/main.ts +++ b/web/root/src/main.ts @@ -1,4 +1,5 @@ - import "./editor.ts" import "./visualizer.ts" -import "./splitters.ts" \ No newline at end of file +import "./splitters.ts" +import "./controls.ts" +import "./theme.ts" \ No newline at end of file diff --git a/web/root/src/splitters.ts b/web/root/src/splitters.ts index 88f7525..a321282 100644 --- a/web/root/src/splitters.ts +++ b/web/root/src/splitters.ts @@ -65,7 +65,7 @@ function setFlexFill(pane: HTMLElement) { export function enableFlexSplitters() { // Horizontal: A | hSplit | B (top/split/bottom) - for (const splitter of document.querySelectorAll(".hSplit")) { + for (const splitter of document.querySelectorAll(".hSplit:not(.styleOnly)")) { const parent = splitter.parentElement as HTMLElement | null; if (!parent) continue; @@ -133,7 +133,7 @@ export function enableFlexSplitters() { } // Vertical: A | vSplit | B (left/split/right) - for (const splitter of document.querySelectorAll(".vSplit")) { + for (const splitter of document.querySelectorAll(".vSplit:not(.styleOnly)")) { const parent = splitter.parentElement as HTMLElement | null; if (!parent) continue; @@ -158,7 +158,7 @@ export function enableFlexSplitters() { // --split-default: 30% (right pane width) // --split-min-a: 220px (min left) // --split-min-b: 220px (min right) - const defPct = getVarPct(splitter, "--split-default", 30); + const defPct = getVarPct(splitter, "--split-default", 50); const minA = getVarPx(splitter, "--split-min-a", 220); const minB = getVarPx(splitter, "--split-min-b", 220); diff --git a/web/root/src/theme.ts b/web/root/src/theme.ts new file mode 100644 index 0000000..b55bc10 --- /dev/null +++ b/web/root/src/theme.ts @@ -0,0 +1,75 @@ +import { network } from "./visualizer.ts"; + +function cssVar(name: string, fallback = ""): string { + return getComputedStyle(document.documentElement) + .getPropertyValue(name) + .trim() || fallback; +} + +const themeBtn = document.getElementById("themeToggle") as HTMLButtonElement; + +type Theme = "dark" | "light"; + +function getPreferredTheme(): Theme { + // 1) saved preference + const saved = localStorage.getItem("theme"); + if (saved === "dark" || saved === "light") return saved; + + // 2) OS preference + const prefersLight = globalThis.window.matchMedia?.( + "(prefers-color-scheme: light)", + )?.matches; + return prefersLight ? "light" : "dark"; +} + +function setTheme(theme: Theme) { + document.documentElement.dataset.theme = theme; + localStorage.setItem("theme", theme); + + // update button label + themeBtn.textContent = theme === "dark" ? "🌙 Dark" : "☀️ Light"; + applyGraphTheme(); +} + +function toggleTheme() { + const current = (document.documentElement.dataset.theme as Theme) || "dark"; + setTheme(current === "dark" ? "light" : "dark"); +} + +// init +setTheme(getPreferredTheme()); + +// click handler +themeBtn.addEventListener("click", toggleTheme); + +// optional: respond to OS theme changes (only if user hasn't chosen a theme) +globalThis.window.matchMedia?.("(prefers-color-scheme: light)") + ?.addEventListener("change", () => { + if (localStorage.getItem("theme")) return; // user has chosen, don't override + setTheme(getPreferredTheme()); + }); + +export function applyGraphTheme() { + network.setOptions({ + nodes: { + color: { + background: cssVar("--graph-node-bg"), + border: cssVar("--graph-node-border"), + highlight: { + background: cssVar("--graph-node-active-bg"), + border: cssVar("--graph-node-active-border"), + }, + }, + font: { + color: cssVar("--graph-node-text"), + }, + }, + edges: { + color: { + color: cssVar("--graph-edge"), + highlight: cssVar("--graph-edge-active"), + hover: cssVar("--graph-edge-hover"), + }, + }, + }); +} diff --git a/web/root/src/visualizer.ts b/web/root/src/visualizer.ts index a6b6cf5..5a64693 100644 --- a/web/root/src/visualizer.ts +++ b/web/root/src/visualizer.ts @@ -3,11 +3,8 @@ // deno-lint-ignore no-import-prefix import * as vis from "npm:vis-network/standalone"; - - -const nodes = new vis.DataSet(); -const edges = new vis.DataSet(); - +export const nodes = new vis.DataSet(); +export const edges = new vis.DataSet(); const automaton = { states: ["q0", "q1"], @@ -18,29 +15,29 @@ const automaton = { { from: "q0", to: "q0", - label: "ε, z0 → A z0\n" + label: "ε, z0 → A z0\n", }, { from: "q0", to: "q0", - label: "ε, z0 → B z0" + label: "ε, z0 → B z0", }, { from: "q0", to: "q1", - label: "ε, z0 → z0" + label: "ε, z0 → z0", }, { from: "q1", to: "q1", - label: "a, A → ε" + label: "a, A → ε", }, { from: "q1", to: "q1", - label: "b, B → ε" - } - ] + label: "b, B → ε", + }, + ], }; function renderNode({ @@ -57,7 +54,6 @@ function renderNode({ ctx.save(); const r = style.size; - ctx.beginPath(); ctx.arc(x, y, r, 0, 2 * Math.PI); ctx.fillStyle = "red"; @@ -67,29 +63,27 @@ function renderNode({ ctx.stroke(); ctx.fillStyle = "black"; - ctx.textAlign = 'center'; + ctx.textAlign = "center"; ctx.fillText(label, x, y, r); - - ctx.textAlign = 'center'; - ctx.strokeStyle = 'white'; + ctx.textAlign = "center"; + ctx.strokeStyle = "white"; ctx.fillStyle = "black"; let cy = y - (r + 10); for (const part of "meow[]\nbeeep".split("\n").reverse()) { const metrics = ctx.measureText(part); - cy -= metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent; + cy -= metrics.actualBoundingBoxAscent + + metrics.actualBoundingBoxDescent; ctx.strokeText(part, x, cy); ctx.fillText(part, x, cy); } - ctx.restore(); }, nodeDimensions: { width: 20, height: 20 }, }; } - // Populate nodes for (const state of automaton.states) { nodes.add({ @@ -104,24 +98,31 @@ automaton.transitions.forEach((t, i) => { id: `e${i}`, from: t.from, to: t.to, - label: t.label + label: t.label, }); }); - -function chosen_edge(_: vis.ChosenNodeValues, id: vis.IdType,selected: boolean, hovered: boolean) { - console.log("edge", id, selected, hovered) +function chosen_edge( + _: vis.ChosenNodeValues, + id: vis.IdType, + selected: boolean, + hovered: boolean, +) { + console.log("edge", id, selected, hovered); } -function chosen_node(_: vis.ChosenNodeValues, id: vis.IdType,selected: boolean, hovered: boolean) { - console.log("node", id, selected, hovered) +function chosen_node( + _: vis.ChosenNodeValues, + id: vis.IdType, + selected: boolean, + hovered: boolean, +) { + console.log("node", id, selected, hovered); } - -const network: vis.Network = createGraph(); +export const network: vis.Network = createGraph(); function createGraph(): vis.Network { - const container = document.getElementById("graph")!; const network = new vis.Network( @@ -129,13 +130,15 @@ function createGraph(): vis.Network { { nodes, edges }, { layout: { improvedLayout: true }, - autoResize: true, - width: "99%", physics: { enabled: true, solver: "barnesHut", - barnesHut: { gravitationalConstant: -8000, springLength: 120, springConstant: 0.04 }, - stabilization: { iterations: 200 } + barnesHut: { + gravitationalConstant: -8000, + springLength: 120, + springConstant: 0.04, + }, + stabilization: { iterations: 200 }, }, interaction: { dragNodes: true, @@ -149,7 +152,7 @@ function createGraph(): vis.Network { color: { background: "#1f6feb", border: "#79c0ff", - highlight: { background: "#388bfd", border: "#a5d6ff" } + highlight: { background: "#388bfd", border: "#a5d6ff" }, }, // @ts-expect-error bad library chosen: { @@ -162,7 +165,7 @@ function createGraph(): vis.Network { edges: { chosen: { // @ts-expect-error bad library - edge: chosen_edge + edge: chosen_edge, }, arrowStrikethrough: false, font: { align: "middle", color: "#000000ff" }, @@ -170,20 +173,19 @@ function createGraph(): vis.Network { // @ts-expect-error bad library smooth: { type: "dynamic" }, arrows: "to", - } - } + }, + }, ); - vis.DataSet + vis.DataSet; network.on("doubleClick", (params: any) => { - - for (const node_id of params.nodes){ + for (const node_id of params.nodes) { // @ts-expect-error bad library const node: vis.Node = nodes.get(node_id)!; node.physics = !node.physics; - nodes.update(node) + nodes.update(node); } }); - + return network; -} +} \ No newline at end of file diff --git a/web/root/style/controls.scss b/web/root/style/controls.scss new file mode 100644 index 0000000..71af4aa --- /dev/null +++ b/web/root/style/controls.scss @@ -0,0 +1,99 @@ +.controls { + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-2); + user-select: none; +} + +.controls .spacer { + flex: 1; +} + +.btn { + appearance: none; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.06); + color: var(--fg-0); + padding: 8px 12px; + border-radius: var(--radius-md); + font: + 600 13px/1.1 + var(--font-ui); + cursor: pointer; + + transition: + transform 0.04s ease, + background var(--dur-med) var(--ease-standard), + border-color var(--dur-med) var(--ease-standard), + opacity var(--dur-med) var(--ease-standard); + + &:hover { + background: rgba(255, 255, 255, 0.10); + border-color: rgba(255, 255, 255, 0.20); + } + + &:active { + transform: translateY(1px); + } + + &:disabled { + opacity: 0.45; + cursor: not-allowed; + } +} + +.btn-green { + background: color-mix(in srgb, var(--success) 18%, transparent); + border-color: color-mix(in srgb, var(--success) 35%, transparent); + + &:hover { + background: color-mix(in srgb, var(--success) 26%, transparent); + } +} + +.btn-blue { + background: color-mix(in srgb, var(--accent) 14%, transparent); + border-color: color-mix(in srgb, var(--accent) 40%, transparent); + + &:hover { + background: color-mix(in srgb, var(--accent) 22%, transparent); + } +} + +.btn-grey { + background: color-mix(in srgb, var(--accent) 12%, transparent); + border-color: color-mix(in srgb, var(--accent) 28%, transparent); + + &:hover { + background: color-mix(in srgb, var(--accent) 18%, transparent); + } +} + +.btn-toggle.active { + background: color-mix(in srgb, var(--warning) 14%, transparent); + border-color: color-mix(in srgb, var(--warning) 30%, transparent); +} + + +.speed { + display: flex; + align-items: center; + gap: var(--space-1); + padding: 6px 10px; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.04); + border-radius: var(--radius-md); + font: 600 12.5px var(--font-ui); + color: var(--fg-0); +} + +.speed input[type="range"] { + width: 160px; +} + +.speed #speedLabel { + min-width: 40px; + text-align: right; + opacity: 0.9; +} diff --git a/web/root/style/editor.scss b/web/root/style/editor.scss index 85299d4..ccb0e06 100644 --- a/web/root/style/editor.scss +++ b/web/root/style/editor.scss @@ -1,126 +1,131 @@ @use "tooltip.scss"; +/* Editor layout */ .editor { - height: 100%; - width: 100%; -} - - -.cm-editor { - height: 100%; + height: 100%; + width: 100%; } .cm-scroller { - overflow-y: auto !important; + overflow-y: auto !important; + background: var(--bg-0); } - - -.diag { - margin: 0; - padding-left: 18px; +.cm-editor { + height: 100%; + background: var(--bg-1); + color: var(--fg-0); } -.diag li { - margin: 6px 0; +.cm-gutters { + background: var(--bg-2) !important; + color: var(--fg-muted); + border-right: 1px solid color-mix(in srgb, var(--fg-muted) 20%, transparent)!important; } -/* --- Syntax colors via CSS classes applied by decorations --- */ +.cm-lineNumbers .cm-gutterElement { + padding: 0 10px 0 6px; + font-family: var(--font-mono); + font-size: 12px; +} + +.cm-activeLine { + background: color-mix(in srgb, var(--accent) 6%, transparent)!important; +} + +.cm-activeLineGutter { + background: color-mix(in srgb, var(--accent) 8%, transparent)!important; + color: var(--fg-0); +} + +.cm-cursor { + border-left: 2px solid var(--accent)!important; +} + +.cm-focused .cm-cursor { + border-left-color: var(--accent)!important; +} + +/* Syntax colors */ + .tok-comment { - color: #1a7b24; + color: color-mix(in srgb, var(--success) 65%, var(--fg-muted)); } .tok-keyword { - color: #b99400; - font-weight: 600; + color: var(--warning); + font-weight: 600; } .tok-error { - color: #ff0505; - font-weight: 1000; + color: var(--error); + font-weight: 700; } .tok-ident { - color: #90d4e0; + color: var(--accent); } .tok-brace { - color: #d73a49; - font-weight: 600; + color: var(--error); + font-weight: 600; } .tok-punc { - color: #ffffff; + color: var(--fg-0); } .tok-string { - color: #03621e; + color: color-mix(in srgb, var(--success) 75%, transparent); } -/* Rainbow bracket depth classes */ +/* Rainbow brackets */ + .rb-0 { - color: #a35; - font-weight: 700; + color: color-mix(in srgb, var(--error) 85%, transparent); + font-weight: 700; } .rb-1 { - color: #ed0; - font-weight: 700; + color: color-mix(in srgb, var(--warning) 85%, transparent); + font-weight: 700; } .rb-2 { - color: #9d5; - font-weight: 700; + color: color-mix(in srgb, var(--success) 85%, transparent); + font-weight: 700; } .rb-3 { - color: #2cb; - font-weight: 700; + color: color-mix(in srgb, var(--accent) 85%, transparent); + font-weight: 700; } .rb-4 { - color: #36b; - font-weight: 700; + color: color-mix(in srgb, var(--focus) 85%, transparent); + font-weight: 700; } .rb-5 { - color: #639; - font-weight: 700; + color: color-mix(in srgb, var(--accent) 60%, var(--fg-muted)); + font-weight: 700; } +/* Severity underline styles*/ - -/* Optional: diagnostics panel coloring */ -.diag li.error { - color: #d73a49; -} - -.diag li.warning { - color: #b08800; -} - -.diag li.info { - color: #0366d6; -} - - - -/* Severity underline styles */ .cm-diag-error { - text-decoration: underline wavy #d73a49; - /* red */ - text-underline-offset: 2px; + text-decoration: underline wavy var(--error); + text-underline-offset: 2px; } .cm-diag-warning { - text-decoration: underline wavy #ffd33d; - /* yellow */ - text-underline-offset: 2px; + text-decoration: underline wavy var(--warning); + text-underline-offset: 2px; } .cm-diag-info { - text-decoration: underline wavy #79c0ff; - /* cyan-ish */ - text-underline-offset: 2px; + text-decoration: underline wavy var(--accent); + text-underline-offset: 2px; } + diff --git a/web/root/style/loading.scss b/web/root/style/loading.scss index 822e310..81a0502 100644 --- a/web/root/style/loading.scss +++ b/web/root/style/loading.scss @@ -1,41 +1,41 @@ .centered { - margin-right: auto; - margin-left: auto; - display: block; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - color: #f0f0f0; - font-size: 24px; - font-family: Ubuntu-Light, Helvetica, sans-serif; - text-align: center; + margin-right: auto; + margin-left: auto; + display: block; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: #f0f0f0; + font-size: 24px; + font-family: Ubuntu-Light, Helvetica, sans-serif; + text-align: center; } .lds-dual-ring { - display: inline-block; - width: 24px; - height: 24px; + display: inline-block; + width: 24px; + height: 24px; } .lds-dual-ring:after { - content: " "; - display: block; - width: 24px; - height: 24px; - margin: 0px; - border-radius: 50%; - border: 3px solid #fff; - border-color: #fff transparent #fff transparent; - animation: lds-dual-ring 1.2s linear infinite; + content: " "; + display: block; + width: 24px; + height: 24px; + margin: 0px; + border-radius: 50%; + border: 3px solid #fff; + border-color: #fff transparent #fff transparent; + animation: lds-dual-ring 1.2s linear infinite; } @keyframes lds-dual-ring { - 0% { - transform: rotate(0deg); - } + 0% { + transform: rotate(0deg); + } - 100% { - transform: rotate(360deg); - } -} \ No newline at end of file + 100% { + transform: rotate(360deg); + } +} diff --git a/web/root/style/style.scss b/web/root/style/style.scss index f5b65f3..928212e 100644 --- a/web/root/style/style.scss +++ b/web/root/style/style.scss @@ -1,46 +1,60 @@ @use "editor.scss"; @use "terminal.scss"; @use "loading.scss"; +@use "controls.scss"; +@use "themes.scss"; html, body { - height: 100%; - width: 100%; - margin: 0; - font-family: system-ui, sans-serif; - background: #909090; + height: 100%; + width: 100%; + margin: 0; + color: var(--fg-0); + font-family: var(--font-ui); + background: #909090; } .app { - height: 100vh; - width: 100vw; - overflow: hidden; + height: 100vh; + width: 100vw; + overflow: hidden; + background-color: var(--bg-0); } .graph { - width: 100%; - height: 100%; - background: #111; + width: 100%; + height: 100%; + background: var(--graph-bg); } .vscroll { - height: 100%; - overflow-x: scroll; + height: 100%; + overflow-x: scroll; } .hSplit { - cursor: row-resize; + :not( .styleOnly){ + cursor: col-resize; + } height: 8px; - background: rgba(255, 255, 255, 0.06); + background: var(--separator-bg); + transition: + background var(--dur-med) var(--ease-standard), + box-shadow var(--dur-fast) var(--ease-standard); } .vSplit { - cursor: col-resize; + :not( .styleOnly){ + cursor: col-resize; + } width: 8px; - background: rgba(255, 255, 255, 0.06); + background: var(--separator-bg); + transition: + background var(--dur-med) var(--ease-standard), + box-shadow var(--dur-fast) var(--ease-standard); } -.hSplit:hover, -.vSplit:hover { - background: rgba(121, 192, 255, 0.25); -} \ No newline at end of file +.hSplit:hover:not(.styleOnly), +.vSplit:hover:not(.styleOnly) { + background: var(--separator-hover); +} diff --git a/web/root/style/terminal.scss b/web/root/style/terminal.scss index f8cf528..1040c5a 100644 --- a/web/root/style/terminal.scss +++ b/web/root/style/terminal.scss @@ -1,9 +1,9 @@ .terminal { - background: #0b0f14; - color: #c9d1d9; + background: var(--bg-1); + color: var(--fg-0); padding: 1em; margin: 0px; - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; + font-family: var(--font-mono); font-size: 12.5px; line-height: 1.35; white-space: pre-wrap; @@ -18,31 +18,31 @@ .ansi-bold { font-weight: 700; } .ansi-dim { opacity: 0.7; } -/* Foreground colors (standard + bright) */ -.ansi-fg-30 { color: #0b0f14; } /* black */ -.ansi-fg-31 { color: #ff7b72; } /* red */ -.ansi-fg-32 { color: #7ee787; } /* green */ -.ansi-fg-33 { color: #f2cc60; } /* yellow */ -.ansi-fg-34 { color: #79c0ff; } /* blue */ -.ansi-fg-35 { color: #d2a8ff; } /* magenta */ -.ansi-fg-36 { color: #a5d6ff; } /* cyan */ -.ansi-fg-37 { color: #c9d1d9; } /* white */ +/* Foreground colors */ +.ansi-fg-30 { color: var(--ansi-fg-30); } +.ansi-fg-31 { color: var(--ansi-fg-31); } +.ansi-fg-32 { color: var(--ansi-fg-32); } +.ansi-fg-33 { color: var(--ansi-fg-33); } +.ansi-fg-34 { color: var(--ansi-fg-34); } +.ansi-fg-35 { color: var(--ansi-fg-35); } +.ansi-fg-36 { color: var(--ansi-fg-36); } +.ansi-fg-37 { color: var(--ansi-fg-37); } -.ansi-fg-90 { color: #6e7681; } /* bright black / gray */ -.ansi-fg-91 { color: #ffa198; } -.ansi-fg-92 { color: #a6f3a6; } -.ansi-fg-93 { color: #ffe082; } -.ansi-fg-94 { color: #a5d6ff; } -.ansi-fg-95 { color: #e3b8ff; } -.ansi-fg-96 { color: #c7f0ff; } -.ansi-fg-97 { color: #ffffff; } +.ansi-fg-90 { color: var(--ansi-fg-90); } +.ansi-fg-91 { color: var(--ansi-fg-91); } +.ansi-fg-92 { color: var(--ansi-fg-92); } +.ansi-fg-93 { color: var(--ansi-fg-93); } +.ansi-fg-94 { color: var(--ansi-fg-94); } +.ansi-fg-95 { color: var(--ansi-fg-95); } +.ansi-fg-96 { color: var(--ansi-fg-96); } +.ansi-fg-97 { color: var(--ansi-fg-97); } -/* Background colors (optional) */ -.ansi-bg-40 { background: #0b0f14; } -.ansi-bg-41 { background: rgba(255, 123, 114, 0.22); } -.ansi-bg-42 { background: rgba(126, 231, 135, 0.18); } -.ansi-bg-43 { background: rgba(242, 204, 96, 0.18); } -.ansi-bg-44 { background: rgba(121, 192, 255, 0.18); } -.ansi-bg-45 { background: rgba(210, 168, 255, 0.18); } -.ansi-bg-46 { background: rgba(165, 214, 255, 0.18); } -.ansi-bg-47 { background: rgba(201, 209, 217, 0.10); } \ No newline at end of file +/* Background colors */ +.ansi-bg-40 { background: var(--ansi-bg-40); } +.ansi-bg-41 { background: var(--ansi-bg-41); } +.ansi-bg-42 { background: var(--ansi-bg-42); } +.ansi-bg-43 { background: var(--ansi-bg-43); } +.ansi-bg-44 { background: var(--ansi-bg-44); } +.ansi-bg-45 { background: var(--ansi-bg-45); } +.ansi-bg-46 { background: var(--ansi-bg-46); } +.ansi-bg-47 { background: var(--ansi-bg-47); } \ No newline at end of file diff --git a/web/root/style/themes.scss b/web/root/style/themes.scss new file mode 100644 index 0000000..4a6de23 --- /dev/null +++ b/web/root/style/themes.scss @@ -0,0 +1,159 @@ +:root { + color-scheme: dark; +} +@media (prefers-color-scheme: light) { + :root:not([data-theme]) { + color-scheme: light; + } +} + +:root { + --space-0: 0; + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 24px; + --space-6: 32px; + + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 14px; + + --font-ui: + ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; + --font-mono: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono"; + + --dur-fast: 80ms; + --dur-med: 160ms; + --dur-slow: 240ms; + --ease-standard: cubic-bezier(0.2, 0, 0, 1); +} + +:root[data-theme="dark"] { + color-scheme: dark; + + --bg-0: #0d1117; + --bg-1: #161b22; + --bg-2: #21262d; + + --fg-0: #e6edf3; + --fg-1: #c9d1d9; + --fg-muted: #8b949e; + + --separator-bg: rgba(255, 255, 255, 0.06); + --separator-hover: rgba(121, 192, 255, 0.28); + --separator-active: rgba(121, 192, 255, 0.45); + + --accent: #79c0ff; + --focus: #388bfd; + --success: #2ea043; + --warning: #f2cc60; + --error: #f85149; + + --graph-bg: var(--bg-0); + + --graph-node-bg: #1f6feb; + --graph-node-border: #388bfd; + --graph-node-text: #e6edf3; + + --graph-node-active-bg: #79c0ff; + --graph-node-active-border: #a5d6ff; + + --graph-edge: rgba(201, 209, 217, 0.55); + --graph-edge-hover: #79c0ff; + --graph-edge-active: #a5d6ff; + + + --ansi-fg-30: #0b0f14; /* black */ + --ansi-fg-31: #ff7b72; /* red */ + --ansi-fg-32: #7ee787; /* green */ + --ansi-fg-33: #f2cc60; /* yellow */ + --ansi-fg-34: #79c0ff; /* blue */ + --ansi-fg-35: #d2a8ff; /* magenta */ + --ansi-fg-36: #a5d6ff; /* cyan */ + --ansi-fg-37: #c9d1d9; /* white */ + + --ansi-fg-90: #6e7681; /* bright black / gray */ + --ansi-fg-91: #ffa198; + --ansi-fg-92: #a6f3a6; + --ansi-fg-93: #ffe082; + --ansi-fg-94: #a5d6ff; + --ansi-fg-95: #e3b8ff; + --ansi-fg-96: #c7f0ff; + --ansi-fg-97: #ffffff; + + --ansi-bg-40: #0b0f14; + --ansi-bg-41: rgba(255, 123, 114, 0.22); + --ansi-bg-42: rgba(126, 231, 135, 0.18); + --ansi-bg-43: rgba(242, 204, 96, 0.18); + --ansi-bg-44: rgba(121, 192, 255, 0.18); + --ansi-bg-45: rgba(210, 168, 255, 0.18); + --ansi-bg-46: rgba(165, 214, 255, 0.18); + --ansi-bg-47: rgba(201, 209, 217, 0.10); +} + +:root[data-theme="light"] { + color-scheme: light; + + --bg-0: #f6f8fa; + --bg-1: #ffffff; + --bg-2: #eaeef2; + + --fg-0: #0b1220; + --fg-1: #1f2937; + --fg-muted: #5b6472; + + --separator-bg: rgba(0, 0, 0, 0.06); + --separator-hover: rgba(9, 105, 218, 0.28); + --separator-active: rgba(9, 105, 218, 0.45); + + --accent: #1f6feb; + --focus: #0969da; + --success: #1a7f37; + --warning: #9a6700; + --error: #cf222e; + + --graph-bg: var(--bg-0); + + --graph-node-bg: #1f6feb; + --graph-node-border: #0969da; + --graph-node-text: #ffffff; + + --graph-node-active-bg: #54aeff; + --graph-node-active-border: #0969da; + + --graph-edge: rgba(31, 41, 55, 0.45); + --graph-edge-hover: #0969da; + --graph-edge-active: #54aeff; + + + + --ansi-fg-30: #111827; /* black */ + --ansi-fg-31: #b42318; /* red */ + --ansi-fg-32: #1a7f37; /* green */ + --ansi-fg-33: #9a6700; /* yellow */ + --ansi-fg-34: #0969da; /* blue */ + --ansi-fg-35: #8250df; /* magenta */ + --ansi-fg-36: #0550ae; /* cyan */ + --ansi-fg-37: #1f2937; /* white (dark text) */ + + --ansi-fg-90: #6b7280; /* bright black / gray */ + --ansi-fg-91: #cf222e; + --ansi-fg-92: #1f883d; + --ansi-fg-93: #9a6700; + --ansi-fg-94: #0969da; + --ansi-fg-95: #8250df; + --ansi-fg-96: #0550ae; + --ansi-fg-97: #030712; + + --ansi-bg-40: #ffffff; + --ansi-bg-41: rgba(180, 35, 24, 0.16); + --ansi-bg-42: rgba(26, 127, 55, 0.14); + --ansi-bg-43: rgba(154, 103, 0, 0.14); + --ansi-bg-44: rgba(9, 105, 218, 0.14); + --ansi-bg-45: rgba(130, 80, 223, 0.14); + --ansi-bg-46: rgba(5, 80, 174, 0.14); + --ansi-bg-47: rgba(0, 0, 0, 0.06); +} diff --git a/web/root/style/tooltip.scss b/web/root/style/tooltip.scss index f47a5af..02fa4da 100644 --- a/web/root/style/tooltip.scss +++ b/web/root/style/tooltip.scss @@ -1,32 +1,37 @@ -.tipTitle.error { - color: #d73a49; -} +.tipTitle { + font-weight: 700; + margin-bottom: 4px; -.tipTitle.warning { - color: #ffd33d; -} + &.error { + color: var(--error); + } -.tipTitle.info { - color: #79c0ff; -} + &.warning { + color: var(--warning); + } + &.info { + color: var(--accent); + } +} .cm-tooltip.cm-tooltip-hover { - border: 1px solid #ddd; - background: black; - box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12); - border-radius: 10px; - padding: 8px 10px; - max-width: 420px; - font-size: 13px; - line-height: 1.35; -} + border: 1px solid color-mix(in srgb, var(--fg-muted) 35%, transparent); + background: var(--bg-1); + color: var(--fg-0); -.tipTitle { - font-weight: 700; - margin-bottom: 4px; + box-shadow: + 0 8px 30px rgba(0, 0, 0, 0.25); + + border-radius: var(--radius-md); + padding: 8px 10px; + + max-width: 420px; + font-size: 13px; + line-height: 1.35; } .tipBody { - white-space: pre-wrap; + white-space: pre-wrap; + color: var(--fg-1); } \ No newline at end of file