Merge branch 'main' into gh-pages

This commit is contained in:
Parker TenBroeck 2026-01-07 16:41:25 -05:00
commit 11beb9cc1d
15 changed files with 730 additions and 226 deletions

View file

@ -73,8 +73,8 @@ git show-ref --verify --quiet "refs/heads/$PAGES_BRANCH" || {
echo "🌿 Switching to '$PAGES_BRANCH'..."
git switch -q "$PAGES_BRANCH"
echo "🔀 Fast-forward merging '$MAIN_BRANCH' into '$PAGES_BRANCH'..."
git merge --ff-only "$MAIN_BRANCH"
echo "🔀 Merging '$MAIN_BRANCH' into '$PAGES_BRANCH'..."
git merge "$MAIN_BRANCH"
# Copy dist -> docs (replace docs contents)
if [[ ! -d "dist" ]]; then

View file

@ -28,17 +28,45 @@
<div class="vSplit" style="--split-default: 20%" title="Drag to resize canvas width"></div>
<section>
meow
<button id="themeToggle" class="btn btn-grey" title="Toggle light/dark">
🌙 Dark
</button>
</section>
</section>
<div class="hSplit" style="--split-default: 50%" title="Drag to resize canvas height"></div>
<div style="padding-bottom: 10px;">
<div>
<button id="togglePhysics">Toggle Physics</button>
<button id="resetLayout">Reset Layout</button>
<div style="padding-bottom: 10px">
<div class="controls" style="background: var(--bg-0)">
<button id="togglePhysics" class="btn btn-toggle active" title="Toggle physics layout">
Physics: ON
</button>
<button id="resetLayout" class="btn btn-grey" title="Re-enable physics on nodes and reset layout">
Reset Layout
</button>
<span class="spacer"></span>
<button id="resetSim" class="btn btn-blue" title="Stop and reset simulation">
⟲ Reset
</button>
<button id="playPause" class="btn btn-green" title="Run / pause simulation">
▶ Play
</button>
<button id="step" class="btn btn-grey" title="Advance one step">
Step
</button>
<label class="speed">
Speed
<input id="speed" type="range" min="1" max="60" value="10" />
<span id="speedLabel">10x</span>
</label>
</div>
<div class="hSplit styleOnly" title="Drag to resize canvas height"></div>
<div style="height:100%">
<div class="vscroll">
<div id="editor" class="editor"></div>

116
web/root/src/controls.ts Normal file
View file

@ -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 (dont 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;
};

View file

@ -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,

View file

@ -1,4 +1,5 @@
import "./editor.ts"
import "./visualizer.ts"
import "./splitters.ts"
import "./controls.ts"
import "./theme.ts"

View file

@ -65,7 +65,7 @@ function setFlexFill(pane: HTMLElement) {
export function enableFlexSplitters() {
// Horizontal: A | hSplit | B (top/split/bottom)
for (const splitter of document.querySelectorAll<HTMLElement>(".hSplit")) {
for (const splitter of document.querySelectorAll<HTMLElement>(".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<HTMLElement>(".vSplit")) {
for (const splitter of document.querySelectorAll<HTMLElement>(".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);

75
web/root/src/theme.ts Normal file
View file

@ -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"),
},
},
});
}

View file

@ -3,11 +3,8 @@
// deno-lint-ignore no-import-prefix
import * as vis from "npm:vis-network/standalone";
const nodes = new vis.DataSet<vis.Node>();
const edges = new vis.DataSet<vis.Edge>();
export const nodes = new vis.DataSet<vis.Node>();
export const edges = new vis.DataSet<vis.Edge>();
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,18 +173,17 @@ 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);
}
});

View file

@ -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;
}

View file

@ -1,126 +1,131 @@
@use "tooltip.scss";
/* Editor layout */
.editor {
height: 100%;
width: 100%;
}
.cm-scroller {
overflow-y: auto !important;
background: var(--bg-0);
}
.cm-editor {
height: 100%;
background: var(--bg-1);
color: var(--fg-0);
}
.cm-scroller {
overflow-y: auto !important;
.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;
}
.diag {
margin: 0;
padding-left: 18px;
.cm-lineNumbers .cm-gutterElement {
padding: 0 10px 0 6px;
font-family: var(--font-mono);
font-size: 12px;
}
.diag li {
margin: 6px 0;
.cm-activeLine {
background: color-mix(in srgb, var(--accent) 6%, transparent)!important;
}
/* --- Syntax colors via CSS classes applied by decorations --- */
.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;
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;
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;
color: color-mix(in srgb, var(--error) 85%, transparent);
font-weight: 700;
}
.rb-1 {
color: #ed0;
color: color-mix(in srgb, var(--warning) 85%, transparent);
font-weight: 700;
}
.rb-2 {
color: #9d5;
color: color-mix(in srgb, var(--success) 85%, transparent);
font-weight: 700;
}
.rb-3 {
color: #2cb;
color: color-mix(in srgb, var(--accent) 85%, transparent);
font-weight: 700;
}
.rb-4 {
color: #36b;
color: color-mix(in srgb, var(--focus) 85%, transparent);
font-weight: 700;
}
.rb-5 {
color: #639;
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-decoration: underline wavy var(--error);
text-underline-offset: 2px;
}
.cm-diag-warning {
text-decoration: underline wavy #ffd33d;
/* yellow */
text-decoration: underline wavy var(--warning);
text-underline-offset: 2px;
}
.cm-diag-info {
text-decoration: underline wavy #79c0ff;
/* cyan-ish */
text-decoration: underline wavy var(--accent);
text-underline-offset: 2px;
}

View file

@ -1,13 +1,16 @@
@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;
color: var(--fg-0);
font-family: var(--font-ui);
background: #909090;
}
@ -15,12 +18,13 @@ body {
height: 100vh;
width: 100vw;
overflow: hidden;
background-color: var(--bg-0);
}
.graph {
width: 100%;
height: 100%;
background: #111;
background: var(--graph-bg);
}
.vscroll {
@ -29,18 +33,28 @@ body {
}
.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 {
: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);
.hSplit:hover:not(.styleOnly),
.vSplit:hover:not(.styleOnly) {
background: var(--separator-hover);
}

View file

@ -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); }
/* 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); }

159
web/root/style/themes.scss Normal file
View file

@ -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);
}

View file

@ -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;
border: 1px solid color-mix(in srgb, var(--fg-muted) 35%, transparent);
background: var(--bg-1);
color: var(--fg-0);
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;
}
.tipTitle {
font-weight: 700;
margin-bottom: 4px;
}
.tipBody {
white-space: pre-wrap;
color: var(--fg-1);
}