From 1a14cdcae3111274ee2e6a03d1742eb929ec1d12 Mon Sep 17 00:00:00 2001 From: JDoc Date: Wed, 7 Oct 2026 10:09:48 -0400 Subject: [PATCH 1/5] chore(release): bump to 1.2.0 --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index a59c009..149ea85 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "presenter", - "version": "1.1.0", + "version": "1.2.0", "description": "Pointer and fading highlights for live demos, plus toggleable UX enhancements for before/after clips.", "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzSA8AU/9KdGceK/m1e9ke5Gi7B4HObymgejDydm3gAlJXrqcAys1TV5HsphZF/XG4ZNew/Ui2EzRGpJ3ryN9vP90EklOAgHIT4QLSL8zwTarwubELnoLHhqDFtxX/xe8fBb7zQsbXcZbcPHHlj0Fm2qkZgWHjrR1M+6/YzrxkFWOl0tgPibihEarn33hCCBvej/8AXDjF0JEV+farFxSc1f7jVNAgSe7O976GKlrXY6mYurJnXGv6G4EhPG1VGrsrVY/HcKHR3dotAcgp4qPIMHSNOfScxc75UWUKJarqCXKlX5NL9H3Vb84MmMwpOvo01SpDJZc/ZQRrxPOtMCRzQIDAQAB", "permissions": [ From 1dd396177e7d75c78e7bec54db480a7238c35691 Mon Sep 17 00:00:00 2001 From: JDoc Date: Wed, 7 Oct 2026 10:11:33 -0400 Subject: [PATCH 2/5] fix(enhancements): relabel rewrites text nodes only, keeping icons Replacing an element's children dropped its icon and swapped out nodes a framework-rendered page keeps references to. Rewrite the element's own text nodes in place instead, and skip elements whose text is nested deeper. Refs #5 Co-Authored-By: Claude Opus 5.5 --- src/lib/relabel.js | 28 +++++++++++++++++++++------- test/relabel.test.js | 22 ++++++++++++++++------ 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/lib/relabel.js b/src/lib/relabel.js index 8bd42ad..b172e88 100644 --- a/src/lib/relabel.js +++ b/src/lib/relabel.js @@ -2,9 +2,11 @@ * A reversible text relabel — the building block for "say it differently" enhancements. * * Elements matched by `selector` whose trimmed text equals `from` get `to` as their text and - * `className` added. The original child nodes are kept (icons included) and put back by - * `revert`, so the restore is exact. An element the page re-rendered in the meantime is a new - * node that never carried our edit, so there is nothing to restore on it. + * `className` added. Only the element's own text nodes are rewritten — child elements such as + * an icon stay in place, and so do the text nodes themselves, which matters on a page + * rendered by a framework that keeps references to them. `revert` puts each node's original + * text back, so the restore is exact. An element the page re-rendered in the meantime is a + * new node that never carried our edit, so there is nothing to restore on it. * * `apply` skips elements already showing the new label, so it is idempotent. */ @@ -28,19 +30,29 @@ export function createRelabel({ selector, from, to, className }) { for (const element of doc.querySelectorAll(selector)) { // Already relabelled and still showing our text: nothing to do. If the page wrote over // our text in place (a framework re-render), fall through and relabel the new content. - if (element.hasAttribute(RELABELLED) && element.textContent === to) { + if (element.hasAttribute(RELABELLED) && element.textContent.trim() === to) { continue; } if (element.textContent.trim().toLowerCase() !== target) { continue; } - originals.set(element, [...element.childNodes]); + const textNodes = [...element.childNodes].filter( + (node) => node.nodeType === node.TEXT_NODE && node.data.trim() !== "", + ); + if (textNodes.length === 0) { + // The text sits deeper than this element; relabelling it would mean replacing child + // elements, which would not be reversible against a live framework. + continue; + } + originals.set(element, textNodes.map((node) => [node, node.data])); element.setAttribute(RELABELLED, ""); if (className && !element.classList.contains(className)) { element.classList.add(className); element.setAttribute(ADDED_CLASS, className); } - element.textContent = to; + textNodes.forEach((node, index) => { + node.data = index === 0 ? node.data.replace(node.data.trim(), to) : ""; + }); } }; @@ -49,7 +61,9 @@ export function createRelabel({ selector, from, to, className }) { if (!originals.has(element)) { continue; } - element.replaceChildren(...originals.get(element)); + for (const [node, data] of originals.get(element)) { + node.data = data; + } originals.delete(element); element.removeAttribute(RELABELLED); const added = element.getAttribute(ADDED_CLASS); diff --git a/test/relabel.test.js b/test/relabel.test.js index 77f8c9d..c4f2386 100644 --- a/test/relabel.test.js +++ b/test/relabel.test.js @@ -15,19 +15,29 @@ describe("createRelabel", () => { it("relabels only matching elements with matching text", () => { relabel.apply(document); const [ended, running] = document.querySelectorAll(".status"); - expect(ended.textContent).toBe("Stopped"); + expect(ended.textContent.trim()).toBe("Stopped"); expect(ended.classList.contains("is-ended")).toBe(true); + expect(ended.querySelector(".icon")).not.toBeNull(); expect(running.textContent).toBe("running"); expect(document.querySelector(".other").textContent).toBe("completed"); }); - it("restores the page exactly, child nodes included", () => { + it("restores the page exactly, keeping the same nodes", () => { const original = document.body.innerHTML; - const icon = document.querySelector(".icon"); + const ended = document.querySelector(".status"); + const nodes = [...ended.childNodes]; relabel.apply(document); + expect([...ended.childNodes]).toEqual(nodes); relabel.revert(document); expect(document.body.innerHTML).toBe(original); - expect(document.querySelector(".icon")).toBe(icon); + expect([...ended.childNodes]).toEqual(nodes); + }); + + it("leaves an element alone when its text is nested deeper", () => { + document.body.innerHTML = `completed`; + const original = document.body.innerHTML; + relabel.apply(document); + expect(document.body.innerHTML).toBe(original); }); it("is idempotent", () => { @@ -48,8 +58,8 @@ describe("createRelabel", () => { it("relabels again when the page writes over our text in place", () => { relabel.apply(document); const ended = document.querySelector(".status"); - ended.textContent = "completed"; + ended.lastChild.data = "Completed"; relabel.apply(document); - expect(ended.textContent).toBe("Stopped"); + expect(ended.textContent.trim()).toBe("Stopped"); }); }); From e57cef21f52ef75ad42f19cc607b876a3d6537fe Mon Sep 17 00:00:00 2001 From: JDoc Date: Wed, 7 Oct 2026 10:11:33 -0400 Subject: [PATCH 3/5] feat(enhancements): ended Forge sandboxes read as grey Terminated Relabels the status pill from Completed to Terminated and greys it with Forge's own neutral pill variables, replacing the checkmark with a neutral ring. Targets data- attributes only; Forge's classes are hashed. Tested against a hand-written synthetic fixture; numbered zero UUIDs are now accepted as placeholders. Refs #5 Co-Authored-By: Claude Opus 5.5 --- CONTRIBUTING.md | 2 +- enhancements/forge-sandbox-status.js | 51 ++++++++++++++++++++++++ enhancements/index.js | 4 +- test/fixtures-hygiene.test.js | 3 +- test/fixtures/forge-sandbox-list.html | 22 +++++++++++ test/forge-sandbox-status.test.js | 57 +++++++++++++++++++++++++++ 6 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 enhancements/forge-sandbox-status.js create mode 100644 test/fixtures/forge-sandbox-list.html create mode 100644 test/forge-sandbox-status.test.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 16d0e03..9defa4d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ Ordinary git and GitHub practice — opening an issue, naming a branch, writing **Never commit or post:** - **Credentials of any kind** — API keys, tokens, cookies, session IDs, `.env` files, private keys. The `key` in `manifest.json` is a *public* key and is the one exception ([ADR-0002](docs/adr/0002-pin-the-extension-id.md)); its private half was never kept, and must never be generated and committed. -- **Saved copies of real pages.** A page saved from a logged-in site carries names, emails, org and project slugs, resource IDs and internal URLs in its markup. Keep real saves in the gitignored `test/fixtures/private/`, and write committed fixtures in `test/fixtures/` **by hand**: the smallest synthetic markup that reproduces the structure, with placeholder text (`acme`, `someone@example.com`, `00000000-0000-0000-0000-000000000000`). `test/fixtures-hygiene.test.js` fails on a real-looking email or UUID, but it is a backstop, not a licence — it cannot recognise a name or an org slug. +- **Saved copies of real pages.** A page saved from a logged-in site carries names, emails, org and project slugs, resource IDs and internal URLs in its markup. Keep real saves in the gitignored `test/fixtures/private/`, and write committed fixtures in `test/fixtures/` **by hand**: the smallest synthetic markup that reproduces the structure, with placeholder text (`acme`, `someone@example.com`, `00000000-0000-0000-0000-000000000000`, counting up in the last two digits when a fixture needs several). `test/fixtures-hygiene.test.js` fails on a real-looking email or UUID, but it is a backstop, not a licence — it cannot recognise a name or an org slug. - **Personal data** — names, emails, avatars or IDs of anyone but the author, and anything identifying a customer or account. - **Internal information** — internal Slack channels and threads, unreleased features or roadmap, internal URLs or hostnames, rollout plans, who said what, and screenshots of internal tools. - **Clips and screenshots.** Loom recordings and images of enhanced pages stay out of the repo; link to them from wherever they are shared internally, never from here. diff --git a/enhancements/forge-sandbox-status.js b/enhancements/forge-sandbox-status.js new file mode 100644 index 0000000..d054451 --- /dev/null +++ b/enhancements/forge-sandbox-status.js @@ -0,0 +1,51 @@ +/** + * Ended sandboxes in the Forge list read as a grey "Terminated" instead of a green "Completed". + * + * Targets data- attributes only: Forge's class names are build-hashed and change between + * releases. The grey reuses the pill's own colour variables with Forge's neutral values, so + * the proposed state looks native; fallbacks cover a page where those variables are missing. + */ + +import { createRelabel } from "../src/lib/relabel.js"; + +const CLASS = "presenter-sandbox-ended"; + +const relabel = createRelabel({ + selector: 'td[data-key$=":status"] [data-component="Pill"]', + from: "Completed", + to: "Terminated", + className: CLASS, +}); + +export const forgeSandboxStatus = { + id: "forge-sandbox-status", + title: "Ended sandboxes read as grey Terminated", + problem: + "An ended sandbox shows a green Completed, which reads as healthy or running; it cannot be started again, so Terminated in neutral grey says what it is.", + matches: ["https://forge.coreweave.com/sandboxes/*"], + css: ` +[data-component="Pill"].${CLASS} { + --tag-text-color: var(--secondary-text, #5f6368); + --tag-background-color: light-dark(var(--moon-200, #e8eaed), var(--moon-800, #3c4043)); +} +[data-component="Pill"].${CLASS} [data-icon] { + --icon-color: var(--secondary-text, #5f6368); + display: inline-flex; + align-items: center; + justify-content: center; +} +[data-component="Pill"].${CLASS} [data-icon] svg { + display: none; +} +[data-component="Pill"].${CLASS} [data-icon]::before { + content: ""; + width: 0.625rem; + height: 0.625rem; + border: 1.5px solid currentColor; + border-radius: 50%; + box-sizing: border-box; +} +`, + apply: relabel.apply, + revert: relabel.revert, +}; diff --git a/enhancements/index.js b/enhancements/index.js index 53bd9ef..8195d49 100644 --- a/enhancements/index.js +++ b/enhancements/index.js @@ -15,4 +15,6 @@ * Add the new file's export to the list below. */ -export const ENHANCEMENTS = []; +import { forgeSandboxStatus } from "./forge-sandbox-status.js"; + +export const ENHANCEMENTS = [forgeSandboxStatus]; diff --git a/test/fixtures-hygiene.test.js b/test/fixtures-hygiene.test.js index 1c14240..6c7af9c 100644 --- a/test/fixtures-hygiene.test.js +++ b/test/fixtures-hygiene.test.js @@ -9,7 +9,7 @@ const fixtures = join(import.meta.dirname, "fixtures"); const EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/; const UUID = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i; // Synthetic placeholders that are allowed through. -const ALLOWED = [/@example\.(com|org)\b/, /\b0{8}-0{4}-0{4}-0{4}-0{12}\b/]; +const ALLOWED = [/@example\.(com|org)\b/, /\b0{8}-0{4}-0{4}-0{4}-0{10}\d{2}\b/]; function committedFixtures() { if (!existsSync(fixtures)) { @@ -36,5 +36,6 @@ describe("committed fixtures", () => { expect(scrub("contact jane@acme.test")).toMatch(EMAIL); expect(scrub("sandbox 12345678-abcd-4ef0-9abc-1234567890ab")).toMatch(UUID); expect(scrub("someone@example.com 00000000-0000-0000-0000-000000000000")).not.toMatch(EMAIL); + expect(scrub("00000000-0000-0000-0000-000000000000 00000000-0000-0000-0000-000000000042")).not.toMatch(UUID); }); }); diff --git a/test/fixtures/forge-sandbox-list.html b/test/fixtures/forge-sandbox-list.html new file mode 100644 index 0000000..de39031 --- /dev/null +++ b/test/fixtures/forge-sandbox-list.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + +
00000000-0000-0000-0000-000000000000Completed
00000000-0000-0000-0000-000000000001Running
Completed
diff --git a/test/forge-sandbox-status.test.js b/test/forge-sandbox-status.test.js new file mode 100644 index 0000000..a6e92e6 --- /dev/null +++ b/test/forge-sandbox-status.test.js @@ -0,0 +1,57 @@ +// @vitest-environment jsdom +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { beforeEach, describe, expect, it } from "vitest"; +import { createEnhancer } from "../src/content/enhancer.js"; +import { forgeSandboxStatus } from "../enhancements/forge-sandbox-status.js"; +import { urlMatches } from "../src/lib/match.js"; + +const fixture = readFileSync(join(import.meta.dirname, "fixtures", "forge-sandbox-list.html"), "utf8"); +const statusPills = () => [...document.querySelectorAll('td[data-key$=":status"] [data-component="Pill"]')]; + +describe("forge-sandbox-status", () => { + let original; + + beforeEach(() => { + document.head.innerHTML = ""; + document.body.innerHTML = fixture; + original = document.documentElement.outerHTML; + }); + + it("targets the sandboxes pages only", () => { + expect(urlMatches("https://forge.coreweave.com/sandboxes/example-sandboxes", forgeSandboxStatus.matches)).toBe(true); + expect(urlMatches("https://forge.coreweave.com/notebooks", forgeSandboxStatus.matches)).toBe(false); + }); + + it("relabels a Completed status pill and keeps its icon", () => { + const [ended] = statusPills(); + const icon = ended.querySelector("[data-icon]"); + forgeSandboxStatus.apply(document); + expect(ended.textContent.trim()).toBe("Terminated"); + expect(ended.classList.contains("presenter-sandbox-ended")).toBe(true); + expect(ended.querySelector("[data-icon]")).toBe(icon); + }); + + it("leaves other statuses and Completed pills outside the status column alone", () => { + forgeSandboxStatus.apply(document); + const [, running] = statusPills(); + expect(running.textContent.trim()).toBe("Running"); + expect(document.querySelector('[data-key$=":displayName"] [data-component="Pill"]').textContent).toBe("Completed"); + }); + + it("restores the shipped page exactly when switched off", () => { + const enhancer = createEnhancer(document); + enhancer.sync([forgeSandboxStatus]); + expect(document.documentElement.outerHTML).not.toBe(original); + enhancer.sync([]); + expect(document.documentElement.outerHTML).toBe(original); + }); + + it("is idempotent across repeated syncs", () => { + const enhancer = createEnhancer(document); + enhancer.sync([forgeSandboxStatus]); + const once = document.documentElement.outerHTML; + enhancer.sync([forgeSandboxStatus]); + expect(document.documentElement.outerHTML).toBe(once); + }); +}); From d84bdd13cda618cc89c00b43fa926f61aa63192f Mon Sep 17 00:00:00 2001 From: JDoc Date: Wed, 7 Oct 2026 10:19:27 -0400 Subject: [PATCH 4/5] fix(enhancements): grey actually applies, with a crossed-ring icon; centre the badge Forge raises its pill colour rules' specificity by repeating the class, so the plain override never won and the pill stayed green. The colour overrides are now !important (they exist only while the enhancement is on). The checkmark becomes a grey ring with a diagonal bar, drawn as a mask at the original icon's size. The Enhanced badge moves to the top centre. Refs #5 Co-Authored-By: Claude Opus 5.5 --- CONTEXT.md | 2 +- README.md | 2 +- enhancements/forge-sandbox-status.js | 35 ++++++++++++++++++---------- src/content/badge.js | 4 ++-- src/content/presenting.css | 5 ++-- 5 files changed, 30 insertions(+), 18 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index d591c20..521dcb3 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -29,7 +29,7 @@ The two views of a page: **as shipped** is the real product with every enhanceme _Avoid_: before/after as nouns for the views (they describe the clip, not the page), original, modified, live. **Enhanced badge**: -The small "Enhanced" tag in the top-right corner, present exactly while the **proposed** view differs from **as shipped** — so a viewer of a clip always knows which one they are seeing. +The small "Enhanced" tag at the top centre of the page, present exactly while the **proposed** view differs from **as shipped** — so a viewer of a clip always knows which one they are seeing. _Avoid_: watermark, label, indicator. ## Language — both diff --git a/README.md b/README.md index 56e870a..d70ac65 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ On by default. For a readable shared screen, use Chrome's own zoom (`Cmd` `+`), 1. Start recording (Loom, or any screen recorder). 1. Press **Alt+Shift+E** to flip between the shipped page and the proposed one. Change the shortcut at `chrome://extensions/shortcuts`; the popup shows the current one. -While any enhancement is showing, an **Enhanced** badge sits in the top-right corner, so whoever watches the clip knows which version they are seeing. With every enhancement off — or the page flipped to *as shipped* — there is no badge and the page is exactly what shipped. +While any enhancement is showing, an **Enhanced** badge sits at the top centre of the page, so whoever watches the clip knows which version they are seeing. With every enhancement off — or the page flipped to *as shipped* — there is no badge and the page is exactly what shipped. New enhancements are added as files in [`enhancements/`](enhancements/index.js) — see [`CONTRIBUTING.md`](CONTRIBUTING.md) and [ADR-0001](docs/adr/0001-enhancements-are-versioned-files.md). diff --git a/enhancements/forge-sandbox-status.js b/enhancements/forge-sandbox-status.js index d054451..c5e9a1c 100644 --- a/enhancements/forge-sandbox-status.js +++ b/enhancements/forge-sandbox-status.js @@ -4,12 +4,25 @@ * Targets data- attributes only: Forge's class names are build-hashed and change between * releases. The grey reuses the pill's own colour variables with Forge's neutral values, so * the proposed state looks native; fallbacks cover a page where those variables are missing. + * + * The overrides are !important because Forge raises its colour rules' specificity by + * repeating the class (`._green_x._green_x._green_x`), which outranks any plain selector here. + * They only exist while the enhancement is on, so the shipped page is untouched when off. */ import { createRelabel } from "../src/lib/relabel.js"; const CLASS = "presenter-sandbox-ended"; +// A ring with a diagonal bar, like the European no-stopping sign. Drawn as a mask so it takes +// the pill's text colour, at the same 24-unit grid and size as the checkmark it replaces. +const ENDED_ICON = encodeURIComponent( + '' + + '' + + '' + + "", +); + const relabel = createRelabel({ selector: 'td[data-key$=":status"] [data-component="Pill"]', from: "Completed", @@ -25,25 +38,23 @@ export const forgeSandboxStatus = { matches: ["https://forge.coreweave.com/sandboxes/*"], css: ` [data-component="Pill"].${CLASS} { - --tag-text-color: var(--secondary-text, #5f6368); - --tag-background-color: light-dark(var(--moon-200, #e8eaed), var(--moon-800, #3c4043)); + --tag-text-color: var(--secondary-text, #5f6368) !important; + --tag-background-color: light-dark(var(--moon-200, #e8eaed), var(--moon-800, #3c4043)) !important; } [data-component="Pill"].${CLASS} [data-icon] { - --icon-color: var(--secondary-text, #5f6368); - display: inline-flex; - align-items: center; - justify-content: center; + --icon-color: var(--secondary-text, #5f6368) !important; + position: relative; } [data-component="Pill"].${CLASS} [data-icon] svg { - display: none; + visibility: hidden; } [data-component="Pill"].${CLASS} [data-icon]::before { content: ""; - width: 0.625rem; - height: 0.625rem; - border: 1.5px solid currentColor; - border-radius: 50%; - box-sizing: border-box; + position: absolute; + inset: 0; + background-color: var(--icon-color); + -webkit-mask: url("data:image/svg+xml,${ENDED_ICON}") center / contain no-repeat; + mask: url("data:image/svg+xml,${ENDED_ICON}") center / contain no-repeat; } `, apply: relabel.apply, diff --git a/src/content/badge.js b/src/content/badge.js index 850ae2f..23de203 100644 --- a/src/content/badge.js +++ b/src/content/badge.js @@ -1,6 +1,6 @@ /** - * The Enhanced badge: a small top-right tag shown only while the page differs from what - * shipped. Top-right because Loom's camera bubble sits bottom-left by default. + * The Enhanced badge: a small top-centre tag shown only while the page differs from what + * shipped. At the top because Loom's camera bubble sits bottom-left by default. */ export const BADGE_ID = "presenter-enhanced-badge"; diff --git a/src/content/presenting.css b/src/content/presenting.css index 7f8bc9e..7b79fc9 100644 --- a/src/content/presenting.css +++ b/src/content/presenting.css @@ -40,11 +40,12 @@ } -/* The Enhanced badge — top-right, clear of Loom's default bottom-left camera bubble */ +/* The Enhanced badge — top centre, clear of Loom's default bottom-left camera bubble */ #presenter-enhanced-badge { position: fixed; top: 12px; - right: 12px; + left: 50%; + transform: translateX(-50%); z-index: 2147483647; padding: 4px 10px; border-radius: 999px; From 44d717d631fee72610e629d5cfc126545988a5e0 Mon Sep 17 00:00:00 2001 From: JDoc Date: Wed, 7 Oct 2026 10:25:02 -0400 Subject: [PATCH 5/5] fix(enhancements): use a centred dash in the ended-sandbox ring A ring with a horizontal dash (as on the European no-entry sign) reads as ended rather than as an error, which the diagonal bar suggested. Refs #5 Co-Authored-By: Claude Opus 5.5 --- enhancements/forge-sandbox-status.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/enhancements/forge-sandbox-status.js b/enhancements/forge-sandbox-status.js index c5e9a1c..52a6c59 100644 --- a/enhancements/forge-sandbox-status.js +++ b/enhancements/forge-sandbox-status.js @@ -14,12 +14,13 @@ import { createRelabel } from "../src/lib/relabel.js"; const CLASS = "presenter-sandbox-ended"; -// A ring with a diagonal bar, like the European no-stopping sign. Drawn as a mask so it takes +// A ring with a short horizontal dash, like the European no-entry sign — calmer than a +// diagonal bar, and read as "ended" rather than "error". Drawn as a mask so it takes // the pill's text colour, at the same 24-unit grid and size as the checkmark it replaces. const ENDED_ICON = encodeURIComponent( '' + '' + - '' + + '' + "", );