fix(store): rebuild the checkout line so its columns share an axis #91

Merged
yuris merged 6 commits from fix/checkout-line-layout into main 2026-09-06 12:57:50 +00:00
Owner

Reported as messy. I measured the four-line cart before touching anything, and the mess is three separate defects, not a styling problem.

What was actually wrong

1. The quantity stepper floated. .co-controls was a flex row holding both the stepper and the stacked fulfilment pair with align-items: center — so a 46px control was centred against a 138px sibling, sitting 46px down with dead space above and below it. On every line.

2. Unusable width beside the fulfilment pair, and different on every line: 80.1 / 53.0 / 69.3 / 69.3px.

3. Nothing shared a vertical axis. The amount column was auto, and each line is its own grid, so a content-sized track can't match the line above:

before after
amount x 734.9 / 707.8 / 724.0 / 724.0 743 on all four
amount width 128.1 / 155.2 / 139.0 / 139.0 120 on all four
void beside pair 80.1 / 53.0 / 69.3 / 69.3 0
line height 222.5 / 222.5 / 264.5 / 222.5 211.5 / 211.5 / 213 / 211.5

The moving column edge is what reads as messy.

The rebuild

The line is now an explicit four-cell grid — thumb, body and amount on row 1, the fulfilment pair on row 2 indented to the body's axis. Placement is explicit because auto-flow would push whichever child follows a full-width row onto a third row. Markup keeps its reading order: name, quantity, fulfilment, then price and remove.

.co-controls holds only the stepper and the indent tag now, which really are the same height, so centring them is honest. The remove link becomes a block — margin-top always intended that, but inline it sat beside the price and the two read as one string.

The amount track is a fixed 120px: the widest realistic price, Rp 150.000.000, measures 111px at this size.

Two things the fix exposed

The pair's two-up threshold has to be container-driven. It uses repeat(auto-fit, minmax(min(17rem, 100%), 1fr)). 17rem is what one option needs to keep Ongkos pasang dihitung di chat. (209.8px + 56px of radio, gap and padding) on one line. A media query guessed wrong at 900px, where the sticky summary left 172px columns and wrapped Pasang di bengkel (Situbondo) onto three lines. The min(…, 100%) is load-bearing: a bare minmax(17rem, 1fr) still forces a 272px track in a narrower container and overflowed the 229px line at 375. Same idiom kontak.html already uses.

A pre-existing defect worse than anything reported. .checkout grew its 320px summary column at 701px, leaving the main column 309px — narrower than the summary beside it, for all four panels on the page, not just the cart. At 701 that squeezed the line's body track to 23px and wrapped a product name onto ten lines. The summary now waits until 900px. Body width never drops below 174px from 320 to 1600, and the name never exceeds two lines.

.mode-pair itself is untouched, so the product page's buy column still stacks its pair — verified single-column at 386px.

Verification

  • 13 viewports, 320→1600: no horizontal overflow anywhere, amount axis shared on every line at every width, option labels unwrapped from 480 up
  • 42 route × width combinations against the mockups (1440/1120/901/900/701/700/375 × six routes): delta 0 everywhere, and .checkout columns plus .summary position match the mockup at every width including the new 900 boundary — the mockup renders .checkout too, so both sides move together
  • Interactions after the DOM restructure: increment updates price and storage, mode switch flips the line and re-labels a non-shippable item to Ambil di bengkel, remove works, zero page errors
  • 504 tests, both typechecks exit 0, gate PASSED, port idempotent, anchor guard still exits 1, all seven vendored files byte-identical to upstream

Corrections after review

Both reviewers landed findings that changed this PR materially. Recording them here because the original body asserted the opposite.

The premise was false. I claimed no mockup uses .co-line, .co-controls, .co-amount, .mode-pair or .qty. All five were wrong. assets/app.js — loaded by all six mockups, vendored by none — contains coLineMarkup, which renders the checkout page's cart lines. checkout-whatsapp.html is an empty [data-checkout-lines] mount point without it. So the vendored mockups had been 404ing a script since #86, and that absence is exactly what made the premise look true.

Consequently the original change silently restyled the approved design's own line (amount track 138.95→120px, price x 724→765, .co-controls gap 16→12px, remove link inline-block→block), and the headline "42 route × width, delta 0" was hollow on the changed surface — it read delta 0 for .co-* only because that mockup rendered zero lines.

Fixed at the cause: app.js is vendored (0df4accc, 9044 bytes); tokens.css is back to main plus only the .checkout→900px rule, which genuinely belongs in the design; everything else moved to the port's app-additions beside the .pdp > .buy-col precedent.

Proof the design is untouched: with app.js served the mockup renders 3 real lines and its own .co-line is pixel-identical to main — tracks 84px 479.047px 138.953px, amount x724 w139, button inline-block, gap 16px, .switch-row present, .co-fulfil absent.

The track was wrong twice. Rp 150.000.000 is 135.91px, not the 111px I quoted — .co-amount .price is 1.15rem so it renders at 18.4px, and I measured at the inherited 15px, ignoring a rule three lines below the one I was justifying. 120px overflowed from Rp 10.000.000 up, which ten of the fixture's own tyres reach. It is now 8.75rem: a px track holding rem content stops matching when the reader changes font size (three of four prices overflowed at a 20px root; the price painted over the .summary card at 24px). Verified 140 / 175 / 210px at 16 / 20 / 24px roots, no overflow at any.

DOM order flipped. I had kept fulfilment before the amount for destructive-last, and asked to be overruled if the reading went the other way. It did, on a better ground than mine: forward traversal passes every Hapus anyway to reach the next line, so destructive-last bought nothing measurable, while the backwards focus jump was real 4× per cart at both widths. .co-fulfil now follows .co-amount — zero CSS change, since explicit grid-row already pins the visual result. DOM order now equals visual order; the residual step is −22px within row 1, down from −98px across rows.

Accessibility completed. Every per-line name now carries the fulfilment mode: the same product can sit on two lines at once (two PDP clicks; setMode's merge comment proves it is modelled deliberately), and in that state the earlier fix had restored every duplicate. The option group is now a fieldset with an sr-only legend matching the design's own Mau diapakan? grouping, rather than role="radiogroup" as a second convention for one widget.

Also reverted as incidental: .co-controls' gap is back to --s4. And the "margin-top always intended a block" line is gone — a <button> is inline-block and does honour margin-top, so the margin worked and the line box was the problem. Right fix, wrong reason.

Known, pre-existing, not addressed here

Every cart mutation drops focus to <body>, and setMode is remove-then-add so the line jumps to the end of the list. Arrowing through the fulfilment radios therefore commits each option, teleports the line to the bottom, and loses focus. key and setMode are untouched by this PR, so it is not a regression — but it is a larger keyboard defect than either of the two this PR fixed, sitting in the control it touched. Worth its own issue.

Reported as messy. I measured the four-line cart before touching anything, and the mess is **three separate defects**, not a styling problem. ## What was actually wrong **1. The quantity stepper floated.** `.co-controls` was a flex row holding both the stepper *and* the stacked fulfilment pair with `align-items: center` — so a 46px control was centred against a 138px sibling, sitting 46px down with dead space above and below it. On every line. **2. Unusable width beside the fulfilment pair**, and different on every line: **80.1 / 53.0 / 69.3 / 69.3px**. **3. Nothing shared a vertical axis.** The amount column was `auto`, and each line is its own grid, so a content-sized track can't match the line above: | | before | after | |---|---|---| | amount x | 734.9 / 707.8 / 724.0 / 724.0 | **743 on all four** | | amount width | 128.1 / 155.2 / 139.0 / 139.0 | **120 on all four** | | void beside pair | 80.1 / 53.0 / 69.3 / 69.3 | **0** | | line height | 222.5 / 222.5 / **264.5** / 222.5 | 211.5 / 211.5 / 213 / 211.5 | The moving column edge is what reads as messy. ## The rebuild The line is now an explicit four-cell grid — thumb, body and amount on row 1, the fulfilment pair on row 2 indented to the body's axis. Placement is explicit because auto-flow would push whichever child follows a full-width row onto a third row. Markup keeps its reading order: name, quantity, fulfilment, then price and remove. `.co-controls` holds only the stepper and the indent tag now, which really *are* the same height, so centring them is honest. The remove link becomes a block — `margin-top` always intended that, but inline it sat beside the price and the two read as one string. The amount track is a fixed **120px**: the widest realistic price, `Rp 150.000.000`, measures 111px at this size. ## Two things the fix exposed **The pair's two-up threshold has to be container-driven.** It uses `repeat(auto-fit, minmax(min(17rem, 100%), 1fr))`. 17rem is what one option needs to keep `Ongkos pasang dihitung di chat.` (209.8px + 56px of radio, gap and padding) on one line. A media query guessed wrong at 900px, where the sticky summary left 172px columns and wrapped `Pasang di bengkel (Situbondo)` onto three lines. The `min(…, 100%)` is load-bearing: a bare `minmax(17rem, 1fr)` still forces a 272px track in a narrower container and **overflowed the 229px line at 375**. Same idiom `kontak.html` already uses. **A pre-existing defect worse than anything reported.** `.checkout` grew its 320px summary column at **701px**, leaving the main column **309px** — narrower than the summary beside it, for all four panels on the page, not just the cart. At 701 that squeezed the line's body track to **23px** and wrapped a product name onto **ten lines**. The summary now waits until 900px. Body width never drops below 174px from 320 to 1600, and the name never exceeds two lines. `.mode-pair` itself is untouched, so the product page's buy column still stacks its pair — verified single-column at 386px. ## Verification - **13 viewports, 320→1600**: no horizontal overflow anywhere, amount axis shared on every line at every width, option labels unwrapped from 480 up - **42 route × width combinations** against the mockups (1440/1120/901/900/701/700/375 × six routes): delta 0 everywhere, and `.checkout` columns plus `.summary` position match the mockup at every width including the new 900 boundary — the mockup renders `.checkout` too, so both sides move together - **Interactions after the DOM restructure**: increment updates price and storage, mode switch flips the line and re-labels a non-shippable item to `Ambil di bengkel`, remove works, zero page errors - 504 tests, both typechecks exit 0, gate PASSED, port idempotent, anchor guard still exits 1, all seven vendored files byte-identical to upstream ## Corrections after review Both reviewers landed findings that changed this PR materially. Recording them here because the original body asserted the opposite. **The premise was false.** I claimed no mockup uses `.co-line`, `.co-controls`, `.co-amount`, `.mode-pair` or `.qty`. All five were wrong. `assets/app.js` — loaded by all six mockups, **vendored by none** — contains `coLineMarkup`, which renders the checkout page's cart lines. `checkout-whatsapp.html` is an empty `[data-checkout-lines]` mount point without it. So the vendored mockups had been 404ing a script since #86, and that absence is exactly what made the premise look true. Consequently the original change **silently restyled the approved design's own line** (amount track 138.95→120px, price x 724→765, `.co-controls` gap 16→12px, remove link inline-block→block), and the headline "42 route × width, delta 0" was hollow on the changed surface — it read delta 0 for `.co-*` only because that mockup rendered zero lines. Fixed at the cause: `app.js` is vendored (`0df4accc`, 9044 bytes); `tokens.css` is back to `main` plus **only** the `.checkout`→900px rule, which genuinely belongs in the design; everything else moved to the port's app-additions beside the `.pdp > .buy-col` precedent. **Proof the design is untouched:** with app.js served the mockup renders 3 real lines and its own `.co-line` is pixel-identical to `main` — tracks `84px 479.047px 138.953px`, amount x724 w139, button `inline-block`, gap 16px, `.switch-row` present, `.co-fulfil` absent. **The track was wrong twice.** `Rp 150.000.000` is **135.91px**, not the 111px I quoted — `.co-amount .price` is `1.15rem` so it renders at 18.4px, and I measured at the inherited 15px, ignoring a rule three lines below the one I was justifying. 120px overflowed from `Rp 10.000.000` up, which ten of the fixture's own tyres reach. It is now **`8.75rem`**: a px track holding rem content stops matching when the reader changes font size (three of four prices overflowed at a 20px root; the price painted over the `.summary` card at 24px). Verified 140 / 175 / 210px at 16 / 20 / 24px roots, no overflow at any. **DOM order flipped.** I had kept fulfilment before the amount for destructive-last, and asked to be overruled if the reading went the other way. It did, on a better ground than mine: forward traversal passes every `Hapus` anyway to reach the next line, so destructive-last bought nothing measurable, while the backwards focus jump was real 4× per cart at both widths. `.co-fulfil` now follows `.co-amount` — zero CSS change, since explicit `grid-row` already pins the visual result. DOM order now equals visual order; the residual step is −22px within row 1, down from −98px across rows. **Accessibility completed.** Every per-line name now carries the fulfilment mode: the same product can sit on two lines at once (two PDP clicks; `setMode`'s merge comment proves it is modelled deliberately), and in that state the earlier fix had restored every duplicate. The option group is now a `fieldset` with an `sr-only` `legend` matching the design's own `Mau diapakan?` grouping, rather than `role="radiogroup"` as a second convention for one widget. Also reverted as incidental: `.co-controls`' gap is back to `--s4`. And the "`margin-top` always intended a block" line is gone — a `<button>` is inline-block and does honour `margin-top`, so the margin worked and the line box was the problem. Right fix, wrong reason. ## Known, pre-existing, not addressed here Every cart mutation drops focus to `<body>`, and `setMode` is remove-then-add so the line jumps to the end of the list. Arrowing through the fulfilment radios therefore commits each option, teleports the line to the bottom, and loses focus. `key` and `setMode` are untouched by this PR, so it is not a regression — but it is a larger keyboard defect than either of the two this PR fixed, sitting in the control it touched. Worth its own issue.
fix(store): rebuild the checkout line so its columns share an axis
All checks were successful
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 1m18s
Checks / typecheck apps/management (pull_request) Successful in 1m41s
Checks / lint (biome) (pull_request) Successful in 11s
Checks / typecheck apps/store (pull_request) Successful in 1m18s
21751122be
Reported as messy. Measured the four-line cart before touching it, and the
mess is three separate defects rather than a styling problem:

1. The quantity stepper floated. `.co-controls` was a flex row holding both
   the stepper and the stacked fulfilment pair with `align-items: center`, so
   a 46px control was centred against a 138px sibling — the stepper sat 46px
   down with dead space above and below it, on every line.

2. 53-80px of unusable width sat beside the fulfilment pair, and the amount
   differed per line: 80.1 / 53.0 / 69.3 / 69.3px.

3. Nothing shared a vertical axis. The amount column was `auto`, and each
   line is its own grid, so a content-sized track cannot match the line
   above: it measured 128.1 / 155.2 / 139.0 / 139.0px wide at x 734.9 /
   707.8 / 724.0 / 724.0. The column edge moved on every row, which is what
   reads as messy.

The line is now an explicit four-cell grid: thumb, body and amount on row 1,
the fulfilment pair on row 2 indented to the body's axis. Placement is
explicit because auto-flow would push whichever child follows the full-width
row onto a third row. Markup keeps its reading order — name, quantity,
fulfilment, then price and remove.

  amount track   auto -> 120px         (widest realistic price, Rp 150.000.000,
                                        measures 111px at this size)
  amount x       707.8-734.9 -> 743    identical on all four lines
  amount width   128.1-155.2 -> 120    identical on all four lines
  void beside    53.0-80.1 -> 0
  line height    222.5/222.5/264.5/222.5 -> 211.5/211.5/213/211.5

`.co-controls` now holds only the stepper and the indent tag, which really
are the same height, so centring them is honest. The remove link becomes a
block: `margin-top` always intended that, but inline it sat beside the price
and the two read as one string.

## Two things the fix exposed

The fulfilment pair goes two-up via `repeat(auto-fit, minmax(min(17rem,
100%), 1fr))`. 17rem is what one option needs to keep "Ongkos pasang dihitung
di chat." (209.8px plus 56px of radio, gap and padding) on one line. It is
container-driven on purpose: a media query guessed wrong at 900px, where the
sticky summary left 172px columns and wrapped "Pasang di bengkel (Situbondo)"
onto three lines. The `min(…, 100%)` is load-bearing, not decoration — a bare
`minmax(17rem, 1fr)` still forces a 272px track in a narrower container and
overflowed the 229px line at 375. Same idiom kontak.html already uses.

Then a pre-existing defect, worse than anything reported: `.checkout` grew
its 320px summary column at 701px, leaving the main column **309px** —
narrower than the summary beside it, for all four panels on the page. At 701
that squeezed the line's body track to **23px** and wrapped a product name
onto **ten lines**. The summary now waits until 900px, so the main column
keeps full width until there is genuinely room to split it. Body width is
never below 174px at any viewport from 320 to 1600, and the name never
exceeds two lines.

`.mode-pair` itself is untouched, so the product page's buy column still
stacks its pair — verified single-column at 386px.

## Verified

- 13 viewports 320-1600: no horizontal overflow anywhere, amount axis shared
  on every line at every width, option labels unwrapped from 480 up
- 42 route x width combinations against the mockups (1440/1120/901/900/701/
  700/375 x six routes): delta 0 everywhere, and `.checkout` columns plus
  `.summary` position match the mockup at every width including the new 900
  boundary — the mockup renders `.checkout` too, so both sides move together
- interactions after the DOM restructure: increment updates price and
  storage, mode switch flips the line and re-labels a non-shippable item to
  "Ambil di bengkel", remove works, zero page errors
- 504 tests, both typechecks exit 0, gate PASSED, port idempotent, anchor
  guard still exits 1, all seven vendored files byte-identical to upstream
fix(store): name each cart line's remove button and option group
All checks were successful
Checks / test garage (pull_request) Successful in 1m20s
Checks / test garage-store (pull_request) Successful in 1m21s
Checks / typecheck apps/management (pull_request) Successful in 1m36s
Checks / lint (biome) (pull_request) Successful in 12s
Checks / test @garage/catalog (pull_request) Successful in 1m16s
Checks / typecheck apps/store (pull_request) Successful in 1m25s
2e8b275d54
Found while checking my own restructure, before review reached it. Two
defects of the same shape, both pre-existing and both made more visible by
the rebuild.

`Hapus` was the ONLY duplicated accessible name on the page — one per line,
so a screen-reader user pulling up a button list heard "Hapus, Hapus, Hapus"
with nothing to distinguish them. The quantity buttons immediately beside it
already name their line ("Kurangi jumlah Oli TOP 1 GEAR 90"), so this was an
inconsistency inside one component. Now `aria-label="Hapus <name> dari
keranjang"`; the visible text stays short.

The fulfilment pair was an unlabelled group. Each radio takes its name from
its own label, so the options were reachable, but with several lines the page
carries several identical pairs and the user heard "Kirim ke alamat saya,
radio button, 1 of 2" with no way to tell which item it set. Now
`role="radiogroup"` with `aria-label="Pengiriman <name>"`.

Verified in the CDP accessibility tree with three lines: three uniquely named
remove buttons, three uniquely named radio groups, and zero duplicated button
names anywhere on the page (previously exactly one duplicate, "Hapus").
Interaction unaffected — mode switch still flips one line, remove still takes
3 lines to 2.

## Recorded for review rather than decided quietly

Explicit `grid-row` placement means DOM order and visual order now differ by
one cell: DOM is thumb, body, fulfilment, amount; visually the amount sits on
row 1 and the fulfilment below it. So tab order runs name -> quantity ->
options -> remove, and the focus ring jumps back up 98px from the options to
the remove link.

I kept that deliberately. The alternative — amount before fulfilment in DOM —
matches visual order exactly but puts the destructive control in the middle
of the sequence, so a keyboard user setting fulfilment tabs through "Hapus"
on the way. Destructive-last seemed the stronger property, and the order
still preserves meaning: identify the item, set quantity, choose fulfilment,
then remove. It is a judgement call, not a measurement, so a reviewer should
overrule it if they read WCAG 2.4.3 the other way.
fix(store): vendor app.js, and move app-only line layout out of the design
All checks were successful
Checks / test @garage/catalog (pull_request) Successful in 1m17s
Checks / test garage (pull_request) Successful in 1m21s
Checks / test garage-store (pull_request) Successful in 1m22s
Checks / typecheck apps/management (pull_request) Successful in 1m37s
Checks / lint (biome) (pull_request) Successful in 12s
Checks / typecheck apps/store (pull_request) Successful in 1m23s
1a126cd2df
LineSemantics' blocker, and the premise it demolished was mine. I claimed no
mockup used `.co-line`, `.co-controls`, `.co-amount`, `.mode-pair` or `.qty`,
and built on that to edit the design source. All five claims were false.

`assets/app.js` exists in the OpenDesign project and every one of the six
mockups loads it, but it was never vendored — `design/assets/` held only
tokens.css. Its `coLineMarkup` is what renders the checkout page's cart
lines into `[data-checkout-lines]`; `checkout-whatsapp.html` is just an empty
mount point without it. So the vendored mockups have been 404ing a script
since #86, `checkout-whatsapp.html` rendered zero lines, and that absence is
exactly what made "no mockup uses these classes" look true.

Consequences, all measured by the reviewer and reproduced here:

- The PR silently restyled the design's own checkout line: amount track
  138.95 -> 120px, amount x 724 -> 743, price x 724 -> 765, `.co-controls`
  gap 16 -> 12px, remove link inline-block -> block dropping it 22.5px.
- "42 route x width, delta 0 everywhere" was hollow on the changed surface.
  It read delta 0 for `.co-*` only because that mockup rendered no lines.
  `.checkout`/`.summary` were genuinely compared; the classes this PR
  rebuilt could not have been.

Fixed at the cause rather than by relabelling:

1. `assets/app.js` is now vendored, byte-identical to upstream (0df4accc,
   9044 bytes), so the mockups open as-is and the parity check can actually
   see a cart line. The port header said they "reference assets/tokens.css
   relatively" — corrected to name both assets, and to record why the gap
   mattered.
2. `tokens.css` is back to `main` plus one rule: `.checkout` stacking to
   900px. That one belongs in the design, because `.checkout` and `.summary`
   are in the mockup's static markup and the 309px-main-beside-320px-summary
   defect reproduces in the design itself. Verified `.co-*` and `.mode-pair`
   blocks are byte-identical to main.
3. Everything else moved to the port's app additions, next to the `.pdp >
   .buy-col` placement that set the precedent: the four-cell placement, the
   amount track, the remove-link block, `.mode-pair--row`, and the mobile
   overrides. The app's line legitimately differs — it needs a fourth cell
   for the `.mode-pair` radios because it has a case the mockup's
   `.switch-row` checkbox cannot express.

Proof the design is untouched: with app.js served, the mockup renders 3 real
lines and its own `.co-line` is pixel-identical between main and this branch
— tracks `84px 479.047px 138.953px`, amount x724 w139, button inline-block at
x825.6, `.co-controls` gap 16px, `.switch-row` present, `.co-fulfil` absent.

## The track was wrong, twice

LineGeom caught that `Rp 150.000.000` is 135.91px, not the 111px I quoted:
`.co-amount .price` is `1.15rem`, so it renders at 18.4px, and I had measured
at the inherited 15px — ignoring a rule three lines below the one I was
justifying. 120px overflowed at any total from Rp 10.000.000 up, which ten of
the fixture's own tyres reach (Rp 11.100.000).

It is now `8.75rem`, and rem rather than px is the substance, not tidiness. A
px track holding rem content stops matching as soon as the reader changes
font size: the reviewer measured three of four prices overflowing at a 20px
root and the price painting over the `.summary` card at 24px. Verified after:
the track is 140 / 175 / 210px at 16 / 20 / 24px roots and Rp 11.100.000
stays inside the panel padding at all three.

## DOM order flipped, and their argument is why

I had kept fulfilment before the amount so the destructive control came last,
and asked for that to be overruled if the reading went the other way. It did,
on a better ground than I had: forward traversal passes every `Hapus` anyway
on the way to the next line, so destructive-last bought nothing measurable,
while the backwards focus jump was real 4x per cart at both widths.

`.co-fulfil` now follows `.co-amount` in the JSX — zero CSS change, because
explicit `grid-row` already pins the visual result. DOM order now equals
visual order. The residual backwards step is -22px, qty "+" to "Hapus", both
inside row 1, down from -98px across rows.

## Their other findings

- Every per-line accessible name now carries the fulfilment mode, because the
  same product can sit on two lines at once — reachable in two PDP clicks,
  and `setMode`'s merge comment proves the app models it deliberately. With
  one product in both modes the previous commit's fix had restored every
  duplicate. Verified zero duplicates in that exact state.
- The option group is a `fieldset` with a screen-reader-only `legend`,
  matching `produk-shockbreaker-avanza.html`'s own `Mau diapakan?` grouping
  and `add-to-cart.tsx` which mirrors it, instead of introducing
  `role="radiogroup"` as a second convention for one widget. The legend
  borrows the design's wording rather than my "Pengiriman", which was wrong
  anyway — two of the three options are collection and installation.
- `.co-controls`' gap is back to `--s4`; tightening it was incidental and it
  was one of the six things restyling the design.
- The "`margin-top` always intended a block" line is gone: a `<button>` is
  inline-block and does honour margin-top, so the margin worked and the line
  box was the problem. Right fix, wrong reason.
- The drift note now lists what tokens.css actually still carries, and
  records app.js as vendored, unedited, and restorable wholesale.

504 tests, both typechecks exit 0, gate PASSED, port idempotent, anchor guard
exits 1, all eight vendored files byte-identical to upstream.
fix(store): scope the desktop line template so it stops beating the mobile one
All checks were successful
Checks / test @garage/catalog (pull_request) Successful in 1m15s
Checks / test garage (pull_request) Successful in 1m26s
Checks / test garage-store (pull_request) Successful in 1m17s
Checks / lint (biome) (pull_request) Successful in 12s
Checks / typecheck apps/store (pull_request) Successful in 1m20s
Checks / typecheck apps/management (pull_request) Successful in 1m39s
89a4d0648c
LineGeom's regression, and it is the trap my own comment described two lines
above the bug. The appended app additions land after tokens.css, so at equal
specificity they win — including inside tokens.css's `max-width: 700px`
block, because a media query adds no specificity. I restated `.co-amount`,
`.co-amount button` and `.co-fulfil` for exactly that reason and missed
`.co-line`'s own template.

Measured at 320px, root 16: `.co-line` computed `84px 0px 140px`. A
ZERO-width body track, an 84px thumb where the mockup uses 64px, and a 140px
third track held open for an amount cell my surviving override had already
moved to column 2 — so it was purely subtractive. Product names wrapped to
5 / 4 / 3 / 7 lines against a maximum of 2 before. At a 20px root four
widths overflowed, at 24px thirty-nine widths overflowed by up to 39px: real
horizontal scroll on a phone. The clincher was that at 700px the mockup
rendered `64px 554px` while the app rendered `84px 378px 140px` from the same
stylesheet lineage, differing only by append order.

Fixed by scoping rather than by a fourth restatement: the desktop placement
now sits in `@media (min-width: 701px)`, so it cannot reach the mobile branch
and the obligation to remember disappears for that declaration. Below 701 the
mockup's own two-track line applies unopposed and only what it has no rule
for is added — the amount's row, the fulfilment row, the remove link's
margin. `display: block` on the remove link stays unconditional.

Verified across 7 widths x 3 root sizes (21 combinations): 320 is back to
`64px 174px` with a 174px body, 700 matches the mockup's `64px 554px`, 701
picks up `84px 347px 140px`, the amount axis is shared on every line at every
combination, and the price never spills its panel at a 16px or 20px root.

## One residual, measured and not mine

At a 24px root the page overflows at 320px and 901px. I checked whether my
mobile rules cause it by neutralising them: the overflow persists
(scrollWidth 353 against clientWidth 320), and no element inside `.co-line`
overflows at all. The single offender is the summary panel's own total —
`.price` reading `Rp 13.070.000` at 261px. Pre-existing, unrelated to the
cart line, and a genuine WCAG 1.4.10 reflow failure at that zoom level.
Filed separately rather than widened into this PR.

LineGeom also confirmed on this branch: the mockup's `.co-line` is back to
main exactly (`84px 479.047px 138.953px`, amount x724.05 / w138.95, button
inline-block, gap 16px, 64px thumb below 701), app.js vendored at 9044 bytes
with no 404, and 42 route x width parity rows with zero mismatches. And they
agree with dropping the `display: contents` / `subgrid` option — the border
and padding `.co-line` carries would have to be reconstructed across a cell
count that differs between the two branches, which is more moving parts than
the one number it deletes.

504 tests, both typechecks exit 0, gate PASSED, port idempotent.
refactor(store): one placement declaration instead of a per-width block
All checks were successful
Checks / typecheck apps/management (pull_request) Successful in 1m41s
Checks / lint (biome) (pull_request) Successful in 12s
Checks / typecheck apps/store (pull_request) Successful in 1m20s
Checks / test @garage/catalog (pull_request) Successful in 1m13s
Checks / test garage (pull_request) Successful in 1m25s
Checks / test garage-store (pull_request) Successful in 1m19s
6c09a7f658
LineSemantics' remaining should, and their measurement generalised further
than the rule they aimed at.

They showed `.co-body`'s placement earns nothing: forced to `auto`, every
cell box is byte-identical at 320 / 375 / 700 / 701 / 1440. Testing the same
way took three of the four with it. Now that the markup order is thumb,
body, amount, fulfilment — the order they read in, after the DOM flip
earlier in this branch — auto-flow puts the first three in row 1 unaided at
both breakpoints. Verified byte-identical cell boxes at 320 / 375 / 700 /
701 / 900 / 1440 with all placement dropped except one.

So the whole `@media (min-width: 701px)` placement block plus its mobile
counterpart collapse to:

  .co-fulfil { grid-column: 2 / -1; }

That is the only cell auto-flow genuinely cannot express, and `2 / -1` reads
as "indented to the body, out to the end" at two tracks or three without
knowing which. The track itself stays scoped to `min-width: 701px` because it
must not reach the mobile branch.

`.co-body` is gone from the markup too, so the app's second cell is unclassed
like the `coLineMarkup` cell it mirrors. Their argument for cutting rather
than hoisting is the one I'd already written into the cascade fix: remove the
obligation instead of adding a thing to remember. The explicit-at-desktop,
auto-at-mobile split was two mental models by width, and that asymmetry is
what produced the cascade bug in the first place.

## And the breakpoint now tracks the reader's font size

Sweeping 57 width x root combinations left one outlier: at a 24px root and
exactly 901px the body track is 133px and a product name wraps onto six
lines. No overflow, axis shared, order monotonic — but it is the same defect
class as the 701px one this branch already fixed, the summary column
appearing before there is room for it.

`@media (max-width: 900px)` is now `56.25em`. Identical at the default 16px.
The mechanism is narrower than "it scales", and I nearly shipped that
sentence without checking: a media-query `em` resolves against the browser's
INITIAL font size, so `html { font-size }` set inline does not move it —
which is why my first sweep saw no change and I had to measure it properly.
What it does track is a reader who raises their browser's default font size.
Measured with `Page.setFontSizes` at 24px, viewport 901: the em query matches
where the px one does not, the row stays single-column, and the line body
gets 477px instead of 133px. Page zoom needs no help, since it shrinks the
CSS-px viewport and a width query already follows it.

So the two font-size defences are complementary rather than redundant: the
`8.75rem` track handles content growing inside a fixed row, and the `56.25em`
query handles the row itself splitting too early.

504 tests, both typechecks exit 0, gate PASSED, port idempotent, all eight
vendored files byte-identical to upstream.
fix(store): floor the checkout breakpoint so a small default font can't split early
All checks were successful
Checks / typecheck apps/store (pull_request) Successful in 1m24s
Checks / test @garage/catalog (pull_request) Successful in 1m18s
Checks / test garage (pull_request) Successful in 1m34s
Checks / test garage-store (pull_request) Successful in 1m21s
Checks / typecheck apps/management (pull_request) Successful in 1m42s
Checks / lint (biome) (pull_request) Successful in 12s
2a01fbdaf7
LineGeom's last finding, and it is the same defect from the opposite
direction. The em query I added scales the split upward with the reader's
font size, but `.summary`'s own track is still px — so it also scales
DOWNWARD. At a 12px default, which is Chrome's "Small", the row split at
676px against a 320px summary, leaving a 38px body track and a product name
on seven lines. The px query was safe there; making it em opened that side.

`@media (max-width: 56.25em), (max-width: 900px)` — the 900px arm is a floor,
not a duplicate, so em can only ever delay the split and never advance it.
Comma-separated media query lists are CSS2, so this costs no modern-CSS
floor.

Verified with `Page.setFontSizes` across six defaults:

  default  splits at  body track  max name
  12px     901        238.0       4
  13px     901        229.3       4
  14px     901        220.5       4
  16px     901        203.0       4
  20px     1126       289.0       2
  24px     1351       408.0       2

The three small defaults move from splitting at 676 / 732 / 789 to 901, and
16 / 20 / 24 come out unchanged. The minimum body track is now monotone in
font size in both directions, which it was not before.

Their alternative — making the summary track rem — was measured and
rejected: it fixes 12px but the summary then grows with the font, so the body
loses 289 -> 189px at 20px and 408 -> 208px at 24px. Recorded in the comment
so the next person does not retry it.

Also noted from their sweep: my "seven lines" and their "three" for the same
defect are both right — magnitude varies with which products are in the cart,
as with the earlier seven-versus-ten. The defect class is the cramped body,
not a specific line count.

Both reviewers now approve with nothing outstanding. Their sweeps on the
previous head: 3843 measurements over 320-1600 x three font sizes with the
placement invariant holding pointwise, app-vs-mockup `.co-line` identical at
all 381 widths from 320 to 700, and the mockup's own line unchanged from main.

504 tests, both typechecks exit 0, gate PASSED, port idempotent, all eight
vendored files byte-identical to upstream.
yuris merged commit 7e31e02876 into main 2026-09-06 12:57:50 +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!91
No description provided.