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