From b5f9f4561302e0e31804bef839591e4a20f57a79 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 14 Oct 2026 00:13:54 +0530 Subject: [PATCH 01/17] fix(web): visible keyboard focus outlines for interactive controls --- web/style.css | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/web/style.css b/web/style.css index 8ce5915..6d2ada0 100644 --- a/web/style.css +++ b/web/style.css @@ -224,6 +224,15 @@ button { button:hover:not(:disabled) { background: var(--accent); } button:disabled { opacity: 0.45; cursor: not-allowed; } +/* Keyboard focus must be visible; default outlines were removed + for the editor, so restore them for interactive controls. */ +button:focus-visible, +.examples select:focus-visible, +#editor:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + #output { flex: 1; padding: 0.9rem; From 1a3547292ce902896e801090d0123554ccbab1cc Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 14 Oct 2026 00:14:11 +0530 Subject: [PATCH 02/17] fix(web): disable clear-output button when output is empty --- web/main.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/web/main.js b/web/main.js index d3b6e32..20572ae 100644 --- a/web/main.js +++ b/web/main.js @@ -685,6 +685,7 @@ function runCode() { if (!code.trim()) return; output.textContent = ""; + updateClearBtn(); runBtn.disabled = true; runId += 1; const id = runId; @@ -710,6 +711,7 @@ function renderResult(result) { if (result.output) appendOutput(result.output.replace(/\n$/, "")); if (result.value) appendOutput(result.value); for (const err of result.errors ?? []) appendOutput(err, "err"); + updateClearBtn(); } function appendOutput(text, cls) { @@ -718,10 +720,14 @@ function appendOutput(text, cls) { div.textContent = text; output.append(div); output.scrollTop = output.scrollHeight; + updateClearBtn(); } +function updateClearBtn() { + clearBtn.disabled = !output.textContent.trim(); +} runBtn.addEventListener("click", runCode); -clearBtn.addEventListener("click", () => (output.textContent = "")); +clearBtn.addEventListener("click", () => { output.textContent = ""; updateClearBtn(); }); // ------------------------------------------------------------ examples @@ -760,8 +766,11 @@ const examplesSelect = document.getElementById("examples"); function loadExample(key) { setEditorSource(EXAMPLES[key] ?? EXAMPLES.countdown); output.textContent = ""; + updateClearBtn(); } +updateClearBtn(); + examplesSelect?.addEventListener("change", (e) => loadExample(e.target.value)); // default From 178113e07028068c1bd37032b1b5ff1bcd591fc2 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 14 Oct 2026 00:14:14 +0530 Subject: [PATCH 03/17] fix(web): bound pane heights on small screens so layout stays usable --- web/style.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/style.css b/web/style.css index 6d2ada0..2f700a8 100644 --- a/web/style.css +++ b/web/style.css @@ -65,6 +65,8 @@ main { @media (max-width: 900px) { main { grid-template-columns: 1fr; grid-template-rows: none; } + .pane { min-height: 240px; max-height: 60vh; } + #editor, #output { max-height: 45vh; } } @media (min-width: 901px) { From 6f3f2739c985866b69733a7f8d684c9fe6a57f23 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 14 Oct 2026 00:16:51 +0530 Subject: [PATCH 04/17] fix(web): show running state on run button and restore label --- web/main.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web/main.js b/web/main.js index 20572ae..896d764 100644 --- a/web/main.js +++ b/web/main.js @@ -36,6 +36,7 @@ function spawnWorker() { worker.terminate(); worker = spawnWorker(); runBtn.disabled = false; + runBtn.textContent = "run (ctrl+enter)"; appendOutput("interpreter restarted; saved bindings are gone.", "err"); }; w.postMessage({ type: "keywords" }); @@ -54,12 +55,14 @@ function onWorkerMessage(e) { clearTimeout(pendingRun.timer); pendingRun = null; runBtn.disabled = false; + runBtn.textContent = "run (ctrl+enter)"; renderResult(msg.result); } break; case "boot-error": appendOutput(`failed to load peepo.wasm: ${msg.error}`, "err"); - break; + updateClearBtn(); + break; } } @@ -106,6 +109,7 @@ async function loadEmotes(keywords) { normalize(); // re-render whatever is already in the editor runBtn.disabled = false; + runBtn.textContent = "run (ctrl+enter)"; } function emoteUrl(emote) { @@ -687,6 +691,7 @@ function runCode() { output.textContent = ""; updateClearBtn(); runBtn.disabled = true; + runBtn.textContent = "running…"; runId += 1; const id = runId; const timer = setTimeout(() => { @@ -695,6 +700,7 @@ function runCode() { worker.terminate(); worker = spawnWorker(); runBtn.disabled = false; + runBtn.textContent = "run (ctrl+enter)"; appendOutput( "run exceeded 5s and was killed (infinite loop?). " + "the worker restarted, so saved bindings are gone.", From 65b883237aa0891ee592772f8a801d4c2b84fcf6 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 14 Oct 2026 00:16:56 +0530 Subject: [PATCH 05/17] fix(web): close autocomplete on outside click and support cmd+enter run --- web/main.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/main.js b/web/main.js index 896d764..340244c 100644 --- a/web/main.js +++ b/web/main.js @@ -476,6 +476,10 @@ editor.addEventListener("paste", (e) => { editor.addEventListener("blur", () => hideAutocompleteSoon()); +document.addEventListener("mousedown", (e) => { + if (!acEl.contains(e.target) && e.target !== editor) hideAutocomplete(); +}); + // The editor is the only scroller; the gutter mirrors it vertically and // never moves horizontally (wide lines scroll under fixed numbers, like // a real editor). @@ -675,7 +679,7 @@ editor.addEventListener("keydown", (e) => { }); document.addEventListener("keydown", (e) => { - if (e.key === "Enter" && e.ctrlKey) { + if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) { e.preventDefault(); runCode(); } From ef5c5d2b1373c989e52b758380141c44b974f566 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 14 Oct 2026 00:17:00 +0530 Subject: [PATCH 06/17] style(web): distinguish primary run button from secondary clear button --- web/style.css | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/web/style.css b/web/style.css index 2f700a8..ffe2b5e 100644 --- a/web/style.css +++ b/web/style.css @@ -221,11 +221,22 @@ button { border-radius: 6px; padding: 0.5rem 1rem; font-size: 0.85rem; + font-weight: 500; cursor: pointer; + transition: background 0.15s ease, transform 0.05s ease; } button:hover:not(:disabled) { background: var(--accent); } +button:active:not(:disabled) { transform: translateY(1px); } button:disabled { opacity: 0.45; cursor: not-allowed; } +/* Secondary button for non-destructive utilities. */ +#clear-btn { + background: transparent; + border: 1px solid var(--panel-edge); + color: var(--muted); +} +#clear-btn:hover:not(:disabled) { background: var(--panel-edge); color: var(--text); } + /* Keyboard focus must be visible; default outlines were removed for the editor, so restore them for interactive controls. */ button:focus-visible, From edb339e3b3f12dafcc4cd58f3f9cce424be4d9e2 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 14 Oct 2026 00:17:03 +0530 Subject: [PATCH 07/17] style(web): add friendly empty state to output pane --- web/style.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/web/style.css b/web/style.css index ffe2b5e..1b2d372 100644 --- a/web/style.css +++ b/web/style.css @@ -259,6 +259,13 @@ button:focus-visible, #output .err { color: var(--error); } #output .meta { color: var(--muted); font-style: italic; } +/* Friendly empty state so the right pane never looks broken. */ +#output:empty::before { + content: "run your code to see output here"; + color: var(--muted); + font-style: italic; +} + footer { padding: 0.5rem 1.25rem 0.9rem; color: var(--muted); From ec1ee9c6d2190198b6886d08acef4c6bc964271d Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 14 Oct 2026 00:17:08 +0530 Subject: [PATCH 08/17] style(web): polish panes, autocomplete panel, and shadows --- web/style.css | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/web/style.css b/web/style.css index 1b2d372..0b9a759 100644 --- a/web/style.css +++ b/web/style.css @@ -82,9 +82,10 @@ main { flex-direction: column; background: var(--panel); border: 1px solid var(--panel-edge); - border-radius: 8px; + border-radius: 10px; overflow: hidden; min-height: 320px; + box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35); } .pane-title { @@ -170,14 +171,15 @@ main { #autocomplete { display: none; position: fixed; - z-index: 10; - min-width: 220px; - max-height: 260px; + z-index: 20; + min-width: 240px; + max-height: 300px; overflow-y: auto; - background: #1f1f23; + background: rgba(31, 31, 35, 0.92); + backdrop-filter: blur(6px); border: 1px solid var(--panel-edge); - border-radius: 8px; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); + border-radius: 10px; + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.55); } .ac-item { From 2e46c5caed54a628586cd49f18cc8627a825994e Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 14 Oct 2026 00:17:14 +0530 Subject: [PATCH 09/17] a11y(web): expose editor as multiline textbox and add page description --- web/index.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/index.html b/web/index.html index 4bdc9e9..c9825a6 100644 --- a/web/index.html +++ b/web/index.html @@ -3,6 +3,7 @@ + peepoScript playground @@ -29,8 +30,8 @@

peepoScript playground

-
+
From 1d1db3f1e0f5d7f9c98f4306ada9622e8fe70019 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 14 Oct 2026 00:17:17 +0530 Subject: [PATCH 10/17] ux(web): add tooltip to clear output button --- web/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/index.html b/web/index.html index c9825a6..dc5eb70 100644 --- a/web/index.html +++ b/web/index.html @@ -35,7 +35,7 @@

peepoScript playground

- +
From e40996da1ac82290899c13ebe991aadfcc018aa7 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 14 Oct 2026 00:17:21 +0530 Subject: [PATCH 11/17] ux(web): reflect wasm boot failure in status indicator --- web/main.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web/main.js b/web/main.js index 340244c..37fe9aa 100644 --- a/web/main.js +++ b/web/main.js @@ -61,8 +61,10 @@ function onWorkerMessage(e) { break; case "boot-error": appendOutput(`failed to load peepo.wasm: ${msg.error}`, "err"); - updateClearBtn(); - break; + statusEl.textContent = "interpreter failed to load"; + statusEl.classList.add("error"); + updateClearBtn(); + break; } } From 7787f18adeeeb73d34cab21c0cd77e6212382039 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 14 Oct 2026 00:17:32 +0530 Subject: [PATCH 12/17] ux(web): make Tab indent when no completion candidate --- web/main.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/web/main.js b/web/main.js index 37fe9aa..fa64dd5 100644 --- a/web/main.js +++ b/web/main.js @@ -671,9 +671,15 @@ editor.addEventListener("keydown", (e) => { if (e.key === "Tab") { // No popup yet: complete directly when there is exactly one // candidate, otherwise open the popup for the word at the caret. - e.preventDefault(); + // If there is no word at the caret, fall through to normal tab + // insertion so indentation still works. const hit = currentWordBeforeCaret(); - if (!hit) return; + if (!hit) { + e.preventDefault(); + document.execCommand("insertText", false, " "); + return; + } + e.preventDefault(); const cands = completionCandidates(hit.word).filter((n) => n !== hit.word); if (cands.length === 1) acceptAutocomplete(cands[0]); else updateAutocomplete(); From ea797f3f1277e725bc8c62bf01c3eb9861643829 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 14 Oct 2026 00:18:16 +0530 Subject: [PATCH 13/17] feat(web): add copy-output button --- web/index.html | 5 ++++- web/main.js | 13 +++++++++++++ web/style.css | 7 +++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/web/index.html b/web/index.html index dc5eb70..659cbcc 100644 --- a/web/index.html +++ b/web/index.html @@ -40,7 +40,10 @@

peepoScript playground

-
output
+
+ output + +
diff --git a/web/main.js b/web/main.js index fa64dd5..739666c 100644 --- a/web/main.js +++ b/web/main.js @@ -10,6 +10,7 @@ const gutter = document.getElementById("gutter"); const output = document.getElementById("output"); const runBtn = document.getElementById("run-btn"); const clearBtn = document.getElementById("clear-btn"); +const copyBtn = document.getElementById("copy-btn"); const statusEl = document.getElementById("emote-status"); const acEl = document.createElement("div"); @@ -743,9 +744,21 @@ function appendOutput(text, cls) { function updateClearBtn() { clearBtn.disabled = !output.textContent.trim(); + if (copyBtn) copyBtn.disabled = !output.textContent.trim(); } runBtn.addEventListener("click", runCode); clearBtn.addEventListener("click", () => { output.textContent = ""; updateClearBtn(); }); +if (copyBtn) copyBtn.addEventListener("click", async () => { + try { + await navigator.clipboard.writeText(output.textContent); + const prev = copyBtn.textContent; + copyBtn.textContent = "copied"; + setTimeout(() => (copyBtn.textContent = prev), 1200); + } catch { + copyBtn.textContent = "failed"; + setTimeout(() => (copyBtn.textContent = "copy"), 1200); + } +}); // ------------------------------------------------------------ examples diff --git a/web/style.css b/web/style.css index 0b9a759..3a4bf05 100644 --- a/web/style.css +++ b/web/style.css @@ -99,6 +99,13 @@ main { color: var(--muted); border-bottom: 1px solid var(--panel-edge); } +.pane-title button { + text-transform: none; + letter-spacing: 0; + padding: 0.25rem 0.6rem; + font-size: 0.72rem; + font-weight: 500; +} .pane-title .examples { display: flex; align-items: center; From e811ffb4cadaf3a9fc7d509f2e384e3c8ba35594 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 14 Oct 2026 00:18:37 +0530 Subject: [PATCH 14/17] feat(web): shareable links for editor snippets --- web/index.html | 1 + web/main.js | 37 +++++++++++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/web/index.html b/web/index.html index 659cbcc..d3c0766 100644 --- a/web/index.html +++ b/web/index.html @@ -35,6 +35,7 @@

peepoScript playground

+
diff --git a/web/main.js b/web/main.js index 739666c..0e16753 100644 --- a/web/main.js +++ b/web/main.js @@ -11,6 +11,7 @@ const output = document.getElementById("output"); const runBtn = document.getElementById("run-btn"); const clearBtn = document.getElementById("clear-btn"); const copyBtn = document.getElementById("copy-btn"); +const shareBtn = document.getElementById("share-btn"); const statusEl = document.getElementById("emote-status"); const acEl = document.createElement("div"); @@ -804,5 +805,37 @@ updateClearBtn(); examplesSelect?.addEventListener("change", (e) => loadExample(e.target.value)); -// default -loadExample("countdown"); +function encodeSource(src) { return btoa(unescape(encodeURIComponent(src))); } +function decodeSource(s) { return decodeURIComponent(escape(atob(s))); } + +// share link handling +if (shareBtn) shareBtn.addEventListener("click", async () => { + const code = editorSource(editor); + const url = `${location.origin}${location.pathname}#code=${encodeSource(code)}`; + history.replaceState(null, "", `#code=${encodeSource(code)}`); + try { + await navigator.clipboard.writeText(url); + const prev = shareBtn.textContent; + shareBtn.textContent = "link copied"; + setTimeout(() => (shareBtn.textContent = prev), 1400); + } catch { + const prev = shareBtn.textContent; + shareBtn.textContent = "copied url"; + setTimeout(() => (shareBtn.textContent = prev), 1400); + } +}); + +// default / deep-link load +const hash = location.hash || ""; +const match = hash.match(/^#code=(.+)$/); +if (match) { + try { + setEditorSource(decodeSource(match[1])); + output.textContent = ""; + updateClearBtn(); + } catch { + loadExample("countdown"); + } +} else { + loadExample("countdown"); +} From d5e8d29499abf6f699da15ddec1d4c0751e309a8 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 14 Oct 2026 00:18:48 +0530 Subject: [PATCH 15/17] a11y(web): describe disabled run button via tooltip --- web/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/index.html b/web/index.html index d3c0766..f72a8e6 100644 --- a/web/index.html +++ b/web/index.html @@ -34,7 +34,7 @@

peepoScript playground

aria-multiline="true" aria-label="peepoScript source">
- +
From 301de6ff38bdfff3257a6390dea634782af9b3db Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 14 Oct 2026 00:18:59 +0530 Subject: [PATCH 16/17] ux(web): allow reloading the same example from the dropdown --- web/index.html | 1 + web/main.js | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/web/index.html b/web/index.html index f72a8e6..95d3363 100644 --- a/web/index.html +++ b/web/index.html @@ -22,6 +22,7 @@

peepoScript playground