fixed a few issues with auto resizing

This commit is contained in:
Parker TenBroeck 2026-01-07 14:12:42 -05:00
parent 806545aba6
commit 3d656d45de
6 changed files with 263 additions and 121 deletions

View file

@ -101,6 +101,7 @@ pub fn parse_universal(ctx: &mut Context<'_>) -> Option<Machine> {
use Spanned as S; use Spanned as S;
#[derive(Debug)]
enum Type{ enum Type{
Dfa, Dfa,
Nfa, Nfa,
@ -138,11 +139,10 @@ pub fn parse_universal(ctx: &mut Context<'_>) -> Option<Machine> {
} }
Some(match parse_type(items.next(), ctx)?{ Some(match parse_type(items.next(), ctx)?{
Type::Dfa => todo!(),
Type::Nfa => todo!(),
Type::Dpda => todo!(),
Type::Npda => Machine::Npda(npda::Npda::load_from_ast(items, ctx)?), Type::Npda => Machine::Npda(npda::Npda::load_from_ast(items, ctx)?),
Type::Tm => todo!(), ty => {
Type::Ntm => todo!(), ctx.emit_error_locless(format!("currently unsupported type {ty:?}"));
return None;
}
}) })
} }

1
web/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/dist

93
web/deploy.sh Executable file
View file

@ -0,0 +1,93 @@
#!/usr/bin/env bash
set -euo pipefail
MAIN_BRANCH="main"
PAGES_BRANCH="gh-pages"
REMOTE="origin"
ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null)" || {
echo "❌ Not inside a git repository."
exit 1
}
cd "$ROOT_DIR"
ORIG_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
cleanup() {
# Always try to return to the original branch
git switch -q "$ORIG_BRANCH" 2>/dev/null || true
}
trap cleanup EXIT
echo "📍 Repo: $ROOT_DIR"
echo "🔎 Current branch: $ORIG_BRANCH"
# Ensure clean working tree
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "❌ Working tree has uncommitted changes. Commit or stash before deploying."
exit 1
fi
echo "🔄 Fetching from $REMOTE..."
git fetch "$REMOTE" --prune
# Ensure main branch exists locally and remotely
git show-ref --verify --quiet "refs/heads/$MAIN_BRANCH" || {
echo "❌ Local branch '$MAIN_BRANCH' not found."
exit 1
}
git show-ref --verify --quiet "refs/remotes/$REMOTE/$MAIN_BRANCH" || {
echo "❌ Remote branch '$REMOTE/$MAIN_BRANCH' not found."
exit 1
}
# Switch to main first (so checks are consistent)
git switch -q "$MAIN_BRANCH"
# Check if local main is behind origin/main
LOCAL_MAIN="$(git rev-parse "$MAIN_BRANCH")"
REMOTE_MAIN="$(git rev-parse "$REMOTE/$MAIN_BRANCH")"
BASE_MAIN="$(git merge-base "$MAIN_BRANCH" "$REMOTE/$MAIN_BRANCH")"
if [[ "$LOCAL_MAIN" != "$REMOTE_MAIN" && "$LOCAL_MAIN" == "$BASE_MAIN" ]]; then
echo "❌ '$MAIN_BRANCH' is behind '$REMOTE/$MAIN_BRANCH'."
echo " Please pull/rebase first, then rerun."
exit 1
fi
echo "✅ '$MAIN_BRANCH' is up-to-date (or ahead/diverged). Pulling latest..."
git pull --ff-only "$REMOTE" "$MAIN_BRANCH"
echo "🏗️ Running build: deno task build"
deno task build
# Ensure pages branch exists
git show-ref --verify --quiet "refs/heads/$PAGES_BRANCH" || {
echo "❌ Local branch '$PAGES_BRANCH' not found."
echo " Create it first: git switch -c $PAGES_BRANCH"
exit 1
}
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"
# Copy dist -> docs (replace docs contents)
if [[ ! -d "dist" ]]; then
echo "❌ dist/ not found. Did 'deno task build' output to dist/?"
exit 1
fi
echo "📦 Copying dist/ -> docs/ (sync)..."
rm -rf docs
mkdir -p docs
# Copy contents of dist into docs
cp -R dist/. ../docs/
# Commit if changed
if git diff --quiet; then
echo "✅ No changes to commit
::contentReference[oaicite:0]{index=0}

View file

@ -21,25 +21,34 @@
<div class="app" style="display:none" id="app"> <div class="app" style="display:none" id="app">
<section> <section>
<div id="graph" class="graph"></div> <section>
<div id="graph" class="graph"></div>
</section>
<button id="togglePhysics">Toggle Physics</button> <div class="vSplit" style="--split-default: 20%" title="Drag to resize canvas width"></div>
<button id="resetLayout">Reset Layout</button>
<section>
meow
</section>
</section> </section>
<div class="hSplit" style="--split-default: 50%" title="Drag to resize canvas height"></div> <div class="hSplit" style="--split-default: 50%" title="Drag to resize canvas height"></div>
<section> <div style="padding-bottom: 10px;">
<div class="vscroll"> <div>
<div id="editor" class="editor"></div> <button id="togglePhysics">Toggle Physics</button>
<button id="resetLayout">Reset Layout</button>
</div> </div>
<div style="height:100%">
<div class="vSplit" style="--split-default: 40%" title="Drag to resize terminal/editor width"></div> <div class="vscroll">
<div id="editor" class="editor"></div>
</div>
<div class="vscroll"> <div class="vSplit" style="--split-default: 40%" title="Drag to resize terminal/editor width"></div>
<pre id="terminal" class="terminal"></pre>
<div id="terminal" class="terminal"></div>
</div> </div>
</section> </div>
</div> </div>

View file

@ -1,12 +1,11 @@
type Axis = "x" | "y";
function clamp(n: number, min: number, max: number) { function clamp(n: number, min: number, max: number) {
return Math.max(min, Math.min(max, n)); return Math.max(min, Math.min(max, n));
} }
function parsePx(v: string | null, fallback: number): number { function getVarPx(el: HTMLElement, name: string, fallback: number) {
const v = getComputedStyle(el).getPropertyValue(name).trim();
if (!v) return fallback; if (!v) return fallback;
const s = v.trim().toLowerCase(); const s = v.toLowerCase();
if (s.endsWith("px")) { if (s.endsWith("px")) {
const n = Number(s.slice(0, -2)); const n = Number(s.slice(0, -2));
return Number.isFinite(n) ? n : fallback; return Number.isFinite(n) ? n : fallback;
@ -15,85 +14,91 @@ function parsePx(v: string | null, fallback: number): number {
return Number.isFinite(n) ? n : fallback; return Number.isFinite(n) ? n : fallback;
} }
function parsePercent(v: string | null, fallbackPct: number): number { function getVarPct(el: HTMLElement, name: string, fallback: number) {
if (!v) return fallbackPct; const v = getComputedStyle(el).getPropertyValue(name).trim();
const s = v.trim().toLowerCase(); if (!v) return fallback;
const s = v.toLowerCase();
if (s.endsWith("%")) { if (s.endsWith("%")) {
const n = Number(s.slice(0, -1)); const n = Number(s.slice(0, -1));
return Number.isFinite(n) ? n : fallbackPct; return Number.isFinite(n) ? n : fallback;
} }
const n = Number(s); const n = Number(s);
return Number.isFinite(n) ? n : fallbackPct; return Number.isFinite(n) ? n : fallback;
} }
function getCssVar(el: HTMLElement, name: string): string | null { function ensureFlexParent(parent: HTMLElement, axis: "row" | "column") {
const v = getComputedStyle(el).getPropertyValue(name); // Don't stomp on an existing layout if it's already flex in the right direction
return v ? v.trim() : null; const cs = getComputedStyle(parent);
if (cs.display !== "flex") parent.style.display = "flex";
parent.style.flexDirection = axis;
parent.style.overflow = "hidden";
} }
/** function ensurePaneCanShrink(pane: HTMLElement) {
* Generic rule: // Critical for nested flex layouts (otherwise children overflow)
* - hSplit controls the size of the FIRST pane (top) as a percent of parent height pane.style.minWidth = "0";
* - vSplit controls the size of the THIRD pane (right) as a percent of parent width pane.style.minHeight = "0";
*
* This matches common editor layouts:
* rows: [A][split][B] => A sized, B flex
* cols: [A][split][B] => B sized, A flex
*/
export function enableGenericSplitters() {
enableAll("y", ".hSplit");
enableAll("x", ".vSplit");
} }
function enableAll(axis: Axis, selector: string) { function setFixedSize(
for (const splitter of document.querySelectorAll<HTMLElement>(selector)) { pane: HTMLElement,
axis: "x" | "y",
px: number,
) {
// For flex: fixed pane should not grow/shrink, basis = px
pane.style.flexGrow = "0";
pane.style.flexShrink = "0";
pane.style.flexBasis = `${px}px`;
// Helps some browsers respect size
if (axis === "x") {
pane.style.width = `${px}px`;
} else {
pane.style.height = `${px}px`;
}
}
function setFlexFill(pane: HTMLElement) {
// Fill remaining space
pane.style.flex = "1 1 auto";
}
export function enableFlexSplitters() {
// Horizontal: A | hSplit | B (top/split/bottom)
for (const splitter of document.querySelectorAll<HTMLElement>(".hSplit")) {
const parent = splitter.parentElement as HTMLElement | null; const parent = splitter.parentElement as HTMLElement | null;
if (!parent) continue; if (!parent) continue;
// Require exactly A | splitter | B const kids = Array.from(parent.children) as HTMLElement[];
const kids = Array.from(parent.children);
if (kids.length !== 3 || kids[1] !== splitter) { if (kids.length !== 3 || kids[1] !== splitter) {
console.warn("Splitter parent must have exactly 3 children: A | splitter | B", parent); console.warn("hSplit parent must be A | splitter | B", parent);
continue; continue;
} }
const gap = axis === "y" ? splitter.getBoundingClientRect().height || 8 const a = kids[0];
: splitter.getBoundingClientRect().width || 8; const b = kids[2];
// Read per-splitter overrides from CSS variables (optional) ensureFlexParent(parent, "column");
// Defaults: ensurePaneCanShrink(a);
// - default size = 60% (hSplit) or 30% (vSplit) ensurePaneCanShrink(b);
// - minA/minB = 80/180 for hSplit, 220/220 for vSplit setFlexFill(b); // bottom fills
const defaultPct = parsePercent(
getCssVar(splitter, "--split-default"),
axis === "y" ? 60 : 30,
);
const minA = parsePx( const gap = splitter.getBoundingClientRect().height || 8;
getCssVar(splitter, "--split-min-a"), splitter.style.flex = `0 0 ${gap}px`;
axis === "y" ? 80 : 220,
);
const minB = parsePx( // Optional per-splitter CSS vars:
getCssVar(splitter, "--split-min-b"), // --split-default: 60% (of parent height)
axis === "y" ? 180 : 220, // --split-min-a: 80px
); // --split-min-b: 180px
const defPct = getVarPct(splitter, "--split-default", 60);
const minA = getVarPx(splitter, "--split-min-a", 80);
const minB = getVarPx(splitter, "--split-min-b", 180);
// Make parent a grid automatically (no container classes needed) // Set initial size (A is fixed)
parent.style.display = "grid"; {
parent.style.overflow = "hidden"; const r = parent.getBoundingClientRect();
const px = clamp((defPct / 100) * r.height, minA, r.height - gap - minB);
// Apply initial template if none set yet setFixedSize(a, "y", px);
if (axis === "y") {
// top sized in %, bottom flex
if (!parent.style.gridTemplateRows) {
parent.style.gridTemplateRows = `${defaultPct}% ${gap}px 1fr`;
}
} else {
// right sized in %, left flex
if (!parent.style.gridTemplateColumns) {
parent.style.gridTemplateColumns = `1fr ${gap}px ${defaultPct}%`;
}
} }
let dragging = false; let dragging = false;
@ -101,29 +106,18 @@ function enableAll(axis: Axis, selector: string) {
splitter.addEventListener("pointerdown", (e) => { splitter.addEventListener("pointerdown", (e) => {
dragging = true; dragging = true;
splitter.setPointerCapture(e.pointerId); splitter.setPointerCapture(e.pointerId);
document.body.style.cursor = axis === "y" ? "row-resize" : "col-resize"; document.body.style.cursor = "row-resize";
e.preventDefault(); e.preventDefault();
}); });
splitter.addEventListener("pointermove", (e) => { splitter.addEventListener("pointermove", (e) => {
if (!dragging) return; if (!dragging) return;
const rect = parent.getBoundingClientRect(); const r = parent.getBoundingClientRect();
const y = e.clientY - r.top;
if (axis === "y") { const maxA = r.height - gap - minB;
// control FIRST pane size (top) by mouse Y const newA = clamp(y, minA, maxA);
const y = e.clientY - rect.top; setFixedSize(a, "y", newA);
const maxA = rect.height - gap - minB;
const newA = clamp(y, minA, maxA);
const pct = (newA / rect.height) * 100;
parent.style.gridTemplateRows = `${pct}% ${gap}px 1fr`;
} else {
// control THIRD pane size (right) by distance from right edge
const xFromRight = rect.right - e.clientX;
const maxB = rect.width - gap - minA;
const newB = clamp(xFromRight, minB, maxB);
const pct = (newB / rect.width) * 100;
parent.style.gridTemplateColumns = `1fr ${gap}px ${pct}%`;
}
}); });
splitter.addEventListener("pointerup", (e) => { splitter.addEventListener("pointerup", (e) => {
@ -136,33 +130,76 @@ function enableAll(axis: Axis, selector: string) {
dragging = false; dragging = false;
document.body.style.cursor = ""; document.body.style.cursor = "";
}); });
}
// Optional: keep within bounds on resize (no stored state needed) // Vertical: A | vSplit | B (left/split/right)
globalThis.window.addEventListener("resize", () => { for (const splitter of document.querySelectorAll<HTMLElement>(".vSplit")) {
const rect = parent.getBoundingClientRect(); const parent = splitter.parentElement as HTMLElement | null;
if (axis === "y") { if (!parent) continue;
// read current pct from template if possible; otherwise skip
const parts = (parent.style.gridTemplateRows || "").split(" "); const kids = Array.from(parent.children) as HTMLElement[];
if (parts.length >= 3 && parts[0].endsWith("%")) { if (kids.length !== 3 || kids[1] !== splitter) {
const pct = parseFloat(parts[0]); console.warn("vSplit parent must be A | splitter | B", parent);
const px = (pct / 100) * rect.height; continue;
const maxA = rect.height - gap - minB; }
const clampedPx = clamp(px, minA, maxA);
const clampedPct = (clampedPx / rect.height) * 100; const a = kids[0];
parent.style.gridTemplateRows = `${clampedPct}% ${gap}px 1fr`; const b = kids[2];
}
} else { ensureFlexParent(parent, "row");
const parts = (parent.style.gridTemplateColumns || "").split(" "); ensurePaneCanShrink(a);
if (parts.length >= 3 && parts[2].endsWith("%")) { ensurePaneCanShrink(b);
const pct = parseFloat(parts[2]); setFlexFill(a); // left fills
const px = (pct / 100) * rect.width;
const maxB = rect.width - gap - minA; const gap = splitter.getBoundingClientRect().width || 8;
const clampedPx = clamp(px, minB, maxB); splitter.style.flex = `0 0 ${gap}px`;
const clampedPct = (clampedPx / rect.width) * 100;
parent.style.gridTemplateColumns = `1fr ${gap}px ${clampedPct}%`; // Optional per-splitter CSS vars:
} // --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 minA = getVarPx(splitter, "--split-min-a", 220);
const minB = getVarPx(splitter, "--split-min-b", 220);
// Set initial size (B is fixed)
{
const r = parent.getBoundingClientRect();
const px = clamp((defPct / 100) * r.width, minB, r.width - gap - minA);
setFixedSize(b, "x", px);
}
let dragging = false;
splitter.addEventListener("pointerdown", (e) => {
dragging = true;
splitter.setPointerCapture(e.pointerId);
document.body.style.cursor = "col-resize";
e.preventDefault();
});
splitter.addEventListener("pointermove", (e) => {
if (!dragging) return;
const r = parent.getBoundingClientRect();
// Right pane width = distance from right edge
const xFromRight = r.right - e.clientX;
const maxB = r.width - gap - minA;
const newB = clamp(xFromRight, minB, maxB);
setFixedSize(b, "x", newB);
});
splitter.addEventListener("pointerup", (e) => {
dragging = false;
document.body.style.cursor = "";
splitter.releasePointerCapture(e.pointerId);
});
splitter.addEventListener("pointercancel", () => {
dragging = false;
document.body.style.cursor = "";
}); });
} }
} }
enableGenericSplitters(); enableFlexSplitters();

View file

@ -129,6 +129,8 @@ function createGraph(): vis.Network {
{ nodes, edges }, { nodes, edges },
{ {
layout: { improvedLayout: true }, layout: { improvedLayout: true },
autoResize: true,
width: "99%",
physics: { physics: {
enabled: true, enabled: true,
solver: "barnesHut", solver: "barnesHut",