feat(editor): console copy button + collapsible DOM-floor console + one blue family

The first prod blue screen (finally!) also surfaced the console UX gaps: the
in-app log had to be hand-copied (and arrived truncated — the flight-recorder
dump missing), and the DOM-floor screen's log was a fixed block.

- React console: a copy button beside the toggle (writes the full log to the
  clipboard, confirms in the log); stays collapsible on a fatal as in normal
  editing.
- DOM-floor screen: its console mirrors the editor one — toggle bar
  (▾/▸ console) + copy, collapsible — and its content is ring + trace dump.
- One blue family: both fatal screens now use the boot overlay's #1a1a2e, so
  every full-screen state (boot, consent, fatal, floor) is coherent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
This commit is contained in:
Gergő Törcsvári 2026-08-01 19:50:01 +02:00
commit 875195f721
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
2 changed files with 66 additions and 15 deletions

View file

@ -2373,7 +2373,7 @@ export function WasmTool({
{fatal && (
<div
data-testid="fatal-overlay"
className="absolute inset-0 z-[35] flex flex-col items-center justify-center gap-3 bg-[#1e3a8a] text-white"
className="absolute inset-0 z-[35] flex flex-col items-center justify-center gap-3 bg-[#1a1a2e] text-white"
>
<p className="font-mono text-4xl text-white/90">:(</p>
<p className="font-mono text-sm text-white">
@ -2403,13 +2403,28 @@ export function WasmTool({
visible on a fatal even with chrome hidden, for the same reason. */}
{(!effectiveChromeHidden || fatal) && (
<div className="absolute bottom-0 left-0 right-0 z-40">
<button
className="flex items-center gap-1 bg-black/70 px-3 py-1 font-mono text-xs text-white"
onClick={() => setShowLog((s) => !s)}
>
{showLog ? <ChevronDown size={14} /> : <ChevronUp size={14} />} console
({logs.length})
</button>
<div className="flex items-center bg-black/70">
<button
className="flex items-center gap-1 px-3 py-1 font-mono text-xs text-white"
onClick={() => setShowLog((s) => !s)}
>
{showLog ? <ChevronDown size={14} /> : <ChevronUp size={14} />} console
({logs.length})
</button>
{showLog && (
<button
className="ml-auto px-3 py-1 font-mono text-xs text-white/70 hover:text-white"
onClick={() => {
void navigator.clipboard.writeText(logs.join("\n")).then(
() => append("[console] copied to clipboard"),
() => append("[console] clipboard copy failed"),
);
}}
>
copy
</button>
)}
</div>
{showLog && (
<pre className="max-h-64 overflow-auto bg-black/85 p-3 font-mono text-[11px] leading-tight text-green-300">
{logs.join("\n")}

View file

@ -36,8 +36,10 @@ function buildDom(): void {
const root = doc.createElement("div");
root.id = "pcbjam-fatal-screen";
root.setAttribute("data-testid", "fatal-screen-dom");
// Same blue as the boot overlay / React fatal screen (#1a1a2e) — one
// coherent family across every full-screen state.
root.style.cssText =
"position:fixed;inset:0;z-index:2147483000;background:#1e3a8a;color:#fff;" +
"position:fixed;inset:0;z-index:2147483000;background:#1a1a2e;color:#fff;" +
"display:flex;flex-direction:column;align-items:center;justify-content:center;" +
"gap:12px;font-family:ui-monospace,Menlo,monospace;padding:24px;box-sizing:border-box;";
@ -67,14 +69,48 @@ function buildDom(): void {
"padding:4px 14px;font-size:12px;cursor:pointer;font-family:inherit;";
reload.onclick = () => window.location.reload();
// Console block mirroring the in-editor one: toggle bar + copy, collapsible.
const consoleWrap = doc.createElement("div");
consoleWrap.style.cssText = "position:absolute;bottom:0;left:0;right:0;text-align:left;";
const bar = doc.createElement("div");
bar.style.cssText =
"display:flex;align-items:center;background:#000000b3;font-size:11px;";
const logText = () => [...ring, "", dump()].join("\n");
const toggle = doc.createElement("button");
toggle.textContent = "▾ console";
toggle.style.cssText =
"border:0;background:transparent;color:#fff;padding:4px 12px;cursor:pointer;font:inherit;";
const copy = doc.createElement("button");
copy.textContent = "copy";
copy.style.cssText =
"border:0;background:transparent;color:#ffffffb3;padding:4px 12px;cursor:pointer;" +
"font:inherit;margin-left:auto;";
copy.onclick = () => {
navigator.clipboard.writeText(logText()).then(
() => { copy.textContent = "copied ✓"; },
() => { copy.textContent = "copy failed"; },
);
};
const log = doc.createElement("pre");
log.style.cssText =
"position:absolute;bottom:0;left:0;right:0;max-height:38vh;overflow:auto;" +
"margin:0;padding:10px 12px;background:#000000d9;color:#86efac;" +
"font-size:11px;line-height:1.35;text-align:left;";
log.textContent = [...ring, "", dump()].join("\n");
"max-height:38vh;overflow:auto;margin:0;padding:10px 12px;background:#000000d9;" +
"color:#86efac;font-size:11px;line-height:1.35;";
log.textContent = logText();
root.append(face, title, err, hint, reload, log);
toggle.onclick = () => {
const hidden = log.style.display === "none";
log.style.display = hidden ? "" : "none";
toggle.textContent = hidden ? "▾ console" : "▸ console";
};
bar.append(toggle, copy);
consoleWrap.append(bar, log);
root.append(face, title, err, hint, reload, consoleWrap);
doc.body.appendChild(root);
}