feat(store): rebuild the storefront to match its approved design #86
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "design/od-alignment"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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:
--ink, 482px, 2 photo cards--dark, 210px, 2 buttonsApproach
Reconciling rule-by-rule would drift again, so the design's own stylesheet is now the source of truth.
apps/store/scripts/port-design.mjsports~/.od/projects/garage/assets/tokens.cssintoapp/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.cssshould not be hand-edited.Two forced divergences from that source, both recorded in the generated file:
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 asrgb(r g b / a).--focus, not the design's2px solid var(--accent)— on an accent-filled.btn--primarythat 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: covercrops the notice burned into the raster.Verification
Per page, at 1440 and 375, by measurement:
/services615/615 desktop and 1336/1336 mobile;/contact934/934 desktop — exact..section--sunkrgb(237,235,229), product grid5 x 236.797px, PDP764+420with a 44px price — all identical to the mockups./contactat 375, where the OSM credit wraps to a second line.?kategori=ban.504 tests pass (24 + 370 + 110), both typechecks exit 0,
next buildexports 21 pages.#67 / #72 — read this bit
The
!importantthat #67 added to hold.product-typeabove 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!importantacross all 253 rules. If a future revision puts the price back in a<p>, this returns; re-read #67.Also in here
next/imagewith explicit dimensions (images.unoptimizedis already set, so it works in the export and costs nothing). The four remaining raw<img>are catalog photography and carrybiome-ignores naming why: their URLs come fromR2_PUBLIC_URL, an env var with no build-time hostname, sonext/imagecannot allowlist them.app/category-tiles.tsxdeleted —.cat-grid/.cat-tileappear in no current mockup, andproduk.html's category entry point is the filter group.findServiceresolving.installService != nullis the shop's assertion from the management app;lib/services.tsis 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.tsgainshoursRangeandsameDayCutoff.Four rules leave the biome-gate
BASELINEbecause they no longer exist, which the gate requires be recorded in the commit that eliminates them:noImportantStylesandnoDescendingSpecificity(the two flagged as load-bearing — see the rewritten note there),noImgElement, andsuppressions/unused. Gate re-verified both ways: PASSED at this head, FAILED on purpose against a planteddebugger.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.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:
globals.cssis generated and idempotentmd5 46f58df96d6f8e10b19dde2811e07b42, matching the committed file.oklch(from …)resolutions plus the 2:focus-visiblelines. Everything else differs by formatter whitespace only.#e4f3e9vs#e4f3e8, rounding).--ink/--bgalpha forms23 32 24/247 245 239are exact.tsc --noEmitexit 0, no output.The four removed
BASELINErules — ruled on independently. First I closed the trap that would make all four read zero for the same wrong reason: all 6parseerrors areapps/management/app/globals.css, and plantingcolor: red !importantinto the store's stylesheet raiseslint/complexity/noImportantStylesimmediately. biome fully lints the new file, so the zeroes are real.noImportantStyles— genuinely gone.apps/storecontains zero!importantin any.cssor.tsx; postcss confirms 0 important declarations across the stylesheet. I verified the cascade claim statically too:.card-priceis1.35rem(= 21.6px) and.pricecarriesfont-weight: 700, against.card-nameat 13px/500 and.card-brandat 11px; the onlypselectors in the file are.rail-card pand.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 withFAIL: 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.htmlcontains bothOrder sebelum 15.00 WIB dikirim hari ituand<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.tsandlib/cart.tsare untouched. The cart still keys onsameLine(slug, installAtShop), and everysetQty/removein the re-skinned view passes both, with the React key and the radiogroupname both including the mode — kirim and pasang stay distinct, andcart.test.tsalready covers that invariant. All fourFACET_KEYSplusqround-trip:qenters through the two native GET forms to/productsand is read back by the unchangedselectionFromParams.ProductCardtakes exactly one prop at all four call sites. ThefindServicedecoupling is right — capability keys on the rawinstallServicefield, 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, andString.replacewith 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:Green. But
--focusis now absent from:root, while the output still emits:var(--focus)with--focusundefined is invalid at computed-value time, sooutlinefalls 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: lightsurvives only because:root {still matches, which isolates the fault to the--containeranchor.Two lines, matching the guard style already used three times in the same file:
2. The
next/imagejustification is false as written. It appears in fourbiome-ignorecomments 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 whenunoptimizedis set, sodefaultLoader— the only placehasRemoteMatchis ever called — never runs. That check is additionallyNODE_ENV !== 'production'gated and only fires whendomains || remotePatternsis configured. Soimages: { unoptimized: true }removes hostname validation; it does not add a failure mode, andnext/imagewould in fact accept an arbitraryR2_PUBLIC_URL-derived absolute URL untouched.The decision to keep raw
<img>is still defensible — underunoptimizedthe 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.unoptimizedis on for static export, sonext/imageperforms no optimization here and adds only a wrapper.3. Regeneration is not reproducible by anyone else.
SRCis~/.od/projects/garage/assets/tokens.css;git ls-filesfinds 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 inpackage.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:331testsproduct.installService !== nullwhile 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 infosimmediately above aBroken 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.tsattributes the cutoff to "(beranda/kontak)";kontak.htmlcontains no occurrence — it is beranda only.7.
product-browser.tsx:101-105justifies the 200 ms debounce by "the search box fires one change per keystroke", but there is no search input left in that component —qnow 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.
request-changes — rendering/accessibility review of
00eddef, measured on a realnext buildstatic export (out/) plus anext devcross-check, against the mockups at~/.od/projects/garage. Fixture: my own 6-row catalog, and a second 16-row fixture replicatingproduk.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-mediais an inline<a>on/, so the map and its ODbL credit fall out of the section into the footerglobals.css:1822gives.map-mediaoverflow,border,border-radiusandaspect-ratio: 4/3but neverdisplay. On/(app/page.tsx:177) that element is an<a>inside the plain block.info-block--map, so it staysdisplay: inline— andaspect-ratio,width/heightandoverfloware all ignored on a non-replaced inline box.Measured on
out/:.map-mediacomputeddisplayinlineinline.info-block--map<main>.site-footerThe consequence is the attribution this PR deliberately added. It lands on the footer's
rgb(23,32,24)while.map-creditis--mutedrgb(77,89,79)→ contrast 2.27:1, failing AA for 12px text. At 375elementsFromPointat the link's own text position returns a footerDIVon top, so the ODbL link is not the hit target./contactescapes only by accident: its anchor carriesstyle="flex:1;…"inside a flex parent, which blockifies it.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.cssis generated, the fix belongs inport-design.mjs.On claim 3 — confirm / refute, my numbers
/services615/615 desktop/services1336/1336 mobile/contact934/934 desktop.section--sunkrgb(237,235,229)oklch5 x 236.797px236.797 ×4 + 236.812, identical both sides764+420, 44px price764px 420pxgap 48px, price 44px/700 — identical/contact+22px at 375 = OSM credit wrapThe data-volume test. Rebuilt with the mockup's own 16 rows: per-card geometry then matched 16/16, zero mismatches, at both widths.
/productsdecomposes cleanly into data and state — sidebar facet cardinality, the mockup's hardcoded active-filter chips, andTampilkan 16 produk lagi(PAGE = 16, soremaining === 0correctly 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/contactat 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:<span class="price card-price">✅.card-nameat 13px, 8.6px of clearance ✅.card-nameis the only<p>in the card body ✅!importantin the built stylesheet (grep and CSSOM agree) ✅So #67's
!importantremoval 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:
/servicesand/productsfix anh1→h3skip the design has, and the OSM credit is added where the design has none at all.3px solid rgb(27,77,143)atoutline-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.alt. No heading-level skips. No horizontal overflow on any of the 12 renders..filter-optis 44px and wraps its 18×18 checkbox, so the target is 44px. Mobile filter toggle flipsaria-expandedcorrectly.Two findings:
nav.navnever setsaria-currenton any route. The rail does — but the rail isdisplay:noneat 1440 andnav.navis the nav actually visible there, so at desktop width the current page is programmatically unmarked. Both mockups mark both navs.h2, h2, h1, h2, h3, h3— "Spesifikasi" and "Kecocokan kendaraan" precede the product's own<h1>in DOM. Copied verbatim fromproduk-shockbreaker-avanza.html. Visually the h1 is first, so this is DOM order only;.pdpis already a 2-col grid, so moving.buy-colahead of.stack-6with agrid-columnswap fixes it with no visual change.Interactivity on the export — all pass
Search →
?q=shockbreaker(2 cards),?q=tieat 375 (1 card). Facets 6→2?merk=KYBand 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 coercesabc/``/0/-5→ 1,2.5→ 2. kirim/pasang switches and is reflected. Badge 0→1→2, persisted toapja-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 totalRp 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-previewmatches 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 istag--indent, from Next's minifier rewriting absoluteoklch()tolab()— build step, not the port. The only survivingoklch(frominglobals.cssis 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/outand all fixture processes removed; no tracked file touched.approve — re-reviewed the delta
00eddef..86fdd95(commits9a55a53,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 survivingoklch(from), theodFocusreplace (explicitincludesthrow immediately before it), the two section-3 substitutions (nowsubstitute()),pageStyles(readFileSyncthrows on a moved file,if (!m) throwon a missing<style>), and the photography/PDP block (pure append, no anchor to move). No unguarded anchor remains.The
--focuspair 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--focusdeclaration from the script's own replacement string: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:
Both throws precede
writeFileSync, and I confirmed neither run emits a partial stylesheet.substitute()also passes the replacement as a function, which avoids$&/$1being interpreted in the replacement — a detail worth keeping.Still idempotent, still lossless.
pnpm port-designtwice → 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 (21oklchresolutions plus the 2:focus-visiblelines), and 0!important.The vendoring is faithful. All seven files —
design/assets/tokens.cssand 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.jsonexclusion is the right call, and formatting the vendored copy would be a defect. The five findings reproduce exactly: 1formatintokens.css, plus 3noInvalidGridAreasand 1noDescendingSpecificityfromberanda.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 andodFocusas a literal four-line block, so a reformat that touched indentation would convert a cosmetic lint fix into a hard port failure. ThenoInvalidGridAreasfindings 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'sMIN_FILESguard is unaffected: still 242 files.The map correction is right. The design's own
.map-phisaspect-ratio: 16 / 10in the vendored source, so.map-mediamatching it is correct and the--widemodifier that existed only to undo 4/3 is gone with no dangling references in CSS or TSX. Thedisplay: blockis genuinely load-bearing rather than tidiness:.map-mediais an<a>atpage.tsx:177andcontact/page.tsx:104, so without it the element computes todisplay: inline, whereaspect-ratio,width/heightandoverfloware 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 readsChecked 242 files. Found 32 errors, 6 warnings, 3 infos, which is exactly what I measure.business.tsrecords that the string appears twice indesign/beranda.htmland nowhere else, matching my own count. Thenext/imagenote 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 --noEmitexit 0, gatePASSEDat 242 files with 41 findings against a baseline of 41.Nit
The
odFocussubstitution in section 2 is the only one not routed throughsubstitute(). It is equivalently guarded by theincludescheck 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 throughsubstitute()would make the invariant uniform and checkable at a glance. Non-blocking.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-vendoredapps/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
00eddef86fdd95.map-mediacomputeddisplayinlineblock.info-block--map<main>bottom.site-footer--bg-sunkDIVI 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
LINKon/and/contactat both widths, and Playwright reports the link actionable on all four./contact's attribution measures 16.72:1 on the white panel. Oneopenstreetmaplink per map-bearing route, none missing.Is the fix complete? Yes. I enumerated every element with a computed
aspect-ratio !== autoacross seven routes at both widths: 41 boxes, zero computing todisplay: inline. Everything isblock,gridorflex. The remainingratio-not-honouredentries are.thumbin 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:
Deleting
--widewas 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:
Section-by-section, the homepage differs from the design by exactly the credit.
/services0.000 at both widths,/contact0.000 desktop and +22.500 at 375. Per-card geometry 16/16 matched, 0 mismatched. The residual/productsdeltas decompose as before into facet cardinality, the mockup's hardcoded active chips, and the load-more button atremaining === 0; beranda's mobilesectiondelta 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.navnow marks the current page on every route at both widths:Produk*on/productsand the PDP,Layanan*on/services,Kontak*on/contact, nothing on/cart,Beranda*on the rail for/. Rail unchanged. Thesite-nav.tsxvariant 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-6at x=104 w=764,buy-colat 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:
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 still3px solid rgb(27,77,143)atoffset: 4px,:focus-visibletrue, 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;pasangselects; badge 0→1→2; checkout still correctly gated on#co-kota, message reflects live state (Rp 1.515.000→Rp 1.965.000on +1) and.wa-previewmatches 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/outand all fixture processes removed; no tracked file touched.approve — re-reviewed
5d2242eon 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. Anchoringcolor-schemethere 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, sincesubstitute()runs after that step:odFocusblock" --container: 1280px;"":root {"(deleted)The container justification is exact:
--containerappears three times, but the other two aremax-width: var(--container);and:root { --section-gap: 36px; --container: 100%; }— different indentation and value, as claimed.resolveRelativeOklchis not an anchor; its balanced-paren loop visits every occurrence by construction, and theif (css.includes("oklch(from")) throwpost-check catches any survivor.split().length - 1is sound, and provably so rather than incidentally. The general hazard is thatsplitcounts non-overlapping occurrences while a self-overlapping anchor has more candidate positions thansplitreports —"ababab".split("abab")counts 1 althoughababsits 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;;odFocusopens with:and closes with}. No self-overlap is possible, so the count is exactly the number of positionsreplacecould pick.splitwith a string separator also performs no regex interpretation, matchingreplace's string-pattern semantics, and the function-form replacement keeps$&/$1inert.Every throw precedes every write, structurally and empirically. Throws sit at lines 65, 76, 98, 103, 151 and 164; the first write is
mkdirSync/writeFileSyncat 317-318, withexecFileSyncat 324. Four negative controls, each exiting 1 and each emitting nothing: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: lightlands at line 58 inside the top-level:root, immediately before--container, and is not inside any@mediablock (checked by brace-depth walk and by regex).globals.cssdiffers from86fdd95by exactly one line removed and one added — the same declaration relocated. Idempotent:pnpm port-designtwice, byte-identical,md5 e42fddfd838c367c046a671ab596f5f8. Still lossless: 276 source rules → 307 generated, 0 absent, 0 appearing fewer times, 23 declaration changes of which 22 are theoklchresolutions plus the focus ring and 1 is the known.thumb-phgradient whitespace reformat. 0!important. GatePASSEDat 242 files with 41 findings against a baseline of 41. Store 110 tests pass,tsc --noEmitexit 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.htmlhas 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) throwcovers 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 withmatchAlland throw unless there is exactly one.