From dc6c994038631cb4395c2e008e65019fac25bbb5 Mon Sep 17 00:00:00 2001 From: Brian Kim Date: Fri, 9 Oct 2026 11:25:13 -0400 Subject: [PATCH 01/24] Type the cell grid context and round out its text attributes getContext("termdom-cellgrid") is typed to return a CanvasCellGridContext, in TermDOM's own canvas and, through a global declaration, in lib.dom's, so no caller casts. The context's canvas is an HTMLCanvasElement, and its settings read back as their own values. columns and rows are gone: canvas.width and canvas.height are the grid's size, as a 2d context reads its canvas's. textDecoration takes line-through as well as underline, alone or together, and getCell reports dim, strikethrough and inverse. Co-Authored-By: Claude Opus 5.5 --- docs/guides/07-images-and-canvas.md | 11 ++--- examples/canvas.ts | 2 +- src/index.ts | 10 +++++ src/internal/canvas.ts | 54 ++++++++++++++-------- src/internal/dom.ts | 8 +++- src/internal/framebuffer.ts | 4 ++ tests/canvas.test.ts | 69 ++++++++++++++++++++--------- 7 files changed, 110 insertions(+), 48 deletions(-) diff --git a/docs/guides/07-images-and-canvas.md b/docs/guides/07-images-and-canvas.md index 83da6b29d..70a39d84b 100644 --- a/docs/guides/07-images-and-canvas.md +++ b/docs/guides/07-images-and-canvas.md @@ -287,13 +287,14 @@ The cell grid context draws cells instead of pixels. The canvas is TermDOM's own, so its name carries TermDOM's prefix, as a browser's own context types once carried theirs (`"moz-webgl"`, `"webkit-3d"`). Its interface is not on `window`. The `@b9g/termdom` package exports it as -`CanvasCellGridContext`. +`CanvasCellGridContext`, and importing the package teaches TypeScript +that `getContext("termdom-cellgrid")` returns one. ```ts const canvas = document.createElement("canvas"); canvas.width = 20; canvas.height = 3; -const grid = canvas.getContext("termdom-cellgrid"); +const grid = canvas.getContext("termdom-cellgrid")!; grid.fillStyle = "#003366"; grid.fillRect(0, 0, 20, 3); grid.fillStyle = "#ffcc00"; @@ -317,16 +318,16 @@ not the page's borders around the canvas. | `lineJoin` | `miter`, the default, or `round`, which curves `strokeRect()`'s corners | | `font` | Keywords from the CSS `font` shorthand: `bold`, `italic`, `lighter` | | `textAlign` | `left`, `right`, `center`, `start` or `end` | -| `textDecoration` | `none` or `underline` | +| `textDecoration` | `none`, or `underline` and `line-through`, alone or together | | `fillText(text, x, y, maxWidth?)` | Writes glyphs in the fill color, over the cells' backgrounds. A wide glyph takes two columns | | `fillRect(x, y, w, h)` | Fills cells with the fill color as their background. `currentcolor` fills them in inverse video | | `clearRect(x, y, w, h)` | Empties cells | | `strokeLine(x1, y1, x2, y2)` | A line from one cell to another, both included, along a row or a column. A slanted line draws nothing | | `strokeRect(x, y, w, h)` | A box around the cells `w` by `h` from (x, y) | | `drawImage(image, ...)` | Draws an image's pixels, two to a cell. Without a size, the image takes its natural size in cells | -| `getCell(x, y)` | The cell's glyph, colors and attributes, or `null` | +| `getCell(x, y)` | The cell's `char`, `color` and `background`, and whether it is `bold`, `italic`, `dim`, `underline`, `strikethrough` or `inverse`; `null` for an empty cell | | `measureText(text)` | The text's width in columns | -| `columns`, `rows`, `reset()` | The grid's size, and clearing it | +| `reset()` | Clears the grid and the settings | ## Text alone diff --git a/examples/canvas.ts b/examples/canvas.ts index 3c15667a8..eb0eb3f3a 100644 --- a/examples/canvas.ts +++ b/examples/canvas.ts @@ -37,7 +37,7 @@ pixels.style.height = "16px"; const ctx = pixels.getContext("2d")!; const grid = (document.getElementById("cells") as HTMLCanvasElement) - .getContext("termdom-cellgrid" as "2d") as any; + .getContext("termdom-cellgrid")!; // Conway's Life on the grid, seeded with a glider, blinkers, a toad and // an R-pentomino, which keeps changing for a long time. diff --git a/src/index.ts b/src/index.ts index 84c0ee5cb..587c3b607 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,6 @@ import "./internal/inspector.ts"; +import type {CanvasCellGridContext} from "./internal/canvas.ts"; import { Cascade, type CellSize, @@ -79,6 +80,15 @@ export type { TerminalTransport, } from "./internal/exchange.ts"; +declare global { + interface HTMLCanvasElement { + getContext( + contextId: "termdom-cellgrid", + options?: unknown, + ): CanvasCellGridContext | null; + } +} + // Images decode on the runtime's web Worker, running src/decode-worker.ts, // which builds to dist/decode-worker.js beside this file. A runtime with // no Worker, as Node is without --experimental-web-worker, and a CommonJS diff --git a/src/internal/canvas.ts b/src/internal/canvas.ts index ddf15fa3b..82158a267 100644 --- a/src/internal/canvas.ts +++ b/src/internal/canvas.ts @@ -2970,7 +2970,7 @@ export interface CanvasCellGridContext { [kLineStyle]: LineStyle["style"]; [kLineJoin]: "miter" | "round"; [kFont]: string; - [kTextAlign]: string; + [kTextAlign]: "start" | "end" | "left" | "right" | "center"; [kTextDecoration]: string; } @@ -2995,16 +2995,8 @@ export class CanvasCellGridContext { this[kTextDecoration] = "none"; } - get canvas(): object { - return this[kGridHost].canvas; - } - - get columns(): number { - return this[kGridHost].cells().cols; - } - - get rows(): number { - return this[kGridHost].cells().rows; + get canvas(): HTMLCanvasElement { + return this[kGridHost].canvas as HTMLCanvasElement; } get fillStyle(): string { @@ -3024,7 +3016,7 @@ export class CanvasCellGridContext { } /** A CSS border style: solid, double, dashed, dotted and the rest. */ - get lineStyle(): string { + get lineStyle(): LineStyle["style"] { return this[kLineStyle]; } @@ -3036,7 +3028,7 @@ export class CanvasCellGridContext { } /** miter, round. A round join curves strokeRect()'s corners. */ - get lineJoin(): string { + get lineJoin(): "miter" | "round" { return this[kLineJoin]; } @@ -3055,24 +3047,41 @@ export class CanvasCellGridContext { this[kFont] = String(value); } - get textAlign(): string { + get textAlign(): "start" | "end" | "left" | "right" | "center" { return this[kTextAlign]; } set textAlign(value: string) { - if (["start", "end", "left", "right", "center"].includes(value)) { + if ( + value === "start" || + value === "end" || + value === "left" || + value === "right" || + value === "center" + ) { this[kTextAlign] = value; } } - /** none, underline. */ + /** none, or underline and line-through, alone or together. */ get textDecoration(): string { return this[kTextDecoration]; } set textDecoration(value: string) { - if (value === "none" || value === "underline") { - this[kTextDecoration] = value; + const words = String(value).trim().toLowerCase().split(/\s+/); + if (words.length === 1 && words[0] === "none") { + this[kTextDecoration] = "none"; + return; + } + const lines = new Set(words); + if ( + lines.size === words.length && + words.every((word) => word === "underline" || word === "line-through") + ) { + this[kTextDecoration] = ["underline", "line-through"] + .filter((line) => lines.has(line)) + .join(" "); } } @@ -3137,7 +3146,8 @@ export class CanvasCellGridContext { bold: words.includes("bold") || words.includes("bolder"), italic: words.includes("italic") || words.includes("oblique"), dim: words.includes("lighter"), - underline: this[kTextDecoration] === "underline", + underline: this[kTextDecoration].includes("underline"), + strikethrough: this[kTextDecoration].includes("line-through"), }; const {grid} = cells; for (const cluster of clusters) { @@ -3320,7 +3330,10 @@ export class CanvasCellGridContext { background: string | null; bold: boolean; italic: boolean; + dim: boolean; underline: boolean; + strikethrough: boolean; + inverse: boolean; } | null { const {grid, cols, rows} = this[kGridHost].cells(); const col = Math.floor(x); @@ -3350,7 +3363,10 @@ export class CanvasCellGridContext { background: css(cell.bg), bold: cell.bold, italic: cell.italic, + dim: cell.dim, underline: cell.underline, + strikethrough: cell.strikethrough, + inverse: cell.inverse, }; } diff --git a/src/internal/dom.ts b/src/internal/dom.ts index de6319ba1..0b5e80e19 100644 --- a/src/internal/dom.ts +++ b/src/internal/dom.ts @@ -14486,6 +14486,10 @@ class HTMLCanvasElement extends HTMLElement { contextId: "2d", options?: globalThis.CanvasRenderingContext2DSettings, ): globalThis.CanvasRenderingContext2D | null; + getContext( + contextId: "termdom-cellgrid", + options?: unknown, + ): CanvasCellGridContext | null; getContext( contextId: "bitmaprenderer", options?: globalThis.ImageBitmapRenderingContextSettings, @@ -14502,7 +14506,9 @@ class HTMLCanvasElement extends HTMLElement { contextId: string, options?: any, ): globalThis.RenderingContext | null; - getContext(contextId: string): globalThis.RenderingContext | null { + getContext( + contextId: string, + ): globalThis.RenderingContext | CanvasCellGridContext | null { if (arguments.length < 1) { throw new TypeError("getContext needs a context id"); } diff --git a/src/internal/framebuffer.ts b/src/internal/framebuffer.ts index e88d4f921..136f25a5d 100644 --- a/src/internal/framebuffer.ts +++ b/src/internal/framebuffer.ts @@ -948,6 +948,8 @@ export function readCell( bold: boolean; italic: boolean; underline: boolean; + strikethrough: boolean; + dim: boolean; inverse: boolean; } | null { if (grid.cluster[index] === 0) { @@ -964,6 +966,8 @@ export function readCell( bold: (attrs & ATTR.Bold) !== 0, italic: (attrs & ATTR.Italic) !== 0, underline: (attrs & (ATTR.Underline | ATTR.DoubleUnderline)) !== 0, + strikethrough: (attrs & ATTR.Strikethrough) !== 0, + dim: (attrs & ATTR.Dim) !== 0, inverse: (attrs & ATTR.Inverse) !== 0, }; } diff --git a/tests/canvas.test.ts b/tests/canvas.test.ts index 67a8720bd..3fd0c05a2 100644 --- a/tests/canvas.test.ts +++ b/tests/canvas.test.ts @@ -77,10 +77,10 @@ test("getContext hands back one context per canvas, and null for the rest", asyn expect(ctx).not.toBeNull(); expect(a.getContext("2d")).toBe(ctx); expect(ctx!.canvas).toBe(a); - expect(a.getContext("termdom-cellgrid" as "2d")).toBeNull(); + expect(a.getContext("termdom-cellgrid")).toBeNull(); expect(a.getContext("webgl")).toBeNull(); expect(b.getContext("bitmaprenderer")).toBeNull(); - const grid = b.getContext("termdom-cellgrid" as "2d"); + const grid = b.getContext("termdom-cellgrid"); expect(grid).not.toBeNull(); expect(b.getContext("2d")).toBeNull(); expect(ctx).toBeInstanceOf((dom.window as any).CanvasRenderingContext2D); @@ -600,9 +600,9 @@ test("a canvas stretched by CSS scales its pixels to the box", async () => { // --------------------------------------------------------------------------- // cellgrid -function grid(document: Document, id = "g"): any { +function grid(document: Document, id = "g"): CanvasCellGridContext { return (document.getElementById(id) as HTMLCanvasElement) - .getContext("termdom-cellgrid" as "2d"); + .getContext("termdom-cellgrid")!; } test("a cellgrid canvas is width columns by height rows of cells", async () => { @@ -612,7 +612,7 @@ test("a cellgrid canvas is width columns by height rows of cells", async () => { await nextFrame(dom); const rect = document.getElementById("g")!.getBoundingClientRect(); expect([rect.width, rect.height]).toEqual([12, 3]); - expect([ctx.columns, ctx.rows]).toEqual([12, 3]); + expect([ctx.canvas.width, ctx.canvas.height]).toEqual([12, 3]); dom.dispose(); }); @@ -645,7 +645,10 @@ test("fillText writes glyphs, fillRect fills backgrounds, clearRect empties", as background: "#003366", bold: true, italic: true, + dim: false, underline: false, + strikethrough: false, + inverse: false, }); ctx.clearRect(0, 0, 12, 1); expect(ctx.getCell(1, 0)).toBeNull(); @@ -655,6 +658,34 @@ test("fillText writes glyphs, fillRect fills backgrounds, clearRect empties", as dom.dispose(); }); +test("textDecoration draws underline and line-through, and getCell reads them back", async () => { + const {dom, document} = + await mount(""); + const ctx = grid(document); + ctx.textDecoration = "line-through underline"; + expect(ctx.textDecoration).toBe("underline line-through"); + ctx.textDecoration = "overline"; + expect(ctx.textDecoration).toBe("underline line-through"); + ctx.font = "lighter"; + ctx.fillText("ab", 0, 0); + ctx.textDecoration = "line-through"; + ctx.font = ""; + ctx.fillText("c", 2, 0); + expect(ctx.getCell(0, 0)).toMatchObject({ + dim: true, + underline: true, + strikethrough: true, + }); + expect(ctx.getCell(2, 0)).toMatchObject({ + dim: false, + underline: false, + strikethrough: true, + }); + ctx.textDecoration = "none"; + expect(ctx.textDecoration).toBe("none"); + dom.dispose(); +}); + test("text that runs past the grid is cut, and wide glyphs take two columns", async () => { const {dom, terminal, document} = await mount("|"); @@ -662,11 +693,11 @@ test("text that runs past the grid is cut, and wide glyphs take two columns", as ctx.fillText("漢字abcdef", 0, 0); await nextFrame(dom); expect(rowText(terminal, 0)).toBe("漢字ab|"); - expect(ctx.getCell(0, 0).char).toBe("漢"); + expect(ctx.getCell(0, 0)!.char).toBe("漢"); expect(ctx.measureText("漢字a").width).toBe(5); ctx.textAlign = "right"; ctx.fillText("Z", 6, 0); - expect(ctx.getCell(5, 0).char).toBe("Z"); + expect(ctx.getCell(5, 0)!.char).toBe("Z"); dom.dispose(); }); @@ -715,7 +746,7 @@ test("resizing a cellgrid canvas resizes and clears its grid", async () => { const ctx = grid(document); ctx.fillText("abcd", 0, 0); canvas.width = 8; - expect(ctx.columns).toBe(8); + expect(ctx.canvas.width).toBe(8); expect(ctx.getCell(0, 0)).toBeNull(); await nextFrame(dom); expect(canvas.getBoundingClientRect().width).toBe(8); @@ -867,7 +898,7 @@ test("strokeRect draws a box and strokeLine joins it as tees", async () => { expect(rowText(terminal, 1)).toBe("│ │ │"); expect(rowText(terminal, 2)).toBe("├──┴─┤"); expect(rowText(terminal, 3)).toBe("└────┘"); - expect(ctx.getCell(3, 2).char).toBe("┴"); + expect(ctx.getCell(3, 2)!.char).toBe("┴"); dom.dispose(); }); @@ -935,9 +966,9 @@ test("a cellgrid samples only the cells an image lands on, however large", async source.fillRect(1, 0, 1, 2); const ctx = grid(document); ctx.drawImage(document.getElementById("src"), -1e6 + 2, 0, 2e6, 1e6); - expect(ctx.getCell(1, 0).background).toBe("#ff0000"); - expect(ctx.getCell(2, 0).background).toBe("#0000ff"); - expect(ctx.getCell(3, 1).background).toBe("#0000ff"); + expect(ctx.getCell(1, 0)!.background).toBe("#ff0000"); + expect(ctx.getCell(2, 0)!.background).toBe("#0000ff"); + expect(ctx.getCell(3, 1)!.background).toBe("#0000ff"); dom.dispose(); }); @@ -975,7 +1006,7 @@ test("with graphics off, a canvas has no 2d context and keeps its cellgrid", asy const b = dom.document.createElement("canvas"); dom.document.body.append(a, b); expect(a.getContext("2d")).toBeNull(); - expect(b.getContext("termdom-cellgrid" as "2d")).toBeInstanceOf( + expect(b.getContext("termdom-cellgrid")).toBeInstanceOf( CanvasCellGridContext, ); dom.dispose(); @@ -1014,9 +1045,7 @@ test("a cell grid clips to a scroller and paints the rows scrolled into view", a "

below

", ); const canvas = document.getElementById("c") as HTMLCanvasElement; - const grid = canvas.getContext("termdom-cellgrid" as "2d") as unknown as { - fillText(text: string, x: number, y: number): void; - }; + const grid = canvas.getContext("termdom-cellgrid")!; for (let row = 0; row < 6; row++) { grid.fillText(`row ${row}`, 0, row); } @@ -1044,9 +1073,7 @@ test("a cell grid narrower than its columns is cut at its content box", async () "|

", ); const canvas = document.getElementById("c") as HTMLCanvasElement; - const grid = canvas.getContext("termdom-cellgrid" as "2d") as unknown as { - fillText(text: string, x: number, y: number): void; - }; + const grid = canvas.getContext("termdom-cellgrid")!; grid.fillText("abcdefghijklmnop", 0, 0); await nextFrame(dom); expect(rowText(terminal, 0)).toBe("abcde|"); @@ -1107,9 +1134,7 @@ test("a cell grid in a scroller shifts with the terminal's scroll", async () => "" + ""; const canvas = dom.document.getElementById("c") as HTMLCanvasElement; - const grid = canvas.getContext("termdom-cellgrid" as "2d") as unknown as { - fillText(text: string, x: number, y: number): void; - }; + const grid = canvas.getContext("termdom-cellgrid")!; for (let row = 0; row < 12; row++) { grid.fillText(`row ${row}`, 0, row); } From 5315782eec63e99889c5b1a04fc856908ba74130 Mon Sep 17 00:00:00 2001 From: Brian Kim Date: Fri, 9 Oct 2026 11:34:15 -0400 Subject: [PATCH 02/24] Build the image decoder as a worker entry, out of the package's exports libuild 0.2.26 builds a src/.worker.ts to dist and leaves it out of exports, which is what a module loaded by file path from new URL(..., import.meta.url) needs. Co-Authored-By: Claude Opus 5.5 --- bun.lock | 4 ++-- docs/guides/06-api.md | 3 --- docs/guides/07-images-and-canvas.md | 5 +++-- package.json | 3 +-- src/{decode-worker.ts => image-decode.worker.ts} | 0 src/index.ts | 6 +++--- src/internal/images.ts | 2 +- 7 files changed, 10 insertions(+), 13 deletions(-) rename src/{decode-worker.ts => image-decode.worker.ts} (100%) diff --git a/bun.lock b/bun.lock index 0fe767cda..09da77fae 100644 --- a/bun.lock +++ b/bun.lock @@ -14,7 +14,7 @@ "devDependencies": { "@b9g/crank": "^0.7.12", "@b9g/eslint-config": "^0.1.10", - "@b9g/libuild": "^0.2.25", + "@b9g/libuild": "^0.2.26", "@codemirror/commands": "^6.11.1", "@codemirror/lang-javascript": "^6.2.5", "@codemirror/language": "^6.12.4", @@ -61,7 +61,7 @@ "@b9g/eslint-config": ["@b9g/eslint-config@0.1.10", "", { "peerDependencies": { "@eslint/js": ">=9", "@stylistic/eslint-plugin": ">=3", "@typescript-eslint/eslint-plugin": ">=8", "@typescript-eslint/parser": ">=8", "eslint": ">=9", "eslint-plugin-acrocase": ">=0.0.1", "eslint-plugin-crank": ">=0.2.2", "eslint-plugin-esfold": ">=0.1.2" } }, "sha512-5uVBZvX+K4XoGilegJ5/tEH4Mzg5gf+/XdOF6I3QJxTX/5iOoyTuMChIKWHPI3oxBAHzbLHYxFeHoNrp7q2gRQ=="], - "@b9g/libuild": ["@b9g/libuild@0.2.25", "", { "dependencies": { "@b9g/async-context": "^0.2.1", "commander": "^15.0.0", "esbuild": "^0.28.1", "expect": "^30.4.1", "glob": "^13.0.6", "pretty-format": "^30.4.1" }, "peerDependencies": { "playwright": "^1.40.0", "typescript": "^5.0.0 || ^6.0.0" }, "optionalPeers": ["playwright", "typescript"], "bin": { "libuild": "cli.js" } }, "sha512-gASB1QiejGfe5vIGbp2wwIhMvz4T1ppo9RlmtwI8tpVXld8GqXl/7NliU0HiY5IU3ZqaLDTbiick7BXSkKAJgw=="], + "@b9g/libuild": ["@b9g/libuild@0.2.26", "", { "dependencies": { "@b9g/async-context": "^0.2.1", "commander": "^15.0.0", "esbuild": "^0.28.1", "expect": "^30.4.1", "glob": "^13.0.6", "pretty-format": "^30.4.1" }, "peerDependencies": { "playwright": "^1.40.0", "typescript": "^5.0.0 || ^6.0.0" }, "optionalPeers": ["playwright", "typescript"], "bin": { "libuild": "cli.js" } }, "sha512-8eZT6nJ+PL5ILlowL64kBntd+PPrH7DpTJHyWazhL6ik9kQwmQpQFJgVtcu27G4FE0BPqtlSYyJMbLPNVE1KlA=="], "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], diff --git a/docs/guides/06-api.md b/docs/guides/06-api.md index 7dac7b513..082d50001 100644 --- a/docs/guides/06-api.md +++ b/docs/guides/06-api.md @@ -373,6 +373,3 @@ is a whole server. - The types `TermDOMOptions`, `TermDOMEventMap`, `CellSize`, `TerminalTransport`, `TerminalCloseInfo`, `TerminalSize`, `ProcessLike`, `TTYReadStream`, `TTYWriteStream` and `SSHSessionLike`. - -`@b9g/termdom/decode-worker` is the module that decodes images on a -`Worker`. Programs do not import it. diff --git a/docs/guides/07-images-and-canvas.md b/docs/guides/07-images-and-canvas.md index 70a39d84b..fe0e93300 100644 --- a/docs/guides/07-images-and-canvas.md +++ b/docs/guides/07-images-and-canvas.md @@ -209,8 +209,9 @@ one does not stall the page. Bun and Deno have one. Node has one from thread there. Without a `Worker`, and in a CommonJS build, images decode on the page's thread in 16 ms slices, so input and frames go on between slices. A decode that takes more than 10 seconds is stopped -and that image fails; the images behind it still decode. The worker runs -the package's `@b9g/termdom/decode-worker` module. +and that image fails; the images behind it still decode. The worker's +module ships in the package beside its entry, and TermDOM starts it +itself. The worker ends a second after its last decode, and when the last TermDOM is disposed, so it never keeps a finished program running. diff --git a/package.json b/package.json index 5675064dc..38f4d4763 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ ], "exports": { ".": "./src/index.ts", - "./decode-worker": "./src/decode-worker.ts", "./package.json": "./package.json" }, "files": [ @@ -59,7 +58,7 @@ "devDependencies": { "@b9g/crank": "^0.7.12", "@b9g/eslint-config": "^0.1.10", - "@b9g/libuild": "^0.2.25", + "@b9g/libuild": "^0.2.26", "@codemirror/commands": "^6.11.1", "@codemirror/lang-javascript": "^6.2.5", "@codemirror/language": "^6.12.4", diff --git a/src/decode-worker.ts b/src/image-decode.worker.ts similarity index 100% rename from src/decode-worker.ts rename to src/image-decode.worker.ts diff --git a/src/index.ts b/src/index.ts index 587c3b607..0be85e0c6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -89,14 +89,14 @@ declare global { } } -// Images decode on the runtime's web Worker, running src/decode-worker.ts, -// which builds to dist/decode-worker.js beside this file. A runtime with +// Images decode on the runtime's web Worker, running src/image-decode.worker.ts, +// which builds to dist/image-decode.worker.js beside this file. A runtime with // no Worker, as Node is without --experimental-web-worker, and a CommonJS // build, which has no import.meta.url, decode on this thread instead. setDecodeWorkerURL( typeof import.meta.url === "string" ? new URL( - `./decode-worker.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`, + `./image-decode.worker.${import.meta.url.endsWith(".ts") ? "ts" : "js"}`, import.meta.url, ) : null, diff --git a/src/internal/images.ts b/src/internal/images.ts index 9d644e931..ef0bad52d 100644 --- a/src/internal/images.ts +++ b/src/internal/images.ts @@ -2187,7 +2187,7 @@ let current: WorkerState | null = null; let workersWork = true; let nextRequest = 0; -/** The module src/decode-worker.ts builds to, which decodes off-thread. */ +/** The module src/image-decode.worker.ts builds to, which decodes off-thread. */ export function setDecodeWorkerURL(url: URL | null): void { if (current !== null) { endWorker(current); From e6fe843f8354a0b65501de9a3e1ed10363d3eca8 Mon Sep 17 00:00:00 2001 From: Brian Kim Date: Fri, 9 Oct 2026 11:40:11 -0400 Subject: [PATCH 03/24] Style gauges through css-forms-1's pseudo-elements and their prefixed aliases A page styles a progress or meter's track and fill through ::slider-track and ::slider-fill, and a meter's level through :optimal-value, :low-value and :high-value, as CSS Form Control Styling specifies. WebKit's ::-webkit-progress-* and ::-webkit-meter-* and Firefox's ::-moz-progress-bar, ::-moz-meter-bar and :-moz-meter-* pseudo-classes are aliases, as its section 4.10 asks. The groove takes the track's color, so ::slider-track colors it. accent-color no longer colors a progress bar's fill. Co-Authored-By: Claude Opus 5.5 --- README.md | 3 +- docs/guides/04-styling.md | 6 ++- docs/guides/06-api.md | 22 +++++++-- examples/animated.ts | 3 +- examples/progress-bar.ts | 3 +- src/internal/cssom.ts | 63 +++++++++++------------- src/internal/cssselectors.ts | 43 +++++++++++++++++ src/internal/cssvalues.ts | 9 ++++ src/internal/dom.ts | 22 +++++++-- src/internal/useragent.ts | 14 +++--- tests/widgets.test.ts | 93 ++++++++++++++++++++++++++++++++---- 11 files changed, 220 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 5e69c19ab..f0cb317d9 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,8 @@ document.body.innerHTML = `
diff --git a/docs/guides/04-styling.md b/docs/guides/04-styling.md index 2c797ea44..c8b424e25 100644 --- a/docs/guides/04-styling.md +++ b/docs/guides/04-styling.md @@ -171,8 +171,10 @@ ranges in `CSS.highlights`. `::placeholder` styles a text control's placeholder. `::backdrop` styles what a modal dialog or popover covers. A `` text types, `