From f41985e407890a57c4171d43bc272abf7dd1e22f Mon Sep 17 00:00:00 2001 From: clover caruso Date: Wed, 14 Oct 2026 02:08:36 -0700 Subject: [PATCH] feat(lib/progress): attachToDocument and formatHtml Beautiful Windows7 progress bars in the browser! Assisted-by: claude-opus-5.5 --- lib/Events.test.ts | 19 ++ lib/Events.ts | 9 +- lib/progress.test.ts | 127 +++++++++++ lib/progress.ts | 492 +++++++++++++++++++++++++++++++++++++++--- lib/readme.changes.md | 5 + lib/readme.md | 2 +- 6 files changed, 620 insertions(+), 34 deletions(-) create mode 100644 lib/Events.test.ts diff --git a/lib/Events.test.ts b/lib/Events.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..a4a3bc77214a2d2cc7da66867bab38ddfd386981 --- /dev/null +++ b/lib/Events.test.ts @@ -0,0 +1,19 @@ +test("disposing a listener leaves the others", () => { + const events = new Events<{ ping: [] }>(); + const calls: string[] = []; + const offA = events.on("ping", () => calls.push("a")); + events.on("ping", () => calls.push("b")); + const offAny = events.onAny(() => calls.push("any a")); + events.onAny(() => calls.push("any b")); + + offA(); + offA(); + offAny(); + offAny(); + events.emit("ping"); + assert.deepEqual(calls, ["b", "any b"]); +}); + +import assert from "node:assert/strict"; +import { test } from "vitest"; +import { Events } from "./Events.ts"; diff --git a/lib/Events.ts b/lib/Events.ts index c2bd2b0c72e596eb80f7470805f00de4e4668862..410c43322d72c2812d9fd0625917aad178218770 100644 --- a/lib/Events.ts +++ b/lib/Events.ts @@ -31,7 +31,7 @@ export class Events { on(channel: C, cb: Events.Handler): ts.Dispose { const list = this.#channels[channel] ??= []; list.push(cb); - return ts.defer(() => void (list.splice(list.indexOf(cb)), 1)); + return ts.defer(() => remove(list, cb)); } /** * listen for one event on the specified channel @@ -77,7 +77,7 @@ export class Events { /** listen for any event */ onAny(cb: (channel: keyof Map, args: unknown[]) => void): ts.Dispose { this.#global.push(cb); - return ts.defer(() => void (this.#global.splice(this.#global.indexOf(cb)), 1)); + return ts.defer(() => remove(this.#global, cb)); } /** emit an event on the specified channel */ emit(channel: C, ...args: Map[C]): void { @@ -95,6 +95,11 @@ export class Events { } } +function remove(list: T[], item: T) { + const i = list.indexOf(item); + if (i !== -1) list.splice(i, 1); +} + export namespace Events { export type Map = { [key: string]: unknown[] }; export type Handler = (...args: Args) => void; diff --git a/lib/progress.test.ts b/lib/progress.test.ts index 280a11dcfe9096780e332c49b582863c36e4fdd8..f14bc30fe55f11d05f1472edeae36522e1e26ba2 100644 --- a/lib/progress.test.ts +++ b/lib/progress.test.ts @@ -309,6 +309,132 @@ test("Ema tracks warmup samples without resetting the first sample", () => { assert.ok(Math.abs(completion - 26_737.70491803279) < 1e-9); }); +// ## html +test("formatHtml draws visible nodes into a declarative shadow root", () => { + const root = new progress.Root(); + const build = root.start("build "); + build.start("secret", { hidden: true }); + root.start("empty group", { passive: true }); + + const html = progress.formatHtml(root.active); + assert.match(html, /^