feat(store): rebuild the storefront to match its approved design #86

Merged
yuris merged 5 commits from design/od-alignment into main 2026-09-04 06:50:20 +00:00
Owner

Closes nothing on the tracker — this is the gap the user reported directly: the built site did not match the approved OpenDesign composition.

What was wrong

The design's token layer had landed and matched the mockups exactly — colours, the 4px spacing scale, the 1280px container, the 44px rhythm, Archivo + IBM Plex Sans. Everything above that was independently reinvented and drifted.

Measured, not asserted: 90 classes the design defines were unused by the app, and 51 classes the app used have no rule in the design at all. The two shared almost no component vocabulary.

Homepage alone, before:

Design Was shipping
Header full-bleed white, 61px, search inside transparent, 92px, no search
Hero white band, py 32, 2-col + image py 48/40, 1-col, no image
Sections 4 5 (extra category tiles)
Product grid 5 cols, cards 237x380 6 cols, cards 195x332
Dark band --ink, 482px, 2 photo cards --dark, 210px, 2 buttons
Location tinted, 3-col alamat/jam/peta plain, one 1290px photo

Approach

Reconciling rule-by-rule would drift again, so the design's own stylesheet is now the source of truth. apps/store/scripts/port-design.mjs ports ~/.od/projects/garage/assets/tokens.css into app/globals.css, and every page's markup was rebuilt onto the design's own class names. The port is idempotent (byte-identical on re-run) and formats its own output, so re-running it is how a design revision reaches the app — globals.css should not be hand-edited.

Two forced divergences from that source, both recorded in the generated file:

  1. Relative oklch(from ...) is resolved to literals. This is a static export and relative colour needs Chrome 119+, where a miss leaves the whole declaration invalid. Values were read out of Chrome, not computed by hand; translucent ones stay exact as rgb(r g b / a).
  2. The focus ring stays the app's blue --focus, not the design's 2px solid var(--accent) — on an accent-filled .btn--primary that ring is invisible.

Where the design shows a grey placeholder it was standing in for photography this repo already has, so those slots use the real shop photos. The homepage map regained the OpenStreetMap attribution the port had dropped: ODbL requires it and object-fit: cover crops the notice burned into the raster.

Verification

Per page, at 1440 and 375, by measurement:

  • /services 615/615 desktop and 1336/1336 mobile; /contact 934/934 desktop — exact.
  • Hero 304px/32px, rail 482px, .section--sunk rgb(237,235,229), product grid 5 x 236.797px, PDP 764+420 with a 44px price — all identical to the mockups.
  • Remaining height deltas are fixture volume: the mockups hardcode 10-16 products, the fixture serves 6. The only non-data delta is +22px on /contact at 375, where the OSM credit wraps to a second line.
  • All 12 built renders (6 routes x 2 widths) hydrate with zero page errors, zero 4xx and no horizontal overflow.
  • Interactivity proven on the export, not just dev: a facet click filters 6 cards to 1 and sets ?kategori=ban.

504 tests pass (24 + 370 + 110), both typechecks exit 0, next build exports 21 pages.

#67 / #72 — read this bit

The !important that #67 added to hold .product-type above the #57 cascade class is gone, and the gate's own comment flagged it as load-bearing and not to be fixed casually. It is not a silent regression: the design renders the price as <span class="price card-price"> while the only paragraph in the card body is .card-name, so no paragraph rule competes with the price and nothing needs to outrank anything.

Verified the way #72 asks for — in a browser, against BUILT output rather than dev: on out/, the price computes to 21.6px/700 Archivo and is the largest text in the card, with 0 !important across all 253 rules. If a future revision puts the price back in a <p>, this returns; re-read #67.

Also in here

  • Local bundled images moved to next/image with explicit dimensions (images.unoptimized is already set, so it works in the export and costs nothing). The four remaining raw <img> are catalog photography and carry biome-ignores naming why: their URLs come from R2_PUBLIC_URL, an env var with no build-time hostname, so next/image cannot allowlist them.
  • app/category-tiles.tsx deleted — .cat-grid/.cat-tile appear in no current mockup, and produk.html's category entry point is the filter group.
  • The kirim/pasang pair no longer depends on findService resolving. installService != null is the shop's assertion from the management app; lib/services.ts is a separate presentation-side copy that can drift, and gating a real fulfilment capability on that lookup silently removes "pasang di tempat" from a product the shop does install.
  • lib/business.ts gains hoursRange and sameDayCutoff.

Four rules leave the biome-gate BASELINE because they no longer exist, which the gate requires be recorded in the commit that eliminates them: noImportantStyles and noDescendingSpecificity (the two flagged as load-bearing — see the rewritten note there), noImgElement, and suppressions/unused. Gate re-verified both ways: PASSED at this head, FAILED on purpose against a planted debugger.

Needs a human answer

business.sameDayCutoff = "15.00 WIB" is design copy, not confirmed with the shop owner. It is flagged in the file and lives in exactly one place. If it is wrong, it is wrong once.

Pre-existing and untouched: the app ships no favicon, so browsers 404 on /favicon.ico.

Closes nothing on the tracker — this is the gap the user reported directly: the built site did not match the approved OpenDesign composition. ## What was wrong The design's **token layer** had landed and matched the mockups exactly — colours, the 4px spacing scale, the 1280px container, the 44px rhythm, Archivo + IBM Plex Sans. Everything above that was independently reinvented and drifted. Measured, not asserted: **90** classes the design defines were unused by the app, and **51** classes the app used have no rule in the design at all. The two shared almost no component vocabulary. Homepage alone, before: | | Design | Was shipping | |---|---|---| | Header | full-bleed white, 61px, search inside | transparent, 92px, no search | | Hero | white band, py 32, 2-col + image | py 48/40, 1-col, no image | | Sections | 4 | 5 (extra category tiles) | | Product grid | 5 cols, cards 237x380 | 6 cols, cards 195x332 | | Dark band | `--ink`, 482px, 2 photo cards | `--dark`, 210px, 2 buttons | | Location | tinted, 3-col alamat/jam/peta | plain, one 1290px photo | ## Approach Reconciling rule-by-rule would drift again, so the design's own stylesheet is now the source of truth. `apps/store/scripts/port-design.mjs` ports `~/.od/projects/garage/assets/tokens.css` into `app/globals.css`, and every page's markup was rebuilt onto the design's own class names. The port is **idempotent** (byte-identical on re-run) and formats its own output, so re-running it is how a design revision reaches the app — `globals.css` should not be hand-edited. Two forced divergences from that source, both recorded in the generated file: 1. Relative `oklch(from ...)` is resolved to literals. This is a static export and relative colour needs Chrome 119+, where a miss leaves the whole declaration invalid. Values were read out of Chrome, not computed by hand; translucent ones stay exact as `rgb(r g b / a)`. 2. The focus ring stays the app's blue `--focus`, not the design's `2px solid var(--accent)` — on an accent-filled `.btn--primary` that ring is invisible. Where the design shows a grey placeholder it was standing in for photography this repo already has, so those slots use the real shop photos. The homepage map regained the **OpenStreetMap attribution** the port had dropped: ODbL requires it and `object-fit: cover` crops the notice burned into the raster. ## Verification Per page, at 1440 and 375, by measurement: - `/services` **615/615** desktop and **1336/1336** mobile; `/contact` **934/934** desktop — exact. - Hero 304px/32px, rail 482px, `.section--sunk` `rgb(237,235,229)`, product grid `5 x 236.797px`, PDP `764+420` with a 44px price — all identical to the mockups. - Remaining height deltas are fixture volume: the mockups hardcode 10-16 products, the fixture serves 6. The only non-data delta is **+22px** on `/contact` at 375, where the OSM credit wraps to a second line. - All 12 built renders (6 routes x 2 widths) hydrate with **zero** page errors, **zero** 4xx and no horizontal overflow. - Interactivity proven on the **export**, not just dev: a facet click filters 6 cards to 1 and sets `?kategori=ban`. 504 tests pass (24 + 370 + 110), both typechecks exit 0, `next build` exports 21 pages. ## #67 / #72 — read this bit The `!important` that #67 added to hold `.product-type` above the #57 cascade class is **gone**, and the gate's own comment flagged it as load-bearing and not to be fixed casually. It is not a silent regression: the design renders the price as `<span class="price card-price">` while the only paragraph in the card body is `.card-name`, so no paragraph rule competes with the price and nothing needs to outrank anything. Verified the way **#72** asks for — in a browser, against **BUILT** output rather than dev: on `out/`, the price computes to **21.6px/700 Archivo** and is the largest text in the card, with **0 `!important` across all 253 rules**. If a future revision puts the price back in a `<p>`, this returns; re-read #67. ## Also in here - Local bundled images moved to `next/image` with explicit dimensions (`images.unoptimized` is already set, so it works in the export and costs nothing). The four remaining raw `<img>` are catalog photography and carry `biome-ignore`s naming why: their URLs come from `R2_PUBLIC_URL`, an env var with no build-time hostname, so `next/image` cannot allowlist them. - `app/category-tiles.tsx` **deleted** — `.cat-grid`/`.cat-tile` appear in no current mockup, and `produk.html`'s category entry point is the filter group. - The kirim/pasang pair no longer depends on `findService` resolving. `installService != null` is the shop's assertion from the management app; `lib/services.ts` is a separate presentation-side copy that can drift, and gating a real fulfilment capability on that lookup silently removes "pasang di tempat" from a product the shop does install. - `lib/business.ts` gains `hoursRange` and `sameDayCutoff`. Four rules leave the biome-gate `BASELINE` because they no longer exist, which the gate requires be recorded in the commit that eliminates them: `noImportantStyles` and `noDescendingSpecificity` (the two flagged as load-bearing — see the rewritten note there), `noImgElement`, and `suppressions/unused`. Gate re-verified **both ways**: PASSED at this head, FAILED on purpose against a planted `debugger`. ## Needs a human answer `business.sameDayCutoff = "15.00 WIB"` is **design copy, not confirmed with the shop owner**. It is flagged in the file and lives in exactly one place. If it is wrong, it is wrong once. Pre-existing and untouched: the app ships no favicon, so browsers 404 on `/favicon.ico`.
feat(store): rebuild the storefront to match its approved design
All checks were successful
Checks / test @garage/catalog (pull_request) Successful in 1m15s
Checks / test garage (pull_request) Successful in 2m25s
Checks / test garage-store (pull_request) Successful in 2m31s
Checks / typecheck apps/management (pull_request) Successful in 1m38s
Checks / lint (biome) (pull_request) Successful in 11s
Checks / typecheck apps/store (pull_request) Successful in 2m21s
00eddef0b6
The design was never actually implemented. What landed was its token layer —
colours, the 4px spacing scale, the 1280px container, the 44px rhythm, Archivo
+ IBM Plex Sans — all of which matched the mockups exactly. Every page's
layout, components and spacing was then independently reinvented and drifted.
Measured, the two shared almost no component vocabulary: 90 classes the design
defines were unused by the app, and 51 classes the app used the design has no
rule for.

The visible cost, homepage alone: no header search, a one-column hero with no
image, an extra category-tile section the design does not have, a 6-column
grid of 195x332 cards where the design has 5 of 237x380, a 210px button strip
standing in for a 482px service rail with photography, and one 1290px photo
where the design has a three-column alamat/jam/peta block.

Rather than reconcile rule-by-rule and drift again, the design's own
stylesheet is now the source of truth: `scripts/port-design.mjs` ports
~/.od/projects/garage/assets/tokens.css into app/globals.css and every page's
markup was rebuilt onto the design's own class names. The port is idempotent
(byte-identical on re-run) and formats its output, so re-running it is how a
design revision reaches the app.

Two forced divergences from that source, both recorded in the generated file:
relative `oklch(from ...)` is resolved to literals, because this is a static
export and relative colour needs Chrome 119+ where a miss invalidates the
whole declaration (values read out of Chrome, not computed by hand;
translucent ones stay exact as `rgb(r g b / a)`); and the focus ring stays the
app's blue `--focus` rather than the design's `2px solid var(--accent)`, which
on an accent-filled `.btn--primary` is an invisible ring.

Where the design shows a grey placeholder it was standing in for photography
this repo already has, so those slots use the real shop photos. The homepage
map regained the OpenStreetMap attribution the port had dropped — ODbL
requires it and `object-fit: cover` crops the notice burned into the raster.

Verified against the mockups at 1440 and 375, per page, by measurement rather
than impression. /services and /contact land on the mockup's numbers exactly
(main 615/615 and 1336/1336; 934/934 desktop). Section heights, paddings,
background colours and grid tracks match on every page — hero 304px/32px,
rail 482px, `.section--sunk` rgb(237,235,229), product grid 5x236.797px, PDP
764+420 with a 44px price. The remaining height deltas are all fixture volume:
the mockups hardcode 10-16 products where the fixture serves 6. The only
non-data delta is +22px on /contact at 375, where the OSM credit wraps.

Also fixed while in here, all of it verified in a browser against BUILT output
rather than dev:

- The `!important` that #67 added to hold `.product-type` above the #57
  cascade class is gone, and this is not a silent regression: the design
  renders the price as `<span class="price card-price">` while the only
  paragraph in the card body is `.card-name`, so no paragraph rule competes
  with the price. On `out/`, the price computes to 21.6px/700 Archivo and is
  the largest text in the card, with 0 `!important` across all 253 rules —
  which is the check #72 asks for. If a revision puts the price back in a
  `<p>`, re-read #67.
- Local bundled images moved to `next/image` with explicit dimensions
  (`images.unoptimized` is already set, so this works in the export and costs
  nothing). The four remaining raw `<img>` are catalog photography and now
  carry `biome-ignore`s naming why: their URLs come from `R2_PUBLIC_URL`, an
  env var with no build-time hostname, so `next/image` cannot allowlist them.
- `app/category-tiles.tsx` deleted: `.cat-grid`/`.cat-tile` appear in no
  current mockup, and produk.html's category entry point is the filter group.
- The kirim/pasang pair no longer depends on `findService` resolving.
  `installService != null` is the shop's assertion from the management app;
  `lib/services.ts` is a separate presentation-side copy that can drift, and
  gating a real fulfilment capability on that lookup silently removes "pasang
  di tempat" from a product the shop does install.
- `lib/business.ts` gains `hoursRange` and `sameDayCutoff`. The cutoff is
  design copy and is NOT confirmed with the owner — flagged in the file.

Four rules leave the biome-gate BASELINE because they no longer exist, which
the gate requires be recorded in the commit that eliminates them:
`noImportantStyles` and `noDescendingSpecificity` (the two the gate flagged as
load-bearing — see the rewritten note there), `noImgElement`, and
`suppressions/unused`. Gate re-verified both ways: PASSED at this head, and
FAILED on purpose against a planted `debugger`.

504 tests pass (24 + 370 + 110), both typechecks exit 0, `next build` exports
21 pages, and all 12 built renders (6 routes x 2 widths) hydrate with zero
page errors, zero 4xx and no horizontal overflow. Interactivity proven on the
export, not just dev: a facet click filters 6 cards to 1 and sets
`?kategori=ban`.

Pre-existing and untouched: the app ships no favicon, so browsers 404 on
/favicon.ico.
Author
Owner

approve-with-nits — reviewed 00eddef (design/od-alignment). Scope of this pass: static correctness only — the port script, the deliberate behaviour changes, the gate baseline edit, and the lint suppressions. I did not build or serve anything; build artifacts and rendered geometry belong to the other reviewer.

The load-bearing claims hold up. I re-derived them rather than reading them:

Claim Result
globals.css is generated and idempotent True. Two consecutive runs, byte-identical, md5 46f58df96d6f8e10b19dde2811e07b42, matching the committed file.
No rule silently dropped from the source stylesheet True. postcss walk: 276 source rules → 305 generated, 0 source rule-keys absent, 0 appearing fewer times.
Only two deliberate divergences True. The only declaration-level deltas are 21 oklch(from …) resolutions plus the 2 :focus-visible lines. Everything else differs by formatter whitespace only.
Colour literals are correct True. Recomputed all 7 independently through sRGB→OKLab→OKLCH: 6 exact, 1 off by 1/255 in blue (#e4f3e9 vs #e4f3e8, rounding). --ink/--bg alpha forms 23 32 24 / 247 245 239 are exact.
Tests / typechecks True. store 110, catalog 24, management 370 = 504, all exit 0. Both tsc --noEmit exit 0, no output.

The four removed BASELINE rules — ruled on independently. First I closed the trap that would make all four read zero for the same wrong reason: all 6 parse errors are apps/management/app/globals.css, and planting color: red !important into the store's stylesheet raises lint/complexity/noImportantStyles immediately. biome fully lints the new file, so the zeroes are real.

  • noImportantStyles — genuinely gone. apps/store contains zero !important in any .css or .tsx; postcss confirms 0 important declarations across the stylesheet. I verified the cascade claim statically too: .card-price is 1.35rem (= 21.6px) and .price carries font-weight: 700, against .card-name at 13px/500 and .card-brand at 11px; the only p selectors in the file are .rail-card p and .proof-body p, neither inside .card. Price is in a <span>, so no paragraph rule can reach it. The rewritten note is honest.
  • noDescendingSpecificity — genuinely gone, with the whole block it lived in. No suppression added.
  • suppressions/unused — genuinely gone, and the run reports 0, so no new dead suppressions were introduced either.
  • noImgElement — suppressed, not gone, and the note says so plainly. Worth recording that this strengthens the gate rather than weakening it: I planted an unsuppressed <img> and the gate failed with FAIL: new rule lint/performance/noImgElement — 1 finding(s), none at baseline. At a baseline of 0 any new one fails; at 3 it would not have.

Gate negative control. Planted debugger; → FAIL: new rule lint/suspicious/noDebugger, exit 1. Planted raw <img> → exit 1 as above. Removed both → biome-gate: PASSED, 41 findings against a baseline of 41, 242 files. The gate still discriminates.

sameDayCutoff — fine to ship. I checked this against the mockups rather than taking the flag at face value: beranda.html contains both Order sebelum 15.00 WIB dikirim hari itu and <li><span>Batas kirim hari sama</span><span>15.00 WIB</span></li>. It is faithful transcription of approved design copy, not an invented operational fact, it is centralised in one constant, and the comment flags the open question accurately.

Behaviour changes — all sound. lib/facets.ts, lib/checkout-message.ts and lib/cart.ts are untouched. The cart still keys on sameLine(slug, installAtShop), and every setQty/remove in the re-skinned view passes both, with the React key and the radio group name both including the mode — kirim and pasang stay distinct, and cart.test.ts already covers that invariant. All four FACET_KEYS plus q round-trip: q enters through the two native GET forms to /products and is read back by the unchanged selectionFromParams. ProductCard takes exactly one prop at all four call sites. The findService decoupling is right — capability keys on the raw installService field, while the resolved lookup only gates the link and the service name, which is the correct split.


Should fix

1. port-design.mjs — two unguarded .replace() calls silently delete the focus ring. (the most important finding)

The script throws loudly on an unmapped relative colour, a missing <style> block, a moved source file, and a missing focus block. The two substitutions in section 3 have no such guard, and String.replace with a string pattern is a no-op when it does not match.

I ran this end to end. Copy the source, apply a plausible design revision — --container: 1280px → 1200px — and re-run the port:

ported 1193 lines -> /tmp/portsim/app/globals.css
PORT_EXIT=0

Green. But --focus is now absent from :root, while the output still emits:

:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 4px;
}

var(--focus) with --focus undefined is invalid at computed-value time, so outline falls back to its initial value and every keyboard focus indicator on the storefront disappears — silently, on the exact workflow the generated header tells the next person to use. color-scheme: light survives only because :root { still matches, which isolates the fault to the --container anchor.

Two lines, matching the guard style already used three times in the same file:

const containerAnchor = "  --container: 1280px;";
if (!css.includes(containerAnchor)) throw new Error("--container anchor not found");
css = css.replace(
  containerAnchor,
  `${containerAnchor}
  --focus: #1b4d8f; /* focus-visible ring; see :focus-visible below */`,
);
if (!css.includes(":root {")) throw new Error(":root not found");

2. The next/image justification is false as written. It appears in four biome-ignore comments and again in the gate. I checked Next 16.2.10's own source: generateImgAttrs (dist/shared/lib/get-img-props.js:96-122) returns { src, srcSet: undefined, sizes: undefined } immediately when unoptimized is set, so defaultLoader — the only place hasRemoteMatch is ever called — never runs. That check is additionally NODE_ENV !== 'production' gated and only fires when domains || remotePatterns is configured. So images: { unoptimized: true } removes hostname validation; it does not add a failure mode, and next/image would in fact accept an arbitrary R2_PUBLIC_URL-derived absolute URL untouched.

The decision to keep raw <img> is still defensible — under unoptimized the component does no optimization and would only add a wrapper — but the stated reason is the opposite of what the framework does, and it is now the authoritative note in five places, including a CI gate. Rewrite it to the true reason: images.unoptimized is on for static export, so next/image performs no optimization here and adds only a wrapper.

3. Regeneration is not reproducible by anyone else. SRC is ~/.od/projects/garage/assets/tokens.css; git ls-files finds no vendored copy of the mockups or the stylesheet. The generated header instructs the next person to re-run the port instead of editing rules, but that only works on a machine holding that OpenDesign project at that exact path — and the script is referenced nowhere in package.json, CI, or any doc, so discoverability rests on someone opening a 1910-line generated file and reading its header. Vendoring the 30 KB source next to the script (and adding a "port" script) would make the claim true. The app-level additions themselves are fine: sections 4 and 5 live inside the script, and my byte-identical re-run confirms a revision preserves them.

Nits

4. [slug]/page.tsx:331 tests product.installService !== null while line 320 tests the same field for truthiness. They diverge only for "", which both dashboard write paths normalise away (installService || null) but which the MCP tool and API validator (z.string().nullable()) accept; the result would be the pasang option offered with the "Bisa dipasang di tempat" tag hidden and no service named. Boolean(product.installService) settles it.

5. The gate header still reads Checked 240 files. Found 32 errors, 12 warnings, 3 infos immediately above a Broken down: list that was updated and now sums to 41. Measured tree is 242 files / 32 errors / 6 warnings / 3 infos. The file's own instruction is that the commit moving the numbers re-measures rather than carrying them forward.

6. business.ts attributes the cutoff to "(beranda/kontak)"; kontak.html contains no occurrence — it is beranda only.

7. product-browser.tsx:101-105 justifies the 200 ms debounce by "the search box fires one change per keystroke", but there is no search input left in that component — q now arrives by navigation. The debounce still earns its place for rapid facet toggling; the rationale just no longer matches.

No blockers. Nothing here changes what a customer can do today; #1 is a latent defect in the maintenance path, and it is a two-line fix.

**approve-with-nits** — reviewed `00eddef` (`design/od-alignment`). Scope of this pass: static correctness only — the port script, the deliberate behaviour changes, the gate baseline edit, and the lint suppressions. I did not build or serve anything; build artifacts and rendered geometry belong to the other reviewer. The load-bearing claims hold up. I re-derived them rather than reading them: | Claim | Result | |---|---| | `globals.css` is generated and idempotent | **True.** Two consecutive runs, byte-identical, `md5 46f58df96d6f8e10b19dde2811e07b42`, matching the committed file. | | No rule silently dropped from the source stylesheet | **True.** postcss walk: 276 source rules → 305 generated, **0 source rule-keys absent**, 0 appearing fewer times. | | Only two deliberate divergences | **True.** The only declaration-level deltas are 21 `oklch(from …)` resolutions plus the 2 `:focus-visible` lines. Everything else differs by formatter whitespace only. | | Colour literals are correct | **True.** Recomputed all 7 independently through sRGB→OKLab→OKLCH: 6 exact, 1 off by 1/255 in blue (`#e4f3e9` vs `#e4f3e8`, rounding). `--ink`/`--bg` alpha forms `23 32 24` / `247 245 239` are exact. | | Tests / typechecks | **True.** store 110, catalog 24, management 370 = **504**, all exit 0. Both `tsc --noEmit` exit 0, no output. | **The four removed `BASELINE` rules — ruled on independently.** First I closed the trap that would make all four read zero for the same wrong reason: all 6 `parse` errors are `apps/management/app/globals.css`, and planting `color: red !important` into the store's stylesheet raises `lint/complexity/noImportantStyles` immediately. biome fully lints the new file, so the zeroes are real. - `noImportantStyles` — **genuinely gone.** `apps/store` contains zero `!important` in any `.css` or `.tsx`; postcss confirms 0 important declarations across the stylesheet. I verified the cascade claim statically too: `.card-price` is `1.35rem` (= 21.6px) and `.price` carries `font-weight: 700`, against `.card-name` at 13px/500 and `.card-brand` at 11px; the only `p` selectors in the file are `.rail-card p` and `.proof-body p`, neither inside `.card`. Price is in a `<span>`, so no paragraph rule can reach it. The rewritten note is honest. - `noDescendingSpecificity` — **genuinely gone**, with the whole block it lived in. No suppression added. - `suppressions/unused` — **genuinely gone**, and the run reports 0, so no new dead suppressions were introduced either. - `noImgElement` — **suppressed, not gone**, and the note says so plainly. Worth recording that this *strengthens* the gate rather than weakening it: I planted an unsuppressed `<img>` and the gate failed with `FAIL: new rule lint/performance/noImgElement — 1 finding(s), none at baseline`. At a baseline of 0 any new one fails; at 3 it would not have. **Gate negative control.** Planted `debugger;` → `FAIL: new rule lint/suspicious/noDebugger`, exit 1. Planted raw `<img>` → exit 1 as above. Removed both → `biome-gate: PASSED`, 41 findings against a baseline of 41, 242 files. The gate still discriminates. **`sameDayCutoff` — fine to ship.** I checked this against the mockups rather than taking the flag at face value: `beranda.html` contains both `Order sebelum 15.00 WIB dikirim hari itu` and `<li><span>Batas kirim hari sama</span><span>15.00 WIB</span></li>`. It is faithful transcription of approved design copy, not an invented operational fact, it is centralised in one constant, and the comment flags the open question accurately. **Behaviour changes — all sound.** `lib/facets.ts`, `lib/checkout-message.ts` and `lib/cart.ts` are untouched. The cart still keys on `sameLine(slug, installAtShop)`, and every `setQty`/`remove` in the re-skinned view passes both, with the React key and the radio `group` name both including the mode — kirim and pasang stay distinct, and `cart.test.ts` already covers that invariant. All four `FACET_KEYS` plus `q` round-trip: `q` enters through the two native GET forms to `/products` and is read back by the unchanged `selectionFromParams`. `ProductCard` takes exactly one prop at all four call sites. The `findService` decoupling is right — capability keys on the raw `installService` field, while the resolved lookup only gates the link and the service name, which is the correct split. --- ### Should fix **1. `port-design.mjs` — two unguarded `.replace()` calls silently delete the focus ring.** *(the most important finding)* The script throws loudly on an unmapped relative colour, a missing `<style>` block, a moved source file, and a missing focus block. The two substitutions in section 3 have no such guard, and `String.replace` with a string pattern is a no-op when it does not match. I ran this end to end. Copy the source, apply a plausible design revision — `--container: 1280px` → `1200px` — and re-run the port: ``` ported 1193 lines -> /tmp/portsim/app/globals.css PORT_EXIT=0 ``` Green. But `--focus` is now absent from `:root`, while the output still emits: ```css :focus-visible { outline: 3px solid var(--focus); outline-offset: 4px; } ``` `var(--focus)` with `--focus` undefined is invalid at computed-value time, so `outline` falls back to its initial value and **every keyboard focus indicator on the storefront disappears** — silently, on the exact workflow the generated header tells the next person to use. `color-scheme: light` survives only because `:root {` still matches, which isolates the fault to the `--container` anchor. Two lines, matching the guard style already used three times in the same file: ```suggestion const containerAnchor = " --container: 1280px;"; if (!css.includes(containerAnchor)) throw new Error("--container anchor not found"); css = css.replace( containerAnchor, `${containerAnchor} --focus: #1b4d8f; /* focus-visible ring; see :focus-visible below */`, ); if (!css.includes(":root {")) throw new Error(":root not found"); ``` **2. The `next/image` justification is false as written.** It appears in four `biome-ignore` comments and again in the gate. I checked Next 16.2.10's own source: `generateImgAttrs` (`dist/shared/lib/get-img-props.js:96-122`) returns `{ src, srcSet: undefined, sizes: undefined }` immediately when `unoptimized` is set, so `defaultLoader` — the only place `hasRemoteMatch` is ever called — never runs. That check is additionally `NODE_ENV !== 'production'` gated and only fires when `domains || remotePatterns` is configured. So `images: { unoptimized: true }` *removes* hostname validation; it does not add a failure mode, and `next/image` would in fact accept an arbitrary `R2_PUBLIC_URL`-derived absolute URL untouched. The decision to keep raw `<img>` is still defensible — under `unoptimized` the component does no optimization and would only add a wrapper — but the stated reason is the opposite of what the framework does, and it is now the authoritative note in five places, including a CI gate. Rewrite it to the true reason: *`images.unoptimized` is on for static export, so `next/image` performs no optimization here and adds only a wrapper.* **3. Regeneration is not reproducible by anyone else.** `SRC` is `~/.od/projects/garage/assets/tokens.css`; `git ls-files` finds no vendored copy of the mockups or the stylesheet. The generated header instructs the next person to re-run the port instead of editing rules, but that only works on a machine holding that OpenDesign project at that exact path — and the script is referenced nowhere in `package.json`, CI, or any doc, so discoverability rests on someone opening a 1910-line generated file and reading its header. Vendoring the 30 KB source next to the script (and adding a `"port"` script) would make the claim true. The app-level additions themselves are fine: sections 4 and 5 live inside the script, and my byte-identical re-run confirms a revision preserves them. ### Nits **4.** `[slug]/page.tsx:331` tests `product.installService !== null` while line 320 tests the same field for truthiness. They diverge only for `""`, which both dashboard write paths normalise away (`installService || null`) but which the MCP tool and API validator (`z.string().nullable()`) accept; the result would be the pasang option offered with the "Bisa dipasang di tempat" tag hidden and no service named. `Boolean(product.installService)` settles it. **5.** The gate header still reads `Checked 240 files. Found 32 errors, 12 warnings, 3 infos` immediately above a `Broken down:` list that was updated and now sums to 41. Measured tree is 242 files / 32 errors / 6 warnings / 3 infos. The file's own instruction is that the commit moving the numbers re-measures rather than carrying them forward. **6.** `business.ts` attributes the cutoff to "(beranda/kontak)"; `kontak.html` contains no occurrence — it is beranda only. **7.** `product-browser.tsx:101-105` justifies the 200 ms debounce by "the search box fires one change per keystroke", but there is no search input left in that component — `q` now arrives by navigation. The debounce still earns its place for rapid facet toggling; the rationale just no longer matches. No blockers. Nothing here changes what a customer can do today; #1 is a latent defect in the maintenance path, and it is a two-line fix.
fix(store): action review 74 — guard the port, correct a false claim, vendor the source
All checks were successful
Checks / typecheck apps/management (pull_request) Successful in 1m36s
Checks / test @garage/catalog (pull_request) Successful in 1m13s
Checks / test garage (pull_request) Successful in 1m24s
Checks / test garage-store (pull_request) Successful in 2m18s
Checks / lint (biome) (pull_request) Successful in 11s
Checks / typecheck apps/store (pull_request) Successful in 1m36s
9a55a53a70
Review of #86: approve-with-nits, 0 blockers, 2 real bugs, 5 shoulds/nits. All
seven actioned. The reviewer verified the load-bearing claims independently —
port idempotent (md5 46f58df), 276 source rules -> 305 generated with zero
source rule-keys dropped, 6 of 7 colour literals exact and one off by 1/255,
504 tests, gate discriminating — so this commit is only the findings.

**The port could silently delete the site's focus ring.** Two `css.replace()`
calls had no guard, and a string-pattern replace is a no-op when its anchor
moves. Demonstrated by the reviewer with a plausible revision
(`--container: 1280px` -> `1200px`): the port prints a line count and exits 0
while `--focus` is never declared, yet `:focus-visible` still references
`var(--focus)`. An undefined custom property is invalid at computed-value time,
so `outline` falls back to none and every keyboard focus indicator disappears —
on the exact workflow the generated header tells the next person to use, and on
the one accessibility decision this PR deliberately kept from the design.

Both substitutions now go through a `substitute()` that throws, and the emitted
stylesheet is asserted to contain `--focus:` and `var(--focus)` together, since
a guard on the anchor cannot catch a declared-but-unreferenced pair. Re-proved
both ways: the planted revision now exits 1 with "anchor moved, refusing to
emit a partial stylesheet", and a normal run is still byte-identical.

**The `next/image` justification was backwards.** All four `biome-ignore`s and
the gate note claimed `images.unoptimized` would "add a host-validation failure
mode". The reviewer read next 16.2.10: `generateImgAttrs` returns before
`defaultLoader`, which is the only caller of `hasRemoteMatch`, and that check is
dev-only and inert unless `domains`/`remotePatterns` is set — `next.config.ts`
sets neither. So `unoptimized` REMOVES hostname validation; `next/image` would
have accepted these URLs untouched. The decision to keep raw `<img>` stands on
its real grounds — under `unoptimized` the component performs no optimization
and the aspect-ratio frame already reserves the space, so it adds a wrapper and
no benefit — and all five notes now say that, including that the earlier claim
was wrong.

**The documented workflow was not reproducible.** The port read
`~/.od/projects/garage/assets/tokens.css`, outside the repo, so on any other
clone it dies at `readFileSync` and `globals.css` becomes a generated file
nobody can regenerate. The design stylesheet and all six mockups are now
vendored at `apps/store/design/` (104K, byte-identical to upstream), the port
reads from there, and `pnpm port-design` makes it discoverable from
package.json rather than only from a comment in the generated CSS.

`apps/store/design/` is excluded in biome.json. It is vendored upstream source,
in the same category as the drizzle snapshots that must never be reformatted:
linting it added 3 `noInvalidGridAreas`, 1 `noDescendingSpecificity` and a
`format` finding for someone else's hand-written CSS, and formatting it would
defeat the point — the port must read the design's own bytes, and a diff on
`design/` should be the design change and nothing else.

Smaller findings:

- `canInstall` used `!== null` while the "Bisa dipasang di tempat" tag three
  lines above tested the same field for truthiness. They disagree on `""`,
  which the published catalog schema permits (`z.string().nullable()`, no min
  length) even though both dashboard write paths normalise it — the pasang
  option would be offered while the tag is hidden and no service is named.
  Both now use `Boolean()`.
- The gate header still read "Checked 240 files. Found 32 errors, 12 warnings"
  above a breakdown this PR had already updated to sum to 41. Re-measured at
  this head rather than carried forward, as that file's own instruction says:
  242 files, 32 errors / 6 warnings / 3 infos.
- `sameDayCutoff`'s provenance note said "(beranda/kontak)". The string appears
  twice in beranda.html and not once in kontak.html. That note is the entire
  justification for shipping an unconfirmed operational fact, so it is now
  accurate — and still flagged as NOT confirmed with the shop owner.
- The debounce rationale in `product-browser.tsx` justified 200ms by "one
  change per keystroke" from a search box the re-skin removed; `q` now arrives
  only by navigation from the native GET forms. The debounce still earns its
  place for rapid facet toggling, so only the reason changed.

504 tests pass, both typechecks exit 0, gate PASSED at 242 files, `next build`
exports 21 pages.
Author
Owner

request-changes — rendering/accessibility review of 00eddef, measured on a real next build static export (out/) plus a next dev cross-check, against the mockups at ~/.od/projects/garage. Fixture: my own 6-row catalog, and a second 16-row fixture replicating produk.html's own 16 cards so "it's just data volume" could be tested rather than taken on trust.

Almost everything in the PR body holds up under independent measurement. One thing does not, and it is on the homepage.

globals.css:1822 gives .map-media overflow, border, border-radius and aspect-ratio: 4/3 but never display. On / (app/page.tsx:177) that element is an <a> inside the plain block .info-block--map, so it stays display: inline — and aspect-ratio, width/height and overflow are all ignored on a non-replaced inline box.

Measured on out/:

1440 375
.map-media computed display inline inline
content spill past .info-block--map +72.813px +72.813px
credit bottom past <main> +28.813px +44.813px
overlaps .site-footer yes yes

The consequence is the attribution this PR deliberately added. It lands on the footer's rgb(23,32,24) while .map-credit is --muted rgb(77,89,79) → contrast 2.27:1, failing AA for 12px text. At 375 elementsFromPoint at the link's own text position returns a footer DIV on top, so the ODbL link is not the hit target. /contact escapes only by accident: its anchor carries style="flex:1;…" inside a flex parent, which blockifies it.

.map-media {
  display: block;
  margin: 0;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: var(--r2);
  background: var(--bg-sunk);
}

Verified in-browser: media height becomes 300px (finally honouring the ported 4/3), spill goes to −3.5px, footer overlap gone at both widths. Since globals.css is generated, the fix belongs in port-design.mjs.

On claim 3 — confirm / refute, my numbers

Claim Measured (app / mockup)
/services 615/615 desktop 615.313 / 615.313 ✅
/services 1336/1336 mobile 1336.25 / 1336.25 ✅
/contact 934/934 desktop 933.672 / 933.672, Δ0.000 ✅
hero 304px / 32px 304 / 304, padding 32/32 both ✅
rail 482px 481.672 / 481.672 ✅
.section--sunk rgb(237,235,229) identical, and pixel-identical to the design's oklch ✅
product grid 5 x 236.797px 236.797 ×4 + 236.812, identical both sides ✅
PDP 764+420, 44px price 764px 420px gap 48px, price 44px/700 — identical ✅
/contact +22px at 375 = OSM credit wrap +22.500; credit holder 16px→38.5px, 1→2 line boxes ✅
"all remaining deltas are fixture volume" refuted — see below ❌

The data-volume test. Rebuilt with the mockup's own 16 rows: per-card geometry then matched 16/16, zero mismatches, at both widths. /products decomposes cleanly into data and state — sidebar facet cardinality, the mockup's hardcoded active-filter chips, and Tampilkan 16 produk lagi (PAGE = 16, so remaining === 0 correctly hides it). That half of the claim is sound.

The homepage is not. At 1440 .section--sunk's info-grid is 326.313 vs the mockup's 281.313 — +45px with no product involved. Part is the intentional real-photo-for-grey-placeholder swap (disclosed elsewhere in the PR, just not covered by this sentence); the rest is the overflow above. So "the only non-data delta is +22px on /contact at 375" is not accurate — please reword it, because it is the sentence that would stop a reader looking at the homepage.

#72 — answered on built output, not agreed with

out/_next/static/chunks/*.css, in a browser:

  • price is <span class="price card-price"> ✅
  • 21.6px / 700 / Archivo at 1440, 20.8px at 375 ✅
  • largest text in every card — next largest is .card-name at 13px, 8.6px of clearance ✅
  • .card-name is the only <p> in the card body ✅
  • 0 !important in the built stylesheet (grep and CSSOM agree) ✅

So #67's !important removal is safe on the shipped artifact. One nit: I count 318 declaration blocks, not 253 — counting-method difference, immaterial to #72.

Accessibility

Passing, and better than the mockups in two places: /services and /products fix an h1→h3 skip the design has, and the OSM credit is added where the design has none at all.

  • Focus ring — 3px solid rgb(27,77,143) at outline-offset: 4px. Against the button fill it is 1.02:1, but the positive offset paints the ring entirely outside the button on the page background, where it measures 8.38:1. SC 1.4.11 satisfied; divergence #2 is justified.
  • Every control labelled on all 6 routes, 0 unlabelled. All images have meaningful alt. No heading-level skips. No horizontal overflow on any of the 12 renders. .filter-opt is 44px and wraps its 18×18 checkbox, so the target is 44px. Mobile filter toggle flips aria-expanded correctly.

Two findings:

  1. nav.nav never sets aria-current on any route. The rail does — but the rail is display:none at 1440 and nav.nav is the nav actually visible there, so at desktop width the current page is programmatically unmarked. Both mockups mark both navs.
  2. PDP heading order is h2, h2, h1, h2, h3, h3 — "Spesifikasi" and "Kecocokan kendaraan" precede the product's own <h1> in DOM. Copied verbatim from produk-shockbreaker-avanza.html. Visually the h1 is first, so this is DOM order only; .pdp is already a 2-col grid, so moving .buy-col ahead of .stack-6 with a grid-column swap fixes it with no visual change.

Interactivity on the export — all pass

Search → ?q=shockbreaker (2 cards), ?q=tie at 375 (1 card). Facets 6→2 ?merk=KYB and 6→1 ?ketersediaan=indent; zero-count options in other groups correctly disable ("Indent 0") while a group never narrows its own counts; URLs reload-safe. Stepper floors at 1 and coerces abc/``/0/-5 → 1, 2.5 → 2. kirim/pasang switches and is reflected. Badge 0→1→2, persisted to apja-cart-v1.

WhatsApp checkout is correctly gated behind #co-kota (disabled button + note until filled — worth noting, since a shallow pass reads that as a missing link). The message tracks live state: qty 3→4 moved the total Rp 1.515.000 → Rp 1.965.000 (arithmetic correct), [pasang di tempat] / [indent — pesan dulu] annotations present, note and fulfilment switch both reflected, and .wa-preview matches the link text exactly.

Also verified

The oklch→literal port is pixel-exact — all 8 relative-colour resolutions rasterize byte-identical to the design, including the translucent /0.06, /0.18, /0.72. The single 1/255 delta is tag--indent, from Next's minifier rewriting absolute oklch() to lab() — build step, not the port. The only surviving oklch(from in globals.css is inside the divergence comment. 21 pages export on a 6-row fixture. dev and export agree on every measured key across all 12 renders, with 0 page errors and 0 4xx.

Fix the display, reword the data-volume sentence, and this is a strong port.

.next/out and all fixture processes removed; no tracked file touched.

**request-changes** — rendering/accessibility review of `00eddef`, measured on a real `next build` static export (`out/`) plus a `next dev` cross-check, against the mockups at `~/.od/projects/garage`. Fixture: my own 6-row catalog, and a second **16-row** fixture replicating `produk.html`'s own 16 cards so "it's just data volume" could be tested rather than taken on trust. Almost everything in the PR body holds up under independent measurement. One thing does not, and it is on the homepage. ## Blocker — `.map-media` is an inline `<a>` on `/`, so the map and its ODbL credit fall out of the section into the footer `globals.css:1822` gives `.map-media` `overflow`, `border`, `border-radius` and `aspect-ratio: 4/3` but never `display`. On `/` (`app/page.tsx:177`) that element is an `<a>` inside the plain block `.info-block--map`, so it stays `display: inline` — and `aspect-ratio`, `width`/`height` and `overflow` are all ignored on a non-replaced inline box. Measured on `out/`: | | 1440 | 375 | |---|---|---| | `.map-media` computed `display` | `inline` | `inline` | | content spill past `.info-block--map` | **+72.813px** | **+72.813px** | | credit bottom past `<main>` | +28.813px | +44.813px | | overlaps `.site-footer` | yes | yes | The consequence is the attribution this PR deliberately added. It lands on the footer's `rgb(23,32,24)` while `.map-credit` is `--muted` `rgb(77,89,79)` → **contrast 2.27:1**, failing AA for 12px text. At 375 `elementsFromPoint` at the link's own text position returns a footer `DIV` on top, so the ODbL link is not the hit target. `/contact` escapes only by accident: its anchor carries `style="flex:1;…"` inside a flex parent, which blockifies it. ```suggestion .map-media { display: block; margin: 0; overflow: hidden; border: 1px solid var(--border); border-radius: var(--r2); background: var(--bg-sunk); } ``` Verified in-browser: media height becomes 300px (finally honouring the ported 4/3), spill goes to −3.5px, footer overlap gone at both widths. Since `globals.css` is generated, the fix belongs in `port-design.mjs`. ## On claim 3 — confirm / refute, my numbers | Claim | Measured (app / mockup) | | |---|---|---| | `/services` 615/615 desktop | 615.313 / 615.313 | ✅ | | `/services` 1336/1336 mobile | 1336.25 / 1336.25 | ✅ | | `/contact` 934/934 desktop | 933.672 / 933.672, Δ**0.000** | ✅ | | hero 304px / 32px | 304 / 304, padding 32/32 both | ✅ | | rail 482px | 481.672 / 481.672 | ✅ | | `.section--sunk` `rgb(237,235,229)` | identical, and pixel-identical to the design's `oklch` | ✅ | | product grid `5 x 236.797px` | `236.797 ×4 + 236.812`, identical both sides | ✅ | | PDP `764+420`, 44px price | `764px 420px` gap 48px, price 44px/700 — identical | ✅ | | `/contact` +22px at 375 = OSM credit wrap | **+22.500**; credit holder 16px→38.5px, 1→2 line boxes | ✅ | | "all remaining deltas are fixture volume" | **refuted** — see below | ❌ | **The data-volume test.** Rebuilt with the mockup's own 16 rows: per-card geometry then matched **16/16, zero mismatches**, at both widths. `/products` decomposes cleanly into data and state — sidebar facet cardinality, the mockup's hardcoded active-filter chips, and `Tampilkan 16 produk lagi` (`PAGE = 16`, so `remaining === 0` correctly hides it). That half of the claim is sound. The homepage is not. At 1440 `.section--sunk`'s info-grid is 326.313 vs the mockup's 281.313 — **+45px with no product involved**. Part is the intentional real-photo-for-grey-placeholder swap (disclosed elsewhere in the PR, just not covered by this sentence); the rest is the overflow above. So "the only non-data delta is +22px on `/contact` at 375" is not accurate — please reword it, because it is the sentence that would stop a reader looking at the homepage. ## #72 — answered on built output, not agreed with `out/_next/static/chunks/*.css`, in a browser: - price is `<span class="price card-price">` ✅ - **21.6px / 700 / Archivo** at 1440, 20.8px at 375 ✅ - **largest text in every card** — next largest is `.card-name` at 13px, 8.6px of clearance ✅ - `.card-name` is the only `<p>` in the card body ✅ - **0 `!important`** in the built stylesheet (grep and CSSOM agree) ✅ So #67's `!important` removal is safe on the shipped artifact. One nit: I count **318** declaration blocks, not 253 — counting-method difference, immaterial to #72. ## Accessibility Passing, and better than the mockups in two places: `/services` and `/products` fix an `h1→h3` skip the design has, and the OSM credit is added where the design has none at all. - **Focus ring** — `3px solid rgb(27,77,143)` at `outline-offset: 4px`. Against the button fill it is 1.02:1, but the positive offset paints the ring entirely *outside* the button on the page background, where it measures **8.38:1**. SC 1.4.11 satisfied; divergence #2 is justified. - Every control labelled on all 6 routes, 0 unlabelled. All images have meaningful `alt`. No heading-level skips. No horizontal overflow on any of the 12 renders. `.filter-opt` is 44px and wraps its 18×18 checkbox, so the target is 44px. Mobile filter toggle flips `aria-expanded` correctly. Two findings: 1. **`nav.nav` never sets `aria-current`** on any route. The rail does — but the rail is `display:none` at 1440 and `nav.nav` is the nav actually visible there, so at desktop width the current page is programmatically unmarked. Both mockups mark both navs. 2. **PDP heading order is `h2, h2, h1, h2, h3, h3`** — "Spesifikasi" and "Kecocokan kendaraan" precede the product's own `<h1>` in DOM. Copied verbatim from `produk-shockbreaker-avanza.html`. Visually the h1 is first, so this is DOM order only; `.pdp` is already a 2-col grid, so moving `.buy-col` ahead of `.stack-6` with a `grid-column` swap fixes it with no visual change. ## Interactivity on the export — all pass Search → `?q=shockbreaker` (2 cards), `?q=tie` at 375 (1 card). Facets 6→2 `?merk=KYB` and 6→1 `?ketersediaan=indent`; zero-count options in *other* groups correctly disable ("Indent 0") while a group never narrows its own counts; URLs reload-safe. Stepper floors at 1 and coerces `abc`/``/`0`/`-5` → 1, `2.5` → 2. kirim/pasang switches and is reflected. Badge 0→1→2, persisted to `apja-cart-v1`. WhatsApp checkout is correctly gated behind `#co-kota` (disabled button + note until filled — worth noting, since a shallow pass reads that as a missing link). The message tracks live state: qty 3→4 moved the total `Rp 1.515.000` → `Rp 1.965.000` (arithmetic correct), `[pasang di tempat]` / `[indent — pesan dulu]` annotations present, note and fulfilment switch both reflected, and `.wa-preview` matches the link text exactly. ## Also verified The **oklch→literal port is pixel-exact** — all 8 relative-colour resolutions rasterize byte-identical to the design, including the translucent `/0.06`, `/0.18`, `/0.72`. The single 1/255 delta is `tag--indent`, from Next's minifier rewriting absolute `oklch()` to `lab()` — build step, not the port. The only surviving `oklch(from` in `globals.css` is inside the divergence comment. 21 pages export on a 6-row fixture. dev and export agree on every measured key across all 12 renders, with **0 page errors and 0 4xx**. Fix the `display`, reword the data-volume sentence, and this is a strong port. <sub>`.next`/`out` and all fixture processes removed; no tracked file touched.</sub>
fix(store): action review 75 — the map was an inline box, and its ratio was wrong
All checks were successful
Checks / test garage-store (pull_request) Successful in 3m2s
Checks / test garage (pull_request) Successful in 2m40s
Checks / test @garage/catalog (pull_request) Successful in 5m14s
Checks / typecheck apps/management (pull_request) Successful in 2m10s
Checks / typecheck apps/store (pull_request) Successful in 1m47s
Checks / lint (biome) (pull_request) Successful in 11s
86fdd95977
Review of #86 by the design reviewer: request-changes, 1 blocker, 3 shoulds.
All actioned. It confirmed the geometry claims independently (/services
615.313 both, /contact 933.672 with delta exactly 0.000, hero 304/32, rail
481.672, `.section--sunk` pixel-identical to the design's oklch, grid
236.797x4+236.812, PDP 764+420 with a 44px/700 price), answered #72 on `out/`
the same way, and confirmed the oklch->literal port is pixel-exact across all
8 relative colours.

**BLOCKER: `.map-media` never set `display`.** It is an `<a>`, so it computed
to `display: inline`, and `aspect-ratio`, `width`/`height` and `overflow` are
all ignored on a non-replaced inline box. Measured on the export: the content
spilled 72.813px out of `.info-block--map` at BOTH widths and landed inside
`.site-footer`. The cost fell precisely on the OpenStreetMap attribution this
PR added for licence compliance — it painted `--muted` on the footer's dark
fill at **2.27:1**, failing AA for 12px text, and at 375 a footer div sat on
top of it so the ODbL link was not the hit target. `/contact` escaped only
because its anchor is a flex item and got blockified.

Fixed in the port, since `globals.css` is generated: anything carrying an
aspect-ratio in that block is now explicitly `display: block`. Verified on the
rebuilt export at both widths — spill is now -22.5px (inside the cell), the
credit sits inside `<main>`, `elementsFromPoint` returns the anchor itself, and
the credit reads **6.16:1** on `--bg-sunk`.

**The map ratio was wrong, and the modifier was hiding it.** Chasing the
reviewer's +45px homepage `.section--sunk` delta found the cause: I had set
`.map-media` to 4/3 while the design sizes every `.map-ph` at 16/10. That made
the homepage map 50px taller than the design, and it is why the contact page
needed a `--wide` modifier at all — the modifier existed to undo my mistake.
`.map-media` is now 16/10 like the thing it replaces, `.map-media--wide` is
deleted, and the contact page's compensating inline ratio is gone. The map box
now measures 250px at 1440 and 214.38px at 375 — **delta 0** against the
mockup at both widths.

That also corrects this PR's own claim. Every non-data delta on every page now
has ONE cause: **+22.5px wherever the ODbL credit renders** (homepage
`.section--sunk` at both widths, `/contact` at 375). `/contact` desktop is
933.67/933.67. The reviewer also disproved my broader wording by rebuilding
with a 16-row fixture replicating the mockup's own cards and matching
`/products` 16/16 with zero mismatches — so those deltas really are data.

**Both navs now mark the current page.** `nav.nav` had no `aria-current` on any
route, and it is the nav actually visible at 1440 (the rail is `display: none`
there), so at desktop width the current page was programmatically unmarked. The
design marks both. `nav-rail.tsx` became `site-nav.tsx` with a `variant`, used
twice, rather than duplicating the logic. Verified on `/products`, `/services`,
`/contact` and a PDP at both widths: whichever nav is visible marks the right
item, and `/products/<slug>` marks Produk.

**The PDP outline started at h2.** DOM order was h2 (Spesifikasi), h2
(Kecocokan kendaraan), h1 (the product) — copied verbatim from the mockup, so
a reader navigating by heading met two section headings before the name of the
thing they were looking at. `.buy-col` is now emitted before `.stack-6` with
explicit grid placement, so the outline starts at h1 while the desktop layout
is unchanged: measured stack at x=104 w=764 and buy-col at x=916 w=420, both
y=113, identical to the mockup.

Below 700px the placement is released so they flow in DOM order, which does
change the mobile order — and deliberately. The design stacked the product's
title and price BELOW its spec table on mobile (h1 at y=1233 against
Spesifikasi at y=640, measured on the vendored mockup). That is the same class
of carelessness as the heading order, so this page now leads with the title
and price. It is the only intentional visual divergence, and it is written up
at the rule.

The nit is not actioned: the stylesheet has 318 declaration blocks, not the
253 this PR quoted. Both numbers are real; they count different things
(`cssRules` vs declaration blocks) and the reviewer called it immaterial.

504 tests, both typechecks exit 0, gate PASSED at 242 files, port still
byte-identical on re-run, `next build` exports 21 pages.
Author
Owner

approve — re-reviewed the delta 00eddef..86fdd95 (commits 9a55a53, 86fdd95). Scoped to static correctness: the port script, the gate baseline, the lint suppressions, and the behaviour changes. 0 blockers, 0 shoulds, 1 nit. All seven findings from my earlier pass are actioned, and I re-measured rather than re-read.

The port guards close the class, not the instance. I enumerated every mutation in the script and each is now guarded: resolveRelativeOklch (throws on an unmapped colour, plus a post-check for a surviving oklch(from), the odFocus replace (explicit includes throw immediately before it), the two section-3 substitutions (now substitute()), pageStyles (readFileSync throws on a moved file, if (!m) throw on a missing <style>), and the photography/PDP block (pure append, no anchor to move). No unguarded anchor remains.

The --focus pair assertion earns its place by closing a different class, which I proved separately. Leaving the vendored source pristine so every anchor matched, and deleting only the --focus declaration from the script's own replacement string:

Error: focus ring incomplete: --focus and var(--focus) must both be present
exit=1
  GOOD: no stylesheet emitted

The anchor guard passed and the assertion still caught it — so it defends against script-edit drift, not just source drift. My original scenario now fails correctly too:

Error: anchor moved, refusing to emit a partial stylesheet: "  --container: 1280px;"
exit=1
  GOOD: no stylesheet emitted

Both throws precede writeFileSync, and I confirmed neither run emits a partial stylesheet. substitute() also passes the replacement as a function, which avoids $&/$1 being interpreted in the replacement — a detail worth keeping.

Still idempotent, still lossless. pnpm port-design twice → byte-identical, md5 e60ff16c267dc06aa8e6b574cfcba508. Re-ran the rule-set comparison against the new vendored source: 276 source rules → 307 generated, zero source rule-keys absent, zero appearing fewer times, still exactly two declaration-level divergences (21 oklch resolutions plus the 2 :focus-visible lines), and 0 !important.

The vendoring is faithful. All seven files — design/assets/tokens.css and the six mockups — are byte-identical to ~/.od/projects/garage/. That identity is what makes "the design cannot drift" a real claim rather than a hopeful one.

The biome.json exclusion is the right call, and formatting the vendored copy would be a defect. The five findings reproduce exactly: 1 format in tokens.css, plus 3 noInvalidGridAreas and 1 noDescendingSpecificity from beranda.html's embedded <style> block. Formatting would break the byte-identity above, which is the port's entire correctness argument. It is also now actively hazardous: substitute() matches " --container: 1280px;" with exactly two leading spaces and odFocus as a literal four-line block, so a reformat that touched indentation would convert a cosmetic lint fix into a hard port failure. The noInvalidGridAreas findings are false positives regardless — biome compares cell counts across separate rules, as the script already documents. Precedent sits two lines up in the same ignore list (!lib/api/client), and the gate's MIN_FILES guard is unaffected: still 242 files.

The map correction is right. The design's own .map-ph is aspect-ratio: 16 / 10 in the vendored source, so .map-media matching it is correct and the --wide modifier that existed only to undo 4/3 is gone with no dangling references in CSS or TSX. The display: block is genuinely load-bearing rather than tidiness: .map-media is an <a> at page.tsx:177 and contact/page.tsx:104, so without it the element computes to display: inline, where aspect-ratio, width/height and overflow are all ignored on a non-replaced inline box.

Everything else actioned and confirmed. canInstall={Boolean(product.installService)} now matches its sibling truthiness test. The gate header reads Checked 242 files. Found 32 errors, 6 warnings, 3 infos, which is exactly what I measure. business.ts records that the string appears twice in design/beranda.html and nowhere else, matching my own count. The next/image note is now not merely corrected but accurate about the mechanism, and both it and the debounce comment explicitly record that the earlier claim was wrong — which is the right way to retire a false note, since the next reader would otherwise have no reason to doubt it.

Numbers at this head: 504 tests green (store 110, catalog 24, management 370), both tsc --noEmit exit 0, gate PASSED at 242 files with 41 findings against a baseline of 41.

Nit

The odFocus substitution in section 2 is the only one not routed through substitute(). It is equivalently guarded by the includes check immediately above it, so this is not a defect — but the section-3 comment states "Every substitution below MUST be guarded", and a reader can miss that the one above is guarded by a different mechanism. Routing it through substitute() would make the invariant uniform and checkable at a glance. Non-blocking.

**approve** — re-reviewed the delta `00eddef..86fdd95` (commits `9a55a53`, `86fdd95`). Scoped to static correctness: the port script, the gate baseline, the lint suppressions, and the behaviour changes. 0 blockers, 0 shoulds, 1 nit. All seven findings from my earlier pass are actioned, and I re-measured rather than re-read. **The port guards close the class, not the instance.** I enumerated every mutation in the script and each is now guarded: `resolveRelativeOklch` (throws on an unmapped colour, plus a post-check for a surviving `oklch(from`), the `odFocus` replace (explicit `includes` throw immediately before it), the two section-3 substitutions (now `substitute()`), `pageStyles` (`readFileSync` throws on a moved file, `if (!m) throw` on a missing `<style>`), and the photography/PDP block (pure append, no anchor to move). No unguarded anchor remains. The `--focus` pair assertion earns its place by closing a *different* class, which I proved separately. Leaving the vendored source pristine so every anchor matched, and deleting only the `--focus` declaration from the script's own replacement string: ``` Error: focus ring incomplete: --focus and var(--focus) must both be present exit=1 GOOD: no stylesheet emitted ``` The anchor guard passed and the assertion still caught it — so it defends against script-edit drift, not just source drift. My original scenario now fails correctly too: ``` Error: anchor moved, refusing to emit a partial stylesheet: " --container: 1280px;" exit=1 GOOD: no stylesheet emitted ``` Both throws precede `writeFileSync`, and I confirmed neither run emits a partial stylesheet. `substitute()` also passes the replacement as a function, which avoids `$&`/`$1` being interpreted in the replacement — a detail worth keeping. **Still idempotent, still lossless.** `pnpm port-design` twice → byte-identical, `md5 e60ff16c267dc06aa8e6b574cfcba508`. Re-ran the rule-set comparison against the new vendored source: 276 source rules → 307 generated, **zero source rule-keys absent**, zero appearing fewer times, still exactly two declaration-level divergences (21 `oklch` resolutions plus the 2 `:focus-visible` lines), and 0 `!important`. **The vendoring is faithful.** All seven files — `design/assets/tokens.css` and the six mockups — are byte-identical to `~/.od/projects/garage/`. That identity is what makes "the design cannot drift" a real claim rather than a hopeful one. **The `biome.json` exclusion is the right call, and formatting the vendored copy would be a defect.** The five findings reproduce exactly: 1 `format` in `tokens.css`, plus 3 `noInvalidGridAreas` and 1 `noDescendingSpecificity` from `beranda.html`'s embedded `<style>` block. Formatting would break the byte-identity above, which is the port's entire correctness argument. It is also now actively hazardous: `substitute()` matches `" --container: 1280px;"` with exactly two leading spaces and `odFocus` as a literal four-line block, so a reformat that touched indentation would convert a cosmetic lint fix into a hard port failure. The `noInvalidGridAreas` findings are false positives regardless — biome compares cell counts across separate rules, as the script already documents. Precedent sits two lines up in the same ignore list (`!lib/api/client`), and the gate's `MIN_FILES` guard is unaffected: still 242 files. **The map correction is right.** The design's own `.map-ph` is `aspect-ratio: 16 / 10` in the vendored source, so `.map-media` matching it is correct and the `--wide` modifier that existed only to undo 4/3 is gone with no dangling references in CSS or TSX. The `display: block` is genuinely load-bearing rather than tidiness: `.map-media` is an `<a>` at `page.tsx:177` and `contact/page.tsx:104`, so without it the element computes to `display: inline`, where `aspect-ratio`, `width`/`height` and `overflow` are all ignored on a non-replaced inline box. **Everything else actioned and confirmed.** `canInstall={Boolean(product.installService)}` now matches its sibling truthiness test. The gate header reads `Checked 242 files. Found 32 errors, 6 warnings, 3 infos`, which is exactly what I measure. `business.ts` records that the string appears twice in `design/beranda.html` and nowhere else, matching my own count. The `next/image` note is now not merely corrected but accurate about the mechanism, and both it and the debounce comment explicitly record that the earlier claim was wrong — which is the right way to retire a false note, since the next reader would otherwise have no reason to doubt it. **Numbers at this head:** 504 tests green (store 110, catalog 24, management 370), both `tsc --noEmit` exit 0, gate `PASSED` at 242 files with 41 findings against a baseline of 41. ### Nit The `odFocus` substitution in section 2 is the only one not routed through `substitute()`. It is equivalently guarded by the `includes` check immediately above it, so this is not a defect — but the section-3 comment states "Every substitution below MUST be guarded", and a reader can miss that the one *above* is guarded by a different mechanism. Routing it through `substitute()` would make the invariant uniform and checkable at a glance. Non-blocking.
Author
Owner

approve — re-review of the delta 00eddef..86fdd95, rendering/accessibility scope. Rebuilt a fresh static export against a 6-row and a 16-row fixture, re-measured all six routes at 1440 and 375 against the now-vendored apps/store/design/ mockups (verified byte-identical to ~/.od/projects/garage). My blocker and all three shoulds are cleared, and the numbers are better than the ones claimed.

Blocker — cleared

00eddef 86fdd95
.map-media computed display inline block
spill past .info-block--map +72.813px −3.5px (contained)
credit vs <main> bottom +28.813 / +44.813px past −47.5 / −31.5px inside
overlaps .site-footer yes, both widths no
credit contrast 2.27:1 on footer 6.16:1 on --bg-sunk
ODbL link hit target @375 footer DIV the anchor itself

I hit-tested each of the link's individual client rects rather than the union box (a wrapped inline's union centre falls in the interline gap and gives a false positive): 3 sample points per line box return LINK on / and /contact at both widths, and Playwright reports the link actionable on all four. /contact's attribution measures 16.72:1 on the white panel. One openstreetmap link per map-bearing route, none missing.

Is the fix complete? Yes. I enumerated every element with a computed aspect-ratio !== auto across seven routes at both widths: 41 boxes, zero computing to display: inline. Everything is block, grid or flex. The remaining ratio-not-honoured entries are .thumb in the mobile horizontal card and the 1/1 hero-search button — both stretch-driven, both identical in the mockup, neither a divergence. The comment you left on the rule is the right place for it.

The ratio bug you found chasing my +45px

Confirmed, and it was the larger half. 16/10 is now right on both slots — every media box on both map-bearing pages matches the design exactly:

design app
beranda hero @1440 420×236.25 (16/9) 420×236.25
beranda rail ×2 @1440 379.797×237.359, 379.781×237.359 identical
beranda location map @1440 400×250 (16/10) 400×250
beranda location map @375 343×214.375 343×214.375
kontak map @1440 / @375 558×348.75 / 309×280 identical

Deleting --wide was right: it existed only to undo the 4/3.

The delta story now holds — I retract my "refuted"

Rebuilt with the mockup's own 16 rows so card counts match. beranda @1440, 10 cards both sides:

hero                 304        exact
section (grid)       912.563    exact
section--ink         481.672    exact
section--sunk       +22.500     <- the ODbL credit, and nothing else
mainH               +22.500

Section-by-section, the homepage differs from the design by exactly the credit. /services 0.000 at both widths, /contact 0.000 desktop and +22.500 at 375. Per-card geometry 16/16 matched, 0 mismatched. The residual /products deltas decompose as before into facet cardinality, the mockup's hardcoded active chips, and the load-more button at remaining === 0; beranda's mobile section delta is my fixture's names wrapping differently (2 × 17.547), not a rule. One cause, +22.5px, wherever the credit renders. The rewritten claim is accurate.

Should 2 — nav

nav.nav now marks the current page on every route at both widths: Produk* on /products and the PDP, Layanan* on /services, Kontak* on /contact, nothing on /cart, Beranda* on the rail for /. Rail unchanged. The site-nav.tsx variant collapses two components into one; good trade.

Should 3 — PDP outline, and the mobile reorder

Outline now starts at h1 on every route including the PDP (h1first=true × 6 routes × 2 widths, exactly one h1 each, zero level skips). Desktop placement is pixel-identical to the design — stack-6 at x=104 w=764, buy-col at x=916 w=420, both y=112.5, mirroring the mockup.

On the mobile reorder: keep it. I agree with the call, and I'd defend it on its own merits rather than as a tolerated divergence.

Measured at 375 — design: gallery y=189, Spesifikasi y=640, h1 y=1233, price y=1307, add-to-cart y=1716. App: h1 y=231, price y=304, stepper and kirim/pasang, add-to-cart y=700, plus a sticky price + buy bar. Three reasons the app's order is better:

  1. It is a commerce PDP. Making a phone visitor scroll past a 450px gallery and a full spec table to reach the product's name and price inverts the priority of the page.
  2. It is what keeps the h1-first fix coherent. Forcing the visual order back to spec-first on mobile would put DOM order and reading order in disagreement at 375 — trading a heading-order defect for a WCAG 1.3.2 meaningful-sequence one.
  3. The design's mobile stack is not an authored mobile decision. The mockup has no mobile placement rules; that order is just what one-column flow does with markup whose DOM order was already the thing we agreed was careless. Matching it would mean replicating the artifact of a mistake we just fixed.

Worth flagging to whoever signs off the composition, which the commit already does.

Nit — leave it

318 vs 253 counts different things and it does not bear on #72. No change wanted.

Regression sweep

All six routes × both widths on the fresh export: 0 page errors, 0 4xx, no horizontal overflow, 0 unlabelled controls, 0 missing alt, 0 heading skips. Focus ring unchanged and still 3px solid rgb(27,77,143) at offset: 4px, :focus-visible true, 8.38:1 against the page (7.69:1 on /contact's panel). Built CSS still 0 !important.

Interactivity re-driven after the restructure, since it touched the PDP and both navs: search → ?q=shockbreaker (2 of 16); facet → ?merk=KYB (2), reload-safe; stepper 1→3; pasang selects; badge 0→1→2; checkout still correctly gated on #co-kota, message reflects live state (Rp 1.515.000 → Rp 1.965.000 on +1) and .wa-preview matches the link text exactly.

Blockers 0, shoulds 0, nits 1 (declined). Good fix — the ratio bug was the more valuable of the two and it came out of chasing a number, not a rule.

Reviewed 86fdd95. .next/out and all fixture processes removed; no tracked file touched.

**approve** — re-review of the delta `00eddef..86fdd95`, rendering/accessibility scope. Rebuilt a fresh static export against a 6-row and a 16-row fixture, re-measured all six routes at 1440 and 375 against the now-vendored `apps/store/design/` mockups (verified byte-identical to `~/.od/projects/garage`). My blocker and all three shoulds are cleared, and the numbers are better than the ones claimed. ## Blocker — cleared | | `00eddef` | `86fdd95` | |---|---|---| | `.map-media` computed `display` | `inline` | `block` | | spill past `.info-block--map` | +72.813px | **−3.5px** (contained) | | credit vs `<main>` bottom | +28.813 / +44.813px past | **−47.5 / −31.5px inside** | | overlaps `.site-footer` | yes, both widths | **no** | | credit contrast | 2.27:1 on footer | **6.16:1** on `--bg-sunk` | | ODbL link hit target @375 | footer `DIV` | **the anchor itself** | I hit-tested each of the link's individual client rects rather than the union box (a wrapped inline's union centre falls in the interline gap and gives a false positive): 3 sample points per line box return `LINK` on `/` and `/contact` at both widths, and Playwright reports the link actionable on all four. `/contact`'s attribution measures 16.72:1 on the white panel. One `openstreetmap` link per map-bearing route, none missing. **Is the fix complete?** Yes. I enumerated every element with a computed `aspect-ratio !== auto` across seven routes at both widths: **41 boxes, zero computing to `display: inline`**. Everything is `block`, `grid` or `flex`. The remaining `ratio-not-honoured` entries are `.thumb` in the mobile horizontal card and the 1/1 hero-search button — both stretch-driven, both identical in the mockup, neither a divergence. The comment you left on the rule is the right place for it. ## The ratio bug you found chasing my +45px Confirmed, and it was the larger half. **16/10 is now right on both slots** — every media box on both map-bearing pages matches the design exactly: | | design | app | |---|---|---| | beranda hero @1440 | 420×236.25 (16/9) | 420×236.25 | | beranda rail ×2 @1440 | 379.797×237.359, 379.781×237.359 | identical | | beranda location map @1440 | 400×**250** (16/10) | 400×**250** | | beranda location map @375 | 343×214.375 | 343×214.375 | | kontak map @1440 / @375 | 558×348.75 / 309×280 | identical | Deleting `--wide` was right: it existed only to undo the 4/3. ## The delta story now holds — I retract my "refuted" Rebuilt with the mockup's own 16 rows so card counts match. **beranda @1440, 10 cards both sides:** ``` hero 304 exact section (grid) 912.563 exact section--ink 481.672 exact section--sunk +22.500 <- the ODbL credit, and nothing else mainH +22.500 ``` Section-by-section, the homepage differs from the design by exactly the credit. `/services` 0.000 at both widths, `/contact` 0.000 desktop and +22.500 at 375. Per-card geometry 16/16 matched, 0 mismatched. The residual `/products` deltas decompose as before into facet cardinality, the mockup's hardcoded active chips, and the load-more button at `remaining === 0`; beranda's mobile `section` delta is my fixture's names wrapping differently (2 × 17.547), not a rule. **One cause, +22.5px, wherever the credit renders.** The rewritten claim is accurate. ## Should 2 — nav `nav.nav` now marks the current page on every route at both widths: `Produk*` on `/products` and the PDP, `Layanan*` on `/services`, `Kontak*` on `/contact`, nothing on `/cart`, `Beranda*` on the rail for `/`. Rail unchanged. The `site-nav.tsx` variant collapses two components into one; good trade. ## Should 3 — PDP outline, and the mobile reorder Outline now starts at h1 on every route including the PDP (`h1first=true` × 6 routes × 2 widths, exactly one h1 each, zero level skips). Desktop placement is pixel-identical to the design — `stack-6` at x=104 w=764, `buy-col` at x=916 w=420, both y=112.5, mirroring the mockup. **On the mobile reorder: keep it. I agree with the call, and I'd defend it on its own merits rather than as a tolerated divergence.** Measured at 375 — design: gallery y=189, Spesifikasi y=640, h1 y=**1233**, price y=1307, add-to-cart y=1716. App: h1 y=231, price y=304, stepper and kirim/pasang, add-to-cart y=700, plus a sticky price + buy bar. Three reasons the app's order is better: 1. It is a commerce PDP. Making a phone visitor scroll past a 450px gallery and a full spec table to reach the product's name and price inverts the priority of the page. 2. It is what keeps the h1-first fix coherent. Forcing the visual order back to spec-first on mobile would put DOM order and reading order in disagreement at 375 — trading a heading-order defect for a WCAG 1.3.2 meaningful-sequence one. 3. The design's mobile stack is not an authored mobile decision. The mockup has no mobile placement rules; that order is just what one-column flow does with markup whose DOM order was already the thing we agreed was careless. Matching it would mean replicating the artifact of a mistake we just fixed. Worth flagging to whoever signs off the composition, which the commit already does. ## Nit — leave it 318 vs 253 counts different things and it does not bear on #72. No change wanted. ## Regression sweep All six routes × both widths on the fresh export: **0 page errors, 0 4xx, no horizontal overflow, 0 unlabelled controls, 0 missing `alt`, 0 heading skips.** Focus ring unchanged and still `3px solid rgb(27,77,143)` at `offset: 4px`, `:focus-visible` true, 8.38:1 against the page (7.69:1 on `/contact`'s panel). Built CSS still **0 `!important`**. Interactivity re-driven after the restructure, since it touched the PDP and both navs: search → `?q=shockbreaker` (2 of 16); facet → `?merk=KYB` (2), reload-safe; stepper 1→3; `pasang` selects; badge 0→1→2; checkout still correctly gated on `#co-kota`, message reflects live state (`Rp 1.515.000` → `Rp 1.965.000` on +1) and `.wa-preview` matches the link text exactly. Blockers 0, shoulds 0, nits 1 (declined). Good fix — the ratio bug was the more valuable of the two and it came out of chasing a number, not a rule. <sub>Reviewed `86fdd95`. `.next`/`out` and all fixture processes removed; no tracked file touched.</sub>
fix(store): make port anchors unique, not merely present
All checks were successful
Checks / test @garage/catalog (pull_request) Successful in 1m15s
Checks / test garage-store (pull_request) Successful in 1m19s
Checks / typecheck apps/management (pull_request) Successful in 1m33s
Checks / lint (biome) (pull_request) Successful in 11s
Checks / typecheck apps/store (pull_request) Successful in 1m24s
Checks / test garage (pull_request) Successful in 1m41s
5d2242e080
Both reviewers approved the delta at 86fdd95 — PortAndBehaviour approve (0
blockers, 0 shoulds, 1 nit) and DesignFidelity approve (0 blockers, 0 shoulds,
1 nit declined). This is the port nit plus a latent bug that actioning it
exposed.

The nit: `substitute()` now wraps the focus-ring replacement too, so there are
zero raw `css.replace()` calls left and the "every substitution is guarded"
invariant is uniform instead of true-by-two-mechanisms.

The bug that found: **existence is not uniqueness.** `substitute()` only
asserted the anchor was present, and `String.replace` takes the FIRST match.
`:root {` occurs three times in the design stylesheet — once at the top and
twice inside media queries — so anchoring `color-scheme: light` on it was
ambiguous. Probing the guard with the top-level rule perturbed showed it
"succeed" while injecting into `@media (max-width: 1120px)`, which would have
applied `color-scheme` only below that width: silent, and invisible at desktop.
It resolved correctly today purely because the top-level rule happens to come
first.

Two changes, both about the class rather than the instance:

- `substitute()` now counts matches and throws on 0 ("anchor moved") AND on
  more than 1 ("anchor is ambiguous (N matches), refusing to guess which").
  An ambiguous anchor is a bug even while it resolves correctly.
- The `:root {` anchor is gone. `color-scheme` and `--focus` now both land on
  `  --container: 1280px;`, which is provably unique — the only other
  `--container` is `100%` inside a media query with different indentation and
  value. Placement inside the block is irrelevant to CSS, and a unique anchor
  is worth more than a tidy one.

Verified: `color-scheme` emits at top level (line 58, outside any media
block); a moved container anchor exits 1, a moved focus anchor exits 1, and a
DUPLICATED container anchor now exits 1 with "ambiguous (2 matches)" where it
previously would have silently taken the first. Output is byte-identical to
86fdd95 apart from the one line whose position changed inside `:root`, and the
port is still idempotent.

Review answers worth keeping, both measured by the reviewers rather than
claimed here: the `display: block` fix holds as a class (41 elements with a
computed aspect-ratio across seven routes, zero computing to inline); both map
slots are exact to 0.000 after the 16/10 correction; and the mobile PDP
reorder was endorsed on merit — forcing the design's order back would trade a
heading-order defect for a WCAG 1.3.2 meaningful-sequence one, and that order
was one-column flow over careless DOM rather than an authored decision. It is
still flagged for whoever signs off the composition.

DesignFidelity also retracted its "refuted" on the delta story after rebuilding
with the mockup's own 16 rows: one cause, the ODbL credit, +22.500 wherever it
renders, and nothing else non-data on any page.

504 tests, both typechecks exit 0, gate PASSED at 242 files, export builds 21
pages.
Author
Owner

approve — re-reviewed 5d2242e on its own (86fdd95..5d2242e). Nothing blocks merge. 0 blockers, 0 shoulds, 1 nit.

The uniqueness bug this commit fixes was real, and my earlier "class, not instance" verdict was indeed too generous — substitute() asserted presence, not uniqueness, and :root { occurs 3 times in the vendored stylesheet. I confirmed that count directly. Anchoring color-scheme there resolved correctly only because the top-level rule happens to sort first, which is luck rather than a guarantee.

Every anchor audited for multiplicity, not just the two that changed. Counted on both the raw source and a post-oklch-resolution copy, since substitute() runs after that step:

anchor matches
odFocus block 1
" --container: 1280px;" 1
":root {" (deleted) 3

The container justification is exact: --container appears three times, but the other two are max-width: var(--container); and :root { --section-gap: 36px; --container: 100%; } — different indentation and value, as claimed. resolveRelativeOklch is not an anchor; its balanced-paren loop visits every occurrence by construction, and the if (css.includes("oklch(from")) throw post-check catches any survivor.

split().length - 1 is sound, and provably so rather than incidentally. The general hazard is that split counts non-overlapping occurrences while a self-overlapping anchor has more candidate positions than split reports — "ababab".split("abab") counts 1 although abab sits at index 0 and index 2. That requires the anchor to have a proper border (a prefix that is also a suffix). I measured the longest proper border of both anchors: 0 for each. The container anchor opens with two spaces and closes with ;; odFocus opens with : and closes with }. No self-overlap is possible, so the count is exactly the number of positions replace could pick. split with a string separator also performs no regex interpretation, matching replace's string-pattern semantics, and the function-form replacement keeps $&/$1 inert.

Every throw precedes every write, structurally and empirically. Throws sit at lines 65, 76, 98, 103, 151 and 164; the first write is mkdirSync/writeFileSync at 317-318, with execFileSync at 324. Four negative controls, each exiting 1 and each emitting nothing:

A: moved --container anchor    -> anchor moved, refusing to emit a partial stylesheet
B: moved odFocus block         -> anchor moved, refusing to emit a partial stylesheet
C: duplicated --container      -> anchor is ambiguous (2 matches), refusing to guess which
D: duplicated odFocus block    -> anchor is ambiguous (2 matches), refusing to guess which
E: pristine control            -> reaches the write step (so A-D are the guards, not the sandbox)

For the record: my first run of control D appeared to pass, but that was a defect in my own test — I dropped the newline after {, so the source was never actually duplicated. Corrected, it fires as designed.

Also verified: color-scheme: light lands at line 58 inside the top-level :root, immediately before --container, and is not inside any @media block (checked by brace-depth walk and by regex). globals.css differs from 86fdd95 by exactly one line removed and one added — the same declaration relocated. Idempotent: pnpm port-design twice, byte-identical, md5 e42fddfd838c367c046a671ab596f5f8. Still lossless: 276 source rules → 307 generated, 0 absent, 0 appearing fewer times, 23 declaration changes of which 22 are the oklch resolutions plus the focus ring and 1 is the known .thumb-ph gradient whitespace reformat. 0 !important. Gate PASSED at 242 files with 41 findings against a baseline of 41. Store 110 tests pass, tsc --noEmit exit 0.

Nit

The <style> extraction is now the only anchor left without a multiplicity guard. html.match(/<style>([\s\S]*?)<\/style>/) has no /g, so it takes the first block; beranda.html has exactly one today and the other five mockups have none, so it is correct right now. But it is the same class this commit closes everywhere else — a design revision adding a second <style> block would silently port only the first. if (!m) throw covers zero blocks; nothing covers two. The failure would be visibly broken layout rather than an invisible one, so this is genuinely a nit, but it is the one place the uniqueness invariant holds by luck rather than by check. Two lines: collect with matchAll and throw unless there is exactly one.

**approve** — re-reviewed `5d2242e` on its own (`86fdd95..5d2242e`). Nothing blocks merge. 0 blockers, 0 shoulds, 1 nit. The uniqueness bug this commit fixes was real, and my earlier "class, not instance" verdict was indeed too generous — `substitute()` asserted presence, not uniqueness, and `:root {` occurs **3 times** in the vendored stylesheet. I confirmed that count directly. Anchoring `color-scheme` there resolved correctly only because the top-level rule happens to sort first, which is luck rather than a guarantee. **Every anchor audited for multiplicity, not just the two that changed.** Counted on both the raw source and a post-`oklch`-resolution copy, since `substitute()` runs after that step: | anchor | matches | |---|---| | `odFocus` block | 1 | | `" --container: 1280px;"` | 1 | | `":root {"` *(deleted)* | 3 | The container justification is exact: `--container` appears three times, but the other two are ` max-width: var(--container);` and ` :root { --section-gap: 36px; --container: 100%; }` — different indentation and value, as claimed. `resolveRelativeOklch` is not an anchor; its balanced-paren loop visits every occurrence by construction, and the `if (css.includes("oklch(from")) throw` post-check catches any survivor. **`split().length - 1` is sound, and provably so rather than incidentally.** The general hazard is that `split` counts non-overlapping occurrences while a self-overlapping anchor has more candidate positions than `split` reports — `"ababab".split("abab")` counts 1 although `abab` sits at index 0 and index 2. That requires the anchor to have a proper border (a prefix that is also a suffix). I measured the longest proper border of both anchors: **0** for each. The container anchor opens with two spaces and closes with `;`; `odFocus` opens with `:` and closes with `}`. No self-overlap is possible, so the count is exactly the number of positions `replace` could pick. `split` with a string separator also performs no regex interpretation, matching `replace`'s string-pattern semantics, and the function-form replacement keeps `$&`/`$1` inert. **Every throw precedes every write**, structurally and empirically. Throws sit at lines 65, 76, 98, 103, 151 and 164; the first write is `mkdirSync`/`writeFileSync` at 317-318, with `execFileSync` at 324. Four negative controls, each exiting 1 and each emitting nothing: ``` A: moved --container anchor -> anchor moved, refusing to emit a partial stylesheet B: moved odFocus block -> anchor moved, refusing to emit a partial stylesheet C: duplicated --container -> anchor is ambiguous (2 matches), refusing to guess which D: duplicated odFocus block -> anchor is ambiguous (2 matches), refusing to guess which E: pristine control -> reaches the write step (so A-D are the guards, not the sandbox) ``` For the record: my first run of control D appeared to pass, but that was a defect in my own test — I dropped the newline after `{`, so the source was never actually duplicated. Corrected, it fires as designed. **Also verified:** `color-scheme: light` lands at line 58 inside the top-level `:root`, immediately before `--container`, and is not inside any `@media` block (checked by brace-depth walk and by regex). `globals.css` differs from `86fdd95` by exactly one line removed and one added — the same declaration relocated. Idempotent: `pnpm port-design` twice, byte-identical, `md5 e42fddfd838c367c046a671ab596f5f8`. Still lossless: 276 source rules → 307 generated, 0 absent, 0 appearing fewer times, 23 declaration changes of which 22 are the `oklch` resolutions plus the focus ring and 1 is the known `.thumb-ph` gradient whitespace reformat. 0 `!important`. Gate `PASSED` at 242 files with 41 findings against a baseline of 41. Store 110 tests pass, `tsc --noEmit` exit 0. ### Nit The `<style>` extraction is now the only anchor left without a multiplicity guard. `html.match(/<style>([\s\S]*?)<\/style>/)` has no `/g`, so it takes the first block; `beranda.html` has exactly one today and the other five mockups have none, so it is correct right now. But it is the same class this commit closes everywhere else — a design revision adding a second `<style>` block would silently port only the first. `if (!m) throw` covers zero blocks; nothing covers two. The failure would be visibly broken layout rather than an invisible one, so this is genuinely a nit, but it is the one place the uniqueness invariant holds by luck rather than by check. Two lines: collect with `matchAll` and throw unless there is exactly one.
fix(store): guard the last anchor that held by luck
All checks were successful
Checks / test @garage/catalog (pull_request) Successful in 1m14s
Checks / test garage (pull_request) Successful in 2m1s
Checks / typecheck apps/store (pull_request) Successful in 1m36s
Checks / test garage-store (pull_request) Successful in 1m21s
Checks / typecheck apps/management (pull_request) Successful in 2m20s
Checks / lint (biome) (pull_request) Successful in 11s
1b1e1fece5
PortAndBehaviour approved `5d2242e` with one nit, and it is the right nit: the
`<style>` extraction was the only remaining anchor with no multiplicity check.
A non-global `match` takes the first block, so a design revision adding a
second `<style>` would have silently ported one and dropped the other — the
same class closed everywhere else two commits ago, left holding by luck rather
than by check because beranda.html happens to have exactly one today and the
other five mockups have none.

Now `matchAll`, with both directions guarded: zero blocks throws "no <style>
block in beranda.html", two blocks throws "2 <style> blocks in beranda.html,
refusing to port only the first". Both verified by planting each case; output
byte-identical either side of the change.

That reviewer also checked anchor multiplicity across the whole script rather
than only what I changed, and confirmed `split().length - 1` is sound here for
a reason worth recording: the count only diverges from the number of positions
`replace` could pick when the anchor self-overlaps, which needs a proper
border, and both anchors have a longest proper border of 0 — the container
anchor starts with two spaces and ends `;`, the focus block starts `:` and ends
`}`. So the count is exact, not incidentally right.

504 tests, both typechecks exit 0, gate PASSED at 242 files, export builds.
yuris merged commit a0caee2801 into main 2026-09-04 06:50:20 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
yuris/garage!86
No description provided.