mirror of
https://github.com/ParkerTenBroeck/automata.git
synced 2026-06-06 21:24:06 -04:00
moved to deno to bundle website
This commit is contained in:
parent
c35d7a9192
commit
7629bdab6d
28 changed files with 1534 additions and 41961 deletions
|
|
@ -1,371 +0,0 @@
|
|||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
font-family: system-ui, sans-serif;
|
||||
background: #909090;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: grid;
|
||||
grid-template-rows: var(--topH, 50vh) 8px 1fr;
|
||||
/* top pane, splitter, editor */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Top pane: terminal area */
|
||||
.topPane {
|
||||
display: grid;
|
||||
grid-template-columns: var(--termW, 50vw) 8px 1fr;
|
||||
/* terminal, splitter, filler */
|
||||
min-height: 80px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Bottom pane: editor */
|
||||
.bottomPane {
|
||||
min-height: 120px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Make editor fill its pane */
|
||||
#editor {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.terminal {
|
||||
background: #0b0f14;
|
||||
color: #c9d1d9;
|
||||
padding: 1em;
|
||||
margin: 0px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.35;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* ANSI text styles */
|
||||
.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 */
|
||||
|
||||
.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; }
|
||||
|
||||
/* 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); }
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* App layout */
|
||||
.app {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: grid;
|
||||
grid-template-rows: var(--canvasH, 35vh) 8px 1fr;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ---------- Canvas ---------- */
|
||||
.canvasPane {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #111;
|
||||
}
|
||||
|
||||
.graph {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* ---------- Bottom area (terminal + editor) ---------- */
|
||||
/* Bottom area (terminal + editor) */
|
||||
.bottomPane {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 8px var(--termW, 40vw);
|
||||
overflow: hidden;
|
||||
/* IMPORTANT */
|
||||
}
|
||||
|
||||
/* Terminal side */
|
||||
.terminalPane {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.terminal {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
/* terminal scrolls */
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* Editor side */
|
||||
.editorPane {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
/* let CodeMirror scroll */
|
||||
}
|
||||
|
||||
/* CodeMirror mount point */
|
||||
#editor {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* VERY IMPORTANT: force CodeMirror to respect container height */
|
||||
.cm-editor {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* CodeMirror’s internal scroller (this is where the scrollbar lives) */
|
||||
.cm-scroller {
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
/* ---------- Splitters ---------- */
|
||||
.hSplit {
|
||||
cursor: row-resize;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.vSplit {
|
||||
cursor: col-resize;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.hSplit:hover,
|
||||
.vSplit:hover {
|
||||
background: rgba(121, 192, 255, 0.25);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.diag {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.diag li {
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
/* --- Syntax colors via CSS classes applied by decorations --- */
|
||||
.tok-comment {
|
||||
color: #1a7b24;
|
||||
}
|
||||
|
||||
.tok-keyword {
|
||||
color: #b99400;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tok-error {
|
||||
color: #ff0505;
|
||||
font-weight: 1000;
|
||||
}
|
||||
|
||||
.tok-ident {
|
||||
color: #90d4e0;
|
||||
}
|
||||
|
||||
.tok-brace {
|
||||
color: #d73a49;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tok-punc {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.tok-string {
|
||||
color: #03621e;
|
||||
}
|
||||
|
||||
/* Rainbow bracket depth classes */
|
||||
.rb-0 {
|
||||
color: #a35;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.rb-1 {
|
||||
color: #ed0;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.rb-2 {
|
||||
color: #9d5;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.rb-3 {
|
||||
color: #2cb;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.rb-4 {
|
||||
color: #36b;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.rb-5 {
|
||||
color: #639;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Severity underline styles */
|
||||
.cm-diag-error {
|
||||
text-decoration: underline wavy #d73a49;
|
||||
/* red */
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.cm-diag-warning {
|
||||
text-decoration: underline wavy #ffd33d;
|
||||
/* yellow */
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.cm-diag-info {
|
||||
text-decoration: underline wavy #79c0ff;
|
||||
/* cyan-ish */
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Tooltip title coloring by severity */
|
||||
.tipTitle.error {
|
||||
color: #d73a49;
|
||||
}
|
||||
|
||||
.tipTitle.warning {
|
||||
color: #ffd33d;
|
||||
}
|
||||
|
||||
.tipTitle.info {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
/* Optional: diagnostics panel coloring */
|
||||
.diag li.error {
|
||||
color: #d73a49;
|
||||
}
|
||||
|
||||
.diag li.warning {
|
||||
color: #b08800;
|
||||
}
|
||||
|
||||
.diag li.info {
|
||||
color: #0366d6;
|
||||
}
|
||||
|
||||
/* Tooltip styling */
|
||||
.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;
|
||||
}
|
||||
|
||||
.tipTitle {
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.tipBody {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Loading screen */
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.lds-dual-ring {
|
||||
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;
|
||||
}
|
||||
|
||||
@keyframes lds-dual-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,11 +5,8 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Automata</title>
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/vis-network/styles/vis-network.min.css">
|
||||
|
||||
|
||||
<link href="editor.css" rel="stylesheet">
|
||||
<link href="style.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
@ -46,7 +43,7 @@
|
|||
</div>
|
||||
|
||||
|
||||
<script type="module" src="index.js"></script>
|
||||
<script type="module" src="src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
|
||||
import wasm from "./wasm.js"
|
||||
import "./editor.js"
|
||||
|
||||
File diff suppressed because one or more lines are too long
27
web/root/js/vis-network.min.js
vendored
27
web/root/js/vis-network.min.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"name": "automata-web",
|
||||
"type": "module",
|
||||
"version": "0.1.0",
|
||||
"files": [
|
||||
"automata_bg.wasm",
|
||||
"automata.js",
|
||||
"automata_bg.js"
|
||||
],
|
||||
"main": "automata.js",
|
||||
"sideEffects": [
|
||||
"./automata.js",
|
||||
"./snippets/*"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,17 +1,26 @@
|
|||
import { EditorView, keymap, hoverTooltip, Decoration, ViewPlugin } from "https://esm.sh/@codemirror/view";
|
||||
import { EditorState, StateField } from "https://esm.sh/@codemirror/state";
|
||||
import { defaultKeymap, history, historyKeymap } from "https://esm.sh/@codemirror/commands";
|
||||
import { lineNumbers, highlightActiveLineGutter } from "https://esm.sh/@codemirror/view";
|
||||
import { bracketMatching, indentOnInput } from "https://esm.sh/@codemirror/language";
|
||||
import { closeBrackets } from "https://esm.sh/@codemirror/autocomplete";
|
||||
import { oneDark } from "https://esm.sh/@codemirror/theme-one-dark";
|
||||
// deno-lint-ignore-file
|
||||
|
||||
import wasm from "./wasm.js"
|
||||
import {
|
||||
EditorView,
|
||||
keymap,
|
||||
hoverTooltip,
|
||||
Decoration,
|
||||
ViewPlugin,
|
||||
lineNumbers,
|
||||
highlightActiveLineGutter,
|
||||
} from "npm:@codemirror/view";
|
||||
|
||||
import * as vis from "./js/vis-network.js"
|
||||
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";
|
||||
|
||||
|
||||
function tokenize(text) {
|
||||
import wasm from "./wasm.ts"
|
||||
|
||||
|
||||
function tokenize(text: string) {
|
||||
try {
|
||||
return wasm.lex(text);
|
||||
} catch (e) {
|
||||
|
|
@ -20,16 +29,17 @@ function tokenize(text) {
|
|||
}
|
||||
}
|
||||
|
||||
function compile(text) {
|
||||
function compile(text: string): wasm.CompileResult {
|
||||
try {
|
||||
return wasm.compile(text);
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return []
|
||||
console.log(e);
|
||||
// @ts-expect-error wasm defines extra cleanup
|
||||
return {log: [], log_formatted: ""};
|
||||
}
|
||||
}
|
||||
|
||||
const tokenClass = (t) =>
|
||||
const tokenClass = (t: string) =>
|
||||
({
|
||||
comment: "tok-comment",
|
||||
keyword: "tok-keyword",
|
||||
|
|
@ -47,20 +57,20 @@ const tokenClass = (t) =>
|
|||
}[t] || "tok-ident");
|
||||
|
||||
|
||||
function severityClass(sev) {
|
||||
function severityClass(sev: string) {
|
||||
const s = (sev || "error").toLowerCase();
|
||||
if (s === "warning") return "cm-diag-warning";
|
||||
if (s === "info") return "cm-diag-info";
|
||||
return "cm-diag-error";
|
||||
}
|
||||
function sevRank(sev) {
|
||||
function sevRank(sev: string) {
|
||||
if (sev === "error") return 3;
|
||||
if (sev === "warning") return 2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
function buildAnalysis(text, doc) {
|
||||
function buildAnalysis(text: string, doc: Text) {
|
||||
const tokens = tokenize(text);
|
||||
const { log, log_formatted } = compile(text);
|
||||
|
||||
|
|
@ -71,7 +81,7 @@ function buildAnalysis(text, doc) {
|
|||
for (const tok of tokens) {
|
||||
const start = Math.max(0, Math.min(docLen, tok.start));
|
||||
const end = Math.max(start, Math.min(docLen, tok.end));
|
||||
var tc = tokenClass(tok.kind);
|
||||
let tc = tokenClass(tok.kind);
|
||||
if (tc === "rb-") {
|
||||
tc += tok.scope_level.toString();
|
||||
}
|
||||
|
|
@ -114,7 +124,7 @@ const analysisField = StateField.define({
|
|||
// ===================== Hover tooltip (uses cached diags) =====================
|
||||
const diagHover = hoverTooltip((view, pos) => {
|
||||
const { log } = view.state.field(analysisField);
|
||||
const hits = log.filter((d) => pos >= d.start && pos <= d.end);
|
||||
const hits = log.filter((d) => d.start !== undefined && d.end !== undefined && pos >= d.start && pos <= d.end);
|
||||
if (hits.length === 0) return null;
|
||||
|
||||
const top = hits.reduce((a, b) => (sevRank(b.level) > sevRank(a.level) ? b : a), hits[0]);
|
||||
|
|
@ -148,27 +158,28 @@ const diagHover = hoverTooltip((view, pos) => {
|
|||
});
|
||||
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
function escapeHtml(s: string) {
|
||||
return s
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
|
||||
|
||||
function ansiToHtml(input) {
|
||||
function ansiToHtml(input: string) {
|
||||
// deno-lint-ignore no-control-regex
|
||||
const ESC_RE = /\x1b\[([0-9;]*)m/g;
|
||||
|
||||
let out = "";
|
||||
let lastIndex = 0;
|
||||
|
||||
// current style state
|
||||
let fg = null; // e.g. 31, 92
|
||||
let bg = null; // e.g. 41
|
||||
let fg: number|null = null; // e.g. 31, 92
|
||||
let bg: number|null = null; // e.g. 41
|
||||
let bold = false;
|
||||
let dim = false;
|
||||
|
||||
function openSpanIfNeeded(text) {
|
||||
function openSpanIfNeeded(text: string) {
|
||||
if (text.length === 0) return "";
|
||||
const classes = [];
|
||||
if (bold) classes.push("ansi-bold");
|
||||
|
|
@ -179,8 +190,8 @@ function ansiToHtml(input) {
|
|||
return `<span class="${classes.join(" ")}">${escapeHtml(text)}</span>`;
|
||||
}
|
||||
|
||||
function applyCodes(codes) {
|
||||
if (codes.length === 0) codes = [0];
|
||||
function applyCodes(codes: string[]) {
|
||||
if (codes.length === 0) codes = ["0"];
|
||||
for (const c of codes) {
|
||||
const code = Number(c);
|
||||
if (Number.isNaN(code)) continue;
|
||||
|
|
@ -220,6 +231,7 @@ function ansiToHtml(input) {
|
|||
return out;
|
||||
}
|
||||
|
||||
// @ts-expect-error bad library
|
||||
function formatTerminal(view) {
|
||||
const term = document.getElementById("terminal");
|
||||
if (!term) return;
|
||||
|
|
@ -234,10 +246,14 @@ function formatTerminal(view) {
|
|||
|
||||
const terminalPlugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
|
||||
// @ts-expect-error bad library
|
||||
constructor(view) {
|
||||
// @ts-expect-error bad library
|
||||
this.view = view;
|
||||
formatTerminal(view);
|
||||
}
|
||||
// @ts-expect-error bad library
|
||||
update(update) {
|
||||
if (update.docChanged) formatTerminal(update.view);
|
||||
}
|
||||
|
|
@ -288,15 +304,15 @@ const state = EditorState.create({
|
|||
],
|
||||
});
|
||||
|
||||
window.editor = new EditorView({
|
||||
const editor = new EditorView({
|
||||
state,
|
||||
parent: document.getElementById("editor"),
|
||||
parent: document.getElementById("editor")!,
|
||||
});
|
||||
|
||||
|
||||
function setDefaultLayoutWeights() {
|
||||
const vh = window.innerHeight;
|
||||
const vw = window.innerWidth;
|
||||
const vh = globalThis.window.innerHeight;
|
||||
const vw = globalThis.window.innerWidth;
|
||||
|
||||
// Canvas: 30% of screen height
|
||||
const canvasH = Math.round(vh * 0.60);
|
||||
|
|
@ -304,7 +320,7 @@ function setDefaultLayoutWeights() {
|
|||
// Terminal: 35% of width
|
||||
const termW = Math.round(vw * 0.30);
|
||||
|
||||
const app = document.getElementById("app");
|
||||
const app = document.getElementById("app")!;
|
||||
app.style.setProperty("--canvasH", `${canvasH}px`);
|
||||
app.style.setProperty("--termW", `${termW}px`);
|
||||
}
|
||||
|
|
@ -313,9 +329,9 @@ setDefaultLayoutWeights();
|
|||
|
||||
|
||||
(function enableLayoutSplitters() {
|
||||
const app = document.getElementById("app");
|
||||
const hSplit = document.getElementById("hSplit");
|
||||
const vSplit = document.getElementById("vSplit");
|
||||
const app = document.getElementById("app")!;
|
||||
const hSplit = document.getElementById("hSplit")!;
|
||||
const vSplit = document.getElementById("vSplit")!;
|
||||
// const canvas = document.getElementById("canvas");
|
||||
const canvasPane = document.getElementById("canvasPane");
|
||||
|
||||
|
|
@ -334,7 +350,7 @@ setDefaultLayoutWeights();
|
|||
e.preventDefault();
|
||||
});
|
||||
|
||||
window.addEventListener("mousemove", (e) => {
|
||||
globalThis.window.addEventListener("mousemove", (e) => {
|
||||
const rect = app.getBoundingClientRect();
|
||||
|
||||
if (draggingH) {
|
||||
|
|
@ -347,7 +363,7 @@ setDefaultLayoutWeights();
|
|||
}
|
||||
|
||||
if (draggingV) {
|
||||
const bottomPane = document.getElementById("bottomPane");
|
||||
const bottomPane = document.getElementById("bottomPane")!;
|
||||
const r = bottomPane.getBoundingClientRect();
|
||||
const x = e.clientX - r.left;
|
||||
const minTerm = 220;
|
||||
|
|
@ -357,256 +373,9 @@ setDefaultLayoutWeights();
|
|||
}
|
||||
});
|
||||
|
||||
window.addEventListener("mouseup", () => {
|
||||
globalThis.window.addEventListener("mouseup", () => {
|
||||
draggingH = false;
|
||||
draggingV = false;
|
||||
document.body.style.cursor = "";
|
||||
});
|
||||
})();
|
||||
|
||||
let network = null;
|
||||
const nodes = new vis.DataSet();
|
||||
const edges = new vis.DataSet();
|
||||
|
||||
|
||||
const automaton = {
|
||||
states: ["q0", "q1"],
|
||||
initialState: "q0",
|
||||
acceptStates: ["q1"],
|
||||
|
||||
transitions: [
|
||||
{
|
||||
from: "q0",
|
||||
to: "q0",
|
||||
label: "ε, z0 → A z0\n"
|
||||
},
|
||||
{
|
||||
from: "q0",
|
||||
to: "q0",
|
||||
label: "ε, z0 → B z0"
|
||||
},
|
||||
{
|
||||
from: "q0",
|
||||
to: "q1",
|
||||
label: "ε, z0 → z0"
|
||||
},
|
||||
{
|
||||
from: "q1",
|
||||
to: "q1",
|
||||
label: "a, A → ε"
|
||||
},
|
||||
{
|
||||
from: "q1",
|
||||
to: "q1",
|
||||
label: "b, B → ε"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**@param {{ctx: CanvasRenderingContext2D}} */
|
||||
function renderNode({
|
||||
ctx,
|
||||
id,
|
||||
x,
|
||||
y,
|
||||
state: { selected, hover },
|
||||
style,
|
||||
label,
|
||||
}) {
|
||||
return {
|
||||
drawNode() {
|
||||
ctx.save();
|
||||
var r = style.size;
|
||||
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, r, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = "red";
|
||||
ctx.fill();
|
||||
ctx.lineWidth = 4;
|
||||
ctx.strokeStyle = "blue";
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = "black";
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(label, x, y, r);
|
||||
|
||||
|
||||
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;
|
||||
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({
|
||||
id: state,
|
||||
label: state,
|
||||
});
|
||||
}
|
||||
|
||||
// Populate edges
|
||||
automaton.transitions.forEach((t, i) => {
|
||||
edges.add({
|
||||
id: `e${i}`,
|
||||
from: t.from,
|
||||
to: t.to,
|
||||
label: t.label
|
||||
});
|
||||
});
|
||||
|
||||
// updateGraphFromText();
|
||||
ensureGraph();
|
||||
function updateGraphFromText() {
|
||||
ensureGraph();
|
||||
|
||||
const trans = []
|
||||
|
||||
// Collect state ids
|
||||
const stateSet = new Set();
|
||||
for (const tr of trans) {
|
||||
stateSet.add(tr.from);
|
||||
stateSet.add(tr.to);
|
||||
}
|
||||
|
||||
// Update nodes (add missing, remove stale)
|
||||
const existingNodeIds = new Set(nodes.getIds());
|
||||
const desiredNodeIds = new Set([...stateSet]);
|
||||
|
||||
// remove stale
|
||||
for (const id of existingNodeIds) {
|
||||
if (!desiredNodeIds.has(id)) nodes.remove(id);
|
||||
}
|
||||
// add/update desired
|
||||
for (const id of desiredNodeIds) {
|
||||
const pos = pinnedPositions.get(id);
|
||||
if (!existingNodeIds.has(id)) {
|
||||
nodes.add({
|
||||
id,
|
||||
label: id,
|
||||
...(pos ? { x: pos.x, y: pos.y, fixed: true } : {})
|
||||
});
|
||||
} else if (pos) {
|
||||
nodes.update({ id, x: pos.x, y: pos.y, fixed: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Update edges (stable IDs so edits don't flicker)
|
||||
const desiredEdgeIds = new Set();
|
||||
const nextEdges = [];
|
||||
|
||||
for (let i = 0; i < trans.length; i++) {
|
||||
const tr = trans[i];
|
||||
const id = `${tr.from}::${tr.to}::${tr.label}::${i}`;
|
||||
desiredEdgeIds.add(id);
|
||||
nextEdges.push({ id, from: tr.from, to: tr.to, label: tr.label });
|
||||
}
|
||||
|
||||
const existingEdgeIds = new Set(edges.getIds());
|
||||
for (const id of existingEdgeIds) {
|
||||
if (!desiredEdgeIds.has(id)) edges.remove(id);
|
||||
}
|
||||
// add/update in batch
|
||||
for (const e of nextEdges) {
|
||||
if (!existingEdgeIds.has(e.id)) edges.add(e);
|
||||
else edges.update(e);
|
||||
}
|
||||
|
||||
// If positions exist for all nodes, we can disable physics to “respect” manual layout
|
||||
// Otherwise leave physics on to auto-layout new nodes.
|
||||
const allPinned = [...desiredNodeIds].every((id) => pinnedPositions.has(id));
|
||||
network.setOptions({ physics: { enabled: !allPinned } });
|
||||
|
||||
// Redraw nicely after updates
|
||||
network.fit({ animation: { duration: 200, easingFunction: "easeInOutQuad" } });
|
||||
}
|
||||
|
||||
// ---------- 4) Hook graph updates into your existing single-pass analysis ----------
|
||||
const graphPlugin = ViewPlugin.fromClass(class {
|
||||
constructor(view) {
|
||||
updateGraphFromText(view.state.doc.toString());
|
||||
}
|
||||
update(update) {
|
||||
if (update.docChanged) {
|
||||
updateGraphFromText(update.state.doc.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function chosen_node(values, id, selected, hovering) {
|
||||
|
||||
console.log(values, id, selected, hovering)
|
||||
}
|
||||
|
||||
function ensureGraph() {
|
||||
if (network) return;
|
||||
|
||||
const container = document.getElementById("graph");
|
||||
network = new vis.Network(
|
||||
container,
|
||||
{ nodes, edges },
|
||||
{
|
||||
layout: { improvedLayout: true },
|
||||
physics: {
|
||||
enabled: true,
|
||||
solver: "barnesHut",
|
||||
barnesHut: { gravitationalConstant: -8000, springLength: 120, springConstant: 0.04 },
|
||||
stabilization: { iterations: 200 }
|
||||
},
|
||||
interaction: {
|
||||
dragNodes: true,
|
||||
hover: true,
|
||||
multiselect: true
|
||||
},
|
||||
nodes: {
|
||||
shape: 'dot',
|
||||
size: 14,
|
||||
font: { color: "#c9d1d9" },
|
||||
color: {
|
||||
background: "#1f6feb",
|
||||
border: "#79c0ff",
|
||||
highlight: { background: "#388bfd", border: "#a5d6ff" }
|
||||
},
|
||||
chosen: {
|
||||
node: chosen_node
|
||||
},
|
||||
shape: "custom",
|
||||
ctxRenderer: renderNode,
|
||||
size: 18,
|
||||
},
|
||||
edges: {
|
||||
arrows: { to: { enabled: true, scaleFactor: 0.8 } },
|
||||
arrowStrikethrough: false,
|
||||
font: { align: "middle", color: "#000000ff" },
|
||||
color: { color: "rgba(201,209,217,0.35)", highlight: "#c9d1d9" },
|
||||
smooth: { type: "dynamic" },
|
||||
arrows: "to",
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Save positions when user drags nodes
|
||||
|
||||
network.on("dragEnd", (params) => {
|
||||
const pos = network.getPositions(params.nodes);
|
||||
for (const id of params.nodes) {
|
||||
pinnedPositions.set(id, pos[id]);
|
||||
}
|
||||
});
|
||||
|
||||
window.network = network;
|
||||
}
|
||||
4
web/root/src/main.ts
Normal file
4
web/root/src/main.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
import "./editor.ts"
|
||||
import "./visualizer.ts"
|
||||
|
||||
187
web/root/src/visualizer.ts
Normal file
187
web/root/src/visualizer.ts
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
// deno-lint-ignore-file no-unversioned-import
|
||||
|
||||
// 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>();
|
||||
|
||||
|
||||
const automaton = {
|
||||
states: ["q0", "q1"],
|
||||
initialState: "q0",
|
||||
acceptStates: ["q1"],
|
||||
|
||||
transitions: [
|
||||
{
|
||||
from: "q0",
|
||||
to: "q0",
|
||||
label: "ε, z0 → A z0\n"
|
||||
},
|
||||
{
|
||||
from: "q0",
|
||||
to: "q0",
|
||||
label: "ε, z0 → B z0"
|
||||
},
|
||||
{
|
||||
from: "q0",
|
||||
to: "q1",
|
||||
label: "ε, z0 → z0"
|
||||
},
|
||||
{
|
||||
from: "q1",
|
||||
to: "q1",
|
||||
label: "a, A → ε"
|
||||
},
|
||||
{
|
||||
from: "q1",
|
||||
to: "q1",
|
||||
label: "b, B → ε"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
function renderNode({
|
||||
ctx,
|
||||
id,
|
||||
x,
|
||||
y,
|
||||
state: { selected, hover },
|
||||
style,
|
||||
label,
|
||||
}: any) {
|
||||
return {
|
||||
drawNode() {
|
||||
ctx.save();
|
||||
const r = style.size;
|
||||
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, r, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = "red";
|
||||
ctx.fill();
|
||||
ctx.lineWidth = 4;
|
||||
ctx.strokeStyle = "blue";
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = "black";
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(label, x, y, r);
|
||||
|
||||
|
||||
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;
|
||||
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({
|
||||
id: state,
|
||||
label: state,
|
||||
});
|
||||
}
|
||||
|
||||
// Populate edges
|
||||
automaton.transitions.forEach((t, i) => {
|
||||
edges.add({
|
||||
id: `e${i}`,
|
||||
from: t.from,
|
||||
to: t.to,
|
||||
label: t.label
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
const network: vis.Network = createGraph();
|
||||
|
||||
function createGraph(): vis.Network {
|
||||
|
||||
const container = document.getElementById("graph")!;
|
||||
|
||||
const network = new vis.Network(
|
||||
container,
|
||||
{ nodes, edges },
|
||||
{
|
||||
layout: { improvedLayout: true },
|
||||
physics: {
|
||||
enabled: true,
|
||||
solver: "barnesHut",
|
||||
barnesHut: { gravitationalConstant: -8000, springLength: 120, springConstant: 0.04 },
|
||||
stabilization: { iterations: 200 }
|
||||
},
|
||||
interaction: {
|
||||
dragNodes: true,
|
||||
hover: true,
|
||||
multiselect: true,
|
||||
hoverConnectedEdges: false,
|
||||
selectConnectedEdges: false,
|
||||
},
|
||||
nodes: {
|
||||
font: { color: "#c9d1d9" },
|
||||
color: {
|
||||
background: "#1f6feb",
|
||||
border: "#79c0ff",
|
||||
highlight: { background: "#388bfd", border: "#a5d6ff" }
|
||||
},
|
||||
// @ts-expect-error bad library
|
||||
chosen: {
|
||||
node: chosen_node,
|
||||
},
|
||||
shape: "custom",
|
||||
ctxRenderer: renderNode,
|
||||
size: 18,
|
||||
},
|
||||
edges: {
|
||||
chosen: {
|
||||
// @ts-expect-error bad library
|
||||
edge: chosen_edge
|
||||
},
|
||||
arrowStrikethrough: false,
|
||||
font: { align: "middle", color: "#000000ff" },
|
||||
color: { color: "rgba(201,209,217,0.35)", highlight: "#c9d1d9" },
|
||||
// @ts-expect-error bad library
|
||||
smooth: { type: "dynamic" },
|
||||
arrows: "to",
|
||||
}
|
||||
}
|
||||
);
|
||||
vis.DataSet
|
||||
|
||||
network.on("doubleClick", (params: any) => {
|
||||
|
||||
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)
|
||||
}
|
||||
});
|
||||
|
||||
return network;
|
||||
}
|
||||
|
|
@ -1,22 +1,21 @@
|
|||
console.debug("Loading wasm…");
|
||||
import init, * as wasm from "./automata/automata_web.js";
|
||||
import init, * as wasm from "../../wasm/automata_web.js";
|
||||
try{
|
||||
console.debug("Wasm loaded. Starting app…");
|
||||
window.wasm = wasm;
|
||||
await init();
|
||||
console.debug("App started.");
|
||||
document.getElementById("center_text").innerHTML = '';
|
||||
document.getElementById("app").style.display = '';
|
||||
document.getElementById("center_text")!.innerHTML = '';
|
||||
document.getElementById("app")!.style.display = '';
|
||||
wasm.init();
|
||||
}catch(e){
|
||||
console.error("Failed to start: " + error);
|
||||
document.getElementById("the_canvas_id").remove();
|
||||
document.getElementById("center_text").innerHTML = `
|
||||
console.error("Failed to start: " + e);
|
||||
document.getElementById("the_canvas_id")!.remove();
|
||||
document.getElementById("center_text")!.innerHTML = `
|
||||
<p>
|
||||
An error occurred during loading:
|
||||
</p>
|
||||
<p style="font-family:Courier New">
|
||||
${error}
|
||||
${e}
|
||||
</p>
|
||||
<p style="font-size:14px">
|
||||
Make sure you use a modern browser with WebGL and WASM enabled.
|
||||
127
web/root/style/editor.scss
Normal file
127
web/root/style/editor.scss
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
@import url("tooltip.scss");
|
||||
|
||||
/* CodeMirror mount point */
|
||||
#editor {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* VERY IMPORTANT: force CodeMirror to respect container height */
|
||||
.cm-editor {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* CodeMirror’s internal scroller (this is where the scrollbar lives) */
|
||||
.cm-scroller {
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.diag {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.diag li {
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
/* --- Syntax colors via CSS classes applied by decorations --- */
|
||||
.tok-comment {
|
||||
color: #1a7b24;
|
||||
}
|
||||
|
||||
.tok-keyword {
|
||||
color: #b99400;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tok-error {
|
||||
color: #ff0505;
|
||||
font-weight: 1000;
|
||||
}
|
||||
|
||||
.tok-ident {
|
||||
color: #90d4e0;
|
||||
}
|
||||
|
||||
.tok-brace {
|
||||
color: #d73a49;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tok-punc {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.tok-string {
|
||||
color: #03621e;
|
||||
}
|
||||
|
||||
/* Rainbow bracket depth classes */
|
||||
.rb-0 {
|
||||
color: #a35;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.rb-1 {
|
||||
color: #ed0;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.rb-2 {
|
||||
color: #9d5;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.rb-3 {
|
||||
color: #2cb;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.rb-4 {
|
||||
color: #36b;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.rb-5 {
|
||||
color: #639;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.cm-diag-warning {
|
||||
text-decoration: underline wavy #ffd33d;
|
||||
/* yellow */
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.cm-diag-info {
|
||||
text-decoration: underline wavy #79c0ff;
|
||||
/* cyan-ish */
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
140
web/root/style/style.scss
Normal file
140
web/root/style/style.scss
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
@import url("./editor.scss");;
|
||||
@import url("./terminal.scss");
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
font-family: system-ui, sans-serif;
|
||||
background: #909090;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: grid;
|
||||
grid-template-rows: var(--topH, 50vh) 8px 1fr;
|
||||
/* top pane, splitter, editor */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Top pane: terminal area */
|
||||
.topPane {
|
||||
display: grid;
|
||||
grid-template-columns: var(--termW, 50vw) 8px 1fr;
|
||||
/* terminal, splitter, filler */
|
||||
min-height: 80px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Bottom pane: editor */
|
||||
.bottomPane {
|
||||
min-height: 120px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* App layout */
|
||||
.app {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: grid;
|
||||
grid-template-rows: var(--canvasH, 35vh) 8px 1fr;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ---------- Canvas ---------- */
|
||||
.canvasPane {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #111;
|
||||
}
|
||||
|
||||
.graph {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* ---------- Bottom area (terminal + editor) ---------- */
|
||||
/* Bottom area (terminal + editor) */
|
||||
.bottomPane {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 8px var(--termW, 40vw);
|
||||
overflow: hidden;
|
||||
/* IMPORTANT */
|
||||
}
|
||||
|
||||
/* Terminal side */
|
||||
.terminalPane {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Editor side */
|
||||
.editorPane {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
/* let CodeMirror scroll */
|
||||
}
|
||||
|
||||
/* ---------- Splitters ---------- */
|
||||
.hSplit {
|
||||
cursor: row-resize;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.vSplit {
|
||||
cursor: col-resize;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.hSplit:hover,
|
||||
.vSplit:hover {
|
||||
background: rgba(121, 192, 255, 0.25);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Loading screen */
|
||||
.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;
|
||||
}
|
||||
|
||||
.lds-dual-ring {
|
||||
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;
|
||||
}
|
||||
|
||||
@keyframes lds-dual-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
48
web/root/style/terminal.scss
Normal file
48
web/root/style/terminal.scss
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
.terminal {
|
||||
background: #0b0f14;
|
||||
color: #c9d1d9;
|
||||
padding: 1em;
|
||||
margin: 0px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.35;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* ANSI text styles */
|
||||
.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 */
|
||||
|
||||
.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; }
|
||||
|
||||
/* 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); }
|
||||
32
web/root/style/tooltip.scss
Normal file
32
web/root/style/tooltip.scss
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
.tipTitle.error {
|
||||
color: #d73a49;
|
||||
}
|
||||
|
||||
.tipTitle.warning {
|
||||
color: #ffd33d;
|
||||
}
|
||||
|
||||
.tipTitle.info {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.tipTitle {
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.tipBody {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue