diff --git a/packages/web/src/schematic.test.ts b/packages/web/src/schematic.test.ts index b78580d..5b62bd8 100644 --- a/packages/web/src/schematic.test.ts +++ b/packages/web/src/schematic.test.ts @@ -29,7 +29,7 @@ describe("schematic", () => { { driverTeeth: 7, drivenTeeth: 63 }, ] }); const gears = [...svg.matchAll( - /\s*]*>(\d+)<\/text>/g, + /]*\/>\s*]*>(\d+)<\/text>/g, )].map((m) => ({ cx: Number(m[1]), r: Number(m[2]), teeth: Number(m[3]) })); expect(gears.length).toBe(4); const byTeeth = (t: number) => gears.find((g) => g.teeth === t)!; @@ -51,4 +51,20 @@ describe("schematic", () => { expect(svg).toContain("idler"); expect(svg).toContain("output: same sense as driver"); }); + + it("carries fill/stroke as presentation attributes so shapes survive CSS being stripped (e.g. Safari Reader view)", () => { + const svg = schematicSvg({ stages: [ + { driverTeeth: 8, drivenTeeth: 59 }, + { driverTeeth: 20, drivenTeeth: 20, isIdler: true }, + ] }); + const circles = [...svg.matchAll(/]*>/g)]; + expect(circles.length).toBeGreaterThan(0); + // every gear circle must declare fill="none" inline — without it, a + // stylesheet-less render (CSS stripped) falls back to a solid black fill + for (const circle of circles) { + expect(circle[0]).toMatch(/fill="none"/); + expect(circle[0]).toMatch(/stroke="#[0-9a-f]+"/); + } + expect(svg).toMatch(/]*stroke="#[0-9a-f]+"[^>]*\/>/); + }); }); diff --git a/packages/web/src/schematic.ts b/packages/web/src/schematic.ts index 0e31567..c59a9ea 100644 --- a/packages/web/src/schematic.ts +++ b/packages/web/src/schematic.ts @@ -43,6 +43,10 @@ export function schematicSvg(train: GearTrain): string { const width = Math.max(naturalWidth, dirLabel.length * 7.2 + PAD * 2); const dx = (width - naturalWidth) / 2; + // Presentation attributes (fill/stroke) mirror the .gear-node CSS as a + // fallback: CSS overrides them when it's loaded, but if it's stripped + // (e.g. Safari Reader view) the shapes still outline instead of + // defaulting to a solid black fill. const nodes = arborGears.map((gearsOnArbor, i) => { // draw the larger gear first so a concentric pinion sits on top of it const sorted = [...gearsOnArbor].sort((a, b) => radius(b.teeth) - radius(a.teeth)); @@ -51,15 +55,17 @@ export function schematicSvg(train: GearTrain): string { // a solo (or the smaller concentric) gear labels at centre; a larger // companion labels just inside its own rim so the two never collide const labelY = sorted.length > 1 && j === 0 ? cy - r + 14 : cy + 4; + const stroke = g.idler ? "#6e695c" : "#5b574c"; + const dash = g.idler ? ` stroke-dasharray="3 3"` : ""; return ` - - ${g.teeth} - ${g.idler ? `idler` : ""}`; + + ${g.teeth} + ${g.idler ? `idler` : ""}`; }).join(""); }).join(""); - const baseline = ``; - const dirTag = `${dirLabel}`; + const baseline = ``; + const dirTag = `${dirLabel}`; // Describe the train for assistive tech — role="img" makes the child // presentational, so everything a sighted user reads must be in the label. const stageWords = train.stages diff --git a/packages/web/src/styles/app.css b/packages/web/src/styles/app.css index a7bb8de..205f727 100644 --- a/packages/web/src/styles/app.css +++ b/packages/web/src/styles/app.css @@ -90,11 +90,6 @@ thead th { line-height: var(--leading-normal); } -.masthead .intro strong { - color: var(--ink); - font-weight: 600; -} - /* ---- Input panel: two labelled groups ---------------------------------- */ /* Groups sit side by side on wide screens and stack when the row gets tight, so the panel uses the horizontal space instead of running down the page. */ diff --git a/packages/web/src/ui/app.ts b/packages/web/src/ui/app.ts index dbfe7d2..a9ea257 100644 --- a/packages/web/src/ui/app.ts +++ b/packages/web/src/ui/app.ts @@ -20,7 +20,7 @@ export function mountApp(root: HTMLElement): void { root.innerHTML = `

Involute

Find the gear tooth counts for a watch complication, and see how accurate each train is.

-

Pick what the train should track — a moon phase, a calendar, world time — and Involute computes the tooth counts and how far each option drifts. The result below is a live example: a moon-phase train accurate to about a day in 122 years. Change the target to solve your own.

+

Pick a target — moon phase, calendar, world time. The result below is a live example; change it to solve your own.