Until now the input was pinned at rows="1" — paste a 40-line program and you edited it through a one-line keyhole. autosizeInput() now sets height to scrollHeight on every input event, history navigation, and post-send clear. CSS min/max-height (1.5em / 30vh) clamp the bounds; once the textarea hits 30vh it scrolls internally instead of pushing the prompt-bar off screen. Prompt sigil top-aligns (align-self: start + matching padding-top) so the λ> stays on the first line of input instead of drifting to vertical-center as the textarea grows. Smoke-tested headless: 1-line stays 22px, 6-line grows to 97px, 40-line caps at 216px (30vh of 720px) with scrollHeight 608 (internal scroll engaged), send empties back to 22px. Zero page errors.
362 lines
9.6 KiB
CSS
362 lines
9.6 KiB
CSS
/* 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;
|
|
grid-template-columns: 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;
|
|
grid-template-columns: 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 .chip-close {
|
|
color: var(--muted);
|
|
font-size: 0.85em;
|
|
cursor: pointer;
|
|
padding: 0 0.15rem;
|
|
}
|
|
.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;
|
|
align-items: center;
|
|
padding: 0.3rem 0.4rem 0.5rem;
|
|
background: var(--code-bg);
|
|
}
|
|
.prompt-bar .prompt-sigil {
|
|
color: var(--green);
|
|
font-weight: 600;
|
|
font-size: 0.95em;
|
|
/* Top-align so multi-line input keeps λ> on the first line instead
|
|
* of drifting to vertical-center as the textarea grows. The padding
|
|
* matches the textarea's padding-top so the sigil sits on the same
|
|
* baseline as the first character. */
|
|
align-self: start;
|
|
padding-top: 0.4rem;
|
|
}
|
|
.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;
|
|
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;
|
|
}
|