lumbda/www/repl/repl.css
russell@unturf.com 2b46c1b79e
repl portal: overwrite-guard, rename, export/import, cross-tab restore
Three UX gaps on the portal-bar closed in one pass plus a defensive
fix on the C tier's heap probe:

* Overwrite guard — saving with an existing name asks 'overwrite?'
  with the existing entry's tier + savedAt. Rename via dbl-click on
  the chip label; same overwrite guard applies on rename.

* Export / import — a ⇣ icon on each chip downloads it as
  <name>.portal.json (opaque blob for c/python, replay-inputs for
  asm). A 📁 import button on the portal-bar accepts a .portal.json
  file via hidden <input type="file">; collisions prompt overwrite,
  decline auto-suffixes (baseName-2, -3, …) so importing a 2nd copy
  always lands somewhere.

* Cross-tab restore — a 📂 this tab / 🌐 all tabs toggle switches
  the chip strip between the active tab's checkpoints and every
  tab's. Global chips render as 'name · tabName' with a dashed
  border; click restores the snapshot into the active tab (the
  saved cp is passed through restoreCheckpoint's new sourceCp
  argument so the chip doesn't need a checkpoints[name] match on
  the active tab). Edit/delete are hidden in global mode — the
  user switches to the owning tab to manage chips.

* heapStats defensive — wasm/c/lumbda-c.loader.js now returns null
  when module.HEAPU8 isn't live yet (caught by the heap poll firing
  during the tiny window between tier reboot and Module init), so a
  restore no longer surfaces 'Cannot read properties of undefined
  (reading byteLength)' as a TypeError.

Smoke-tested headlessly: overwrite confirm fires with the expected
message; rename via dblclick swaps the label; ⇣ produces a download
named '<name>.portal.json'; toggle shows both tabs' chips with the
'· tabName' annotation; import round-trips back into the receiving
tab. Zero page errors across the full flow.
2026-06-15 08:14:53 -04:00

382 lines
11 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* lumbda repl — grid-only layout. Inherits palette + base from style.css. */
body.repl {
/* Plain document flow — no min-height, no overflow. The page scrolls
* naturally when content exceeds the viewport; the prompt-bar uses
* position: sticky so it stays glued to the bottom of the viewport in
* that case, and sits right under the transcript otherwise. */
}
/* ─── Lock screen overlay ──────────────────────────────────────── */
.lock-screen {
position: fixed; inset: 0; z-index: 1000;
display: grid;
place-items: center;
background: rgba(15, 17, 21, 0.85);
backdrop-filter: blur(4px);
}
.lock-card {
background: var(--pane-bg);
border: 1px solid var(--rule);
border-radius: 6px;
padding: 1.5rem 2rem;
max-width: 28rem;
width: 90%;
display: grid;
gap: 0.4rem;
}
.lock-card h2 {
margin: 0 0 0.1rem;
font-family: 'chunkfive', Georgia, serif;
font-size: 1.4rem;
color: var(--green);
font-weight: normal;
}
.lock-card p {
margin: 0;
font-size: 0.85rem;
color: var(--muted);
line-height: 1.5;
}
.lock-card input[type=password] {
width: 100%;
padding: 0.5rem 0.7rem;
margin: 0.4rem 0 0;
background: var(--code-bg);
color: var(--fg);
border: 1px solid var(--rule);
border-radius: 3px;
font-family: var(--mono);
font-size: 0.95rem;
}
.lock-card input[type=password]:focus {
outline: none;
border-color: var(--green);
}
.lock-row {
display: grid;
grid-template-columns: auto auto;
gap: 0.6rem;
margin-top: 0.5rem;
justify-content: start;
}
.lock-row button {
font-family: var(--mono);
font-size: 0.85em; font-weight: 600;
padding: 0.45rem 1.1rem;
border-radius: 3px;
cursor: pointer;
border: 1px solid var(--green);
}
.lock-row .primary { background: var(--green); color: var(--bg); }
.lock-row .secondary { background: transparent; color: var(--green); }
.lock-row .primary:hover { filter: brightness(1.1); }
.lock-row .secondary:hover { background: var(--code-bg); }
.lock-note {
margin: 0.4rem 0 0;
color: var(--err);
font-size: 0.8rem;
min-height: 1.2em;
}
.lock-explain {
margin: 0.4rem 0 0;
font-size: 0.78rem;
color: var(--muted);
}
.lock-explain summary {
cursor: pointer;
color: var(--green);
user-select: none;
}
.lock-explain summary:hover { text-decoration: underline; }
.lock-explain ul {
margin: 0.5rem 0 0;
padding-left: 1.1rem;
display: grid;
gap: 0.3rem;
}
.lock-explain li { line-height: 1.45; }
.lock-explain code {
background: var(--code-bg);
padding: 0 0.2em;
border-radius: 2px;
font-size: 0.92em;
}
.lock-explain a {
color: var(--green);
text-decoration: none;
}
.lock-explain a:hover { text-decoration: underline; }
.lock-explain strong { color: var(--fg); }
/* ─── Tab bar ──────────────────────────────────────────────────── */
.tabbar {
/* Sticks to the top of the viewport so it stays visible after the
* header scrolls away — keeps the active-session controls reachable
* even after a long transcript pushes them off-screen. */
position: sticky;
top: 0;
z-index: 40;
display: grid;
/* tabs · new-tab · spacer · heap-pressure (injected by repl.js) ·
* reboot · clear · cancel · lock — 8 cells. Extend if the toolbar
* grows; CSS Grid drops trailing children onto a new row, which is
* how lock used to wrap. */
grid-template-columns: auto auto 1fr auto auto auto auto auto;
gap: 0.3rem;
align-items: center;
padding: 0.4rem 0.4rem;
margin: 0 0 0.4rem;
background: var(--code-bg);
border-bottom: 1px solid var(--rule);
}
.tabs {
display: grid;
grid-auto-flow: column;
grid-auto-columns: max-content;
gap: 0.3rem;
overflow-x: auto;
}
.tab {
background: var(--pane-bg);
border: 1px solid var(--rule);
padding: 0.25rem 0.6rem;
border-radius: 3px 3px 0 0;
font-size: 0.85em;
cursor: pointer;
display: inline-grid;
grid-template-columns: auto auto;
gap: 0.3rem;
align-items: center;
white-space: nowrap;
}
.tab.active {
background: var(--code-bg);
border-bottom-color: var(--code-bg);
color: var(--green);
}
.tab .tab-close {
opacity: 0.5;
cursor: pointer;
padding: 0 0.2rem;
}
.tab .tab-close:hover { opacity: 1; color: var(--err); }
.tabbar .ghost {
background: transparent;
border: 1px solid var(--rule);
color: var(--muted);
font-family: var(--mono);
font-size: 0.8em;
padding: 0.2rem 0.6rem;
border-radius: 3px;
cursor: pointer;
}
.tabbar .ghost:hover:not(:disabled) {
border-color: var(--green); color: var(--green);
}
.tabbar .ghost:disabled { opacity: 0.4; cursor: default; }
.tabbar .heap-pressure {
color: var(--muted);
font-size: 0.72em;
font-family: var(--mono);
white-space: nowrap;
padding: 0 0.4rem;
}
/* Portal bar — REPL save/resume controls. Sits between tabbar and
* transcript. Hidden when the active tier has no portal support yet
* (the asm-wat tier stays grey). */
.portal-bar {
display: grid;
/* label · save · import · scope · chips-row */
grid-template-columns: auto auto auto auto 1fr;
gap: 0.4rem;
align-items: center;
padding: 0.2rem 0.5rem;
background: var(--code-bg);
border-bottom: 1px solid var(--rule);
font-size: 0.78em;
font-family: var(--mono);
}
.portal-bar .portal-label {
color: var(--muted);
}
.portal-bar .portal-chips {
display: grid;
grid-auto-flow: column;
grid-auto-columns: max-content;
gap: 0.3rem;
overflow-x: auto;
}
.portal-chips .chip {
display: inline-grid;
/* label · ⇣ download · × delete */
grid-template-columns: auto auto auto;
gap: 0.3rem;
align-items: center;
background: var(--pane-bg);
border: 1px solid var(--rule);
border-radius: 3px;
padding: 0.1rem 0.4rem;
color: var(--green);
cursor: pointer;
white-space: nowrap;
}
.portal-chips .chip:hover { border-color: var(--green); }
.portal-chips .chip-global {
/* Cross-tab snapshots get a muted accent so they read as "borrowed
* from another tab" rather than belonging here. */
border-style: dashed;
opacity: 0.85;
}
.portal-chips .chip .chip-export,
.portal-chips .chip .chip-close {
color: var(--muted);
font-size: 0.85em;
cursor: pointer;
padding: 0 0.15rem;
}
.portal-chips .chip .chip-export:hover { color: var(--green); }
.portal-chips .chip .chip-close:hover { color: var(--err); }
/* ─── Transcript ───────────────────────────────────────────────── */
/* The REPL stream is the page's only scroller. Header stays pinned;
* tabs, transcript, and prompt all flow inside this one scroller so
* fresh sessions show the prompt up near the top and it drifts down
* with each new entry. */
.repl-stream {
background: var(--code-bg);
display: grid;
grid-template-rows: auto auto auto;
align-content: start;
}
.transcript {
/* Same horizontal inset as the active prompt-bar so prior λ> sigils
* line up with the live one. No max-width / margin: auto here — a
* centered transcript on wide viewports would mis-align with the
* left-flush prompt-bar. Long lines wrap inside the entry instead. */
padding: 0.4rem 0.4rem 0;
font-family: var(--mono);
font-size: 0.92em;
line-height: 1.45;
}
.transcript .entry {
margin: 0;
padding: 0;
}
.transcript .entry .prompt-line {
color: var(--green);
white-space: pre-wrap;
word-break: break-word;
}
.transcript .entry .prompt-line .sigil {
display: inline-block;
width: 3ch;
opacity: 0.7;
}
.transcript .entry .tier-output {
display: block;
padding: 0;
color: var(--fg);
}
.transcript .entry .tier-result {
white-space: pre-wrap;
word-break: break-word;
padding-left: 3ch; /* line up under "λ> " */
}
.transcript .entry .tier-meta {
color: var(--muted);
font-size: 0.78em;
margin-left: 0.6em;
user-select: none;
}
.transcript .entry .tier-meta::before {
content: "; "; /* render as a lisp comment */
color: var(--muted);
opacity: 0.6;
}
.transcript .entry.error .tier-result { color: var(--err); }
.transcript .entry + .entry { margin-top: 0.4rem; }
/* ─── Prompt bar ───────────────────────────────────────────────── */
.prompt-bar {
/* Flows directly under the last transcript entry — connected, no gap.
* Sticks to the viewport bottom once the page scrolls past it. */
position: sticky;
bottom: 0;
z-index: 50;
display: grid;
grid-template-columns: auto 1fr auto auto;
gap: 0.4rem;
/* Baseline-align: sigil glyph sits on the textarea's first-line
* baseline whether the textarea is 1 row or 40. Center alignment
* with top-aligned sigil drifts visibly off the input baseline on
* single-line input; top alignment without baseline drifts the
* sigil ABOVE the input. Baseline tracks the first line in either
* case, which is what the eye expects from a prompt. */
align-items: baseline;
padding: 0.3rem 0.4rem 0.5rem;
background: var(--code-bg);
}
.prompt-bar .prompt-sigil {
color: var(--green);
font-weight: 600;
/* Match textarea font metrics so the baselines actually line up;
* a larger sigil font would push its baseline higher than the
* textarea's by the cap-height difference. */
font-family: var(--mono);
font-size: 0.92em;
line-height: 1.4;
}
.prompt-bar textarea {
resize: none;
background: var(--code-bg);
border: none;
color: var(--fg);
padding: 0.25rem 0;
font-family: var(--mono);
font-size: 0.92em;
/* Pin line-height so multi-line content + sigil baseline alignment
* stay predictable. Browser default for textarea is 'normal' which
* varies per font; matching .prompt-sigil keeps both baselines on
* the same grid line. */
line-height: 1.4;
min-height: 1.5em;
max-height: 30vh;
min-width: 0;
}
.prompt-bar textarea:focus { outline: none; border-color: var(--green); }
.prompt-bar select {
background: var(--code-bg);
border: 1px solid var(--rule);
color: var(--fg);
padding: 0.35rem 0.5rem;
font-family: var(--mono);
font-size: 0.85em;
border-radius: 3px;
}
.prompt-bar button.primary {
background: var(--green);
color: var(--bg);
border: 1px solid var(--green);
padding: 0.4rem 1rem;
font-family: var(--mono);
font-weight: 600;
border-radius: 3px;
cursor: pointer;
}
.prompt-bar button.primary:hover { filter: brightness(1.1); }
.prompt-bar button.primary:disabled { opacity: 0.4; cursor: wait; }
kbd {
font-family: var(--mono);
background: var(--code-bg);
padding: 0.05em 0.3em;
border: 1px solid var(--rule);
border-radius: 2px;
font-size: 0.85em;
}