Round #46Score 4.5/5Retail / e-commerce

ScanInput shipped on its third sighting while two earlier bets paid off

Route: /receiving
Desktop preview
Mobile preview

Captured at a 375px viewport. Long pages scroll inside the frame.

The screenshots stay in Japanese. Every round is a real screen for a real Japanese industry, so an English re-render would no longer be the screen the agent actually produced.

Write-up

ScanInput shipped on its third sighting while two earlier bets paid off

⁠Build log series⁠: I hand a real screen to an AI that has never seen Gunjo (@gunjo/ui) and let it build. Retail screen five: ⁠receiving inspection⁠ (purchase-order matching, scanning items in one by one, expected against received, shortages, overages, and damage, lots and expiration dates, posting to inventory).

Still running the same policy: three to five screens per industry, deliberately unalike, industry-specific UI first. After selling (), counting (), closing (), and rewinding (), this round accepts the goods: inspecting a delivery in the back room and posting it to stock, the inbound side of retail logistics. Five screens in, the industry verdict finally has depth.

Result: 4.5/5

tsc and build passed, the console stayed clear, 375px (a handheld terminal) switched to cards, a scan incremented the matching row and announced it, an unknown code announced an error, discrepancies rendered as sign plus text plus arrow, a missing discrepancy reason blocked confirmation, a completed receipt went read-only, and the page had a single h1 with the levels kept in order by CardTitle's as prop.

The main event: three sightings of scanning, so I built the component

The agent named barcode scan input as the biggest gap, and it had hand-written the same plumbing yet again:

The missing higher-level component is a ScanInput (scan and match)⁠. There is no scan component, no onScan, no match and no-match handling, no scan history. The whole loop is hand-built. ⁠Retail needs this repeatedly (the POS in , the stocktake in , returns in , and receiving here all rebuild it).⁠

That made ⁠three sightings⁠ in the issue #220 lineage (stocktake SKUs in , receipt lookup in , JAN codes here), and this was the purest form yet: a scan gun is a keyboard that types the code fast and sends Enter, so every screen was rebuilding an Input, an Enter handler, an announcement region, a refocus loop, and a scan history. ⁠The rule of three fired⁠, and I shipped ScanInput (issue #220) through PR #225:

function handleScan(code: string): ScanResult {
  const line = lines.find((l) => l.jan === code)
  if (!line) return { ok: false, message: `Not on this purchase order (${code})` }
  increment(line)            // the consumer updates its own state
  return { ok: true, message: `Checked in 1 unit of ${line.name}` }
}

<ScanInput label="Scan a barcode / JAN" inputMode="numeric" onScan={handleScan} showFeed />
  • onScan(code) fires on Enter. The consumer returns { ok, message } and the ⁠announcement, tone, and history⁠ react. The library never depends on the consumer's data model.
  • Every scan result is announced through a permanent, ⁠polite live region⁠ (role="status"), not an assertive interruption, so rapid consecutive scans never cut each other off.
  • After a scan the field ⁠auto-clears and refocuses⁠ (the scan-gun loop), lockMs suppresses double fires, and showFeed keeps a capped newest-first history.
  • The barcode icon, label and description accessibility, aria-invalid support, and a keyboard-only path (type, then Enter) are all in.

Verified in a browser: scanning 4901234567894 made role="status" announce "Checked in 1 unit of Organic Hojicha 500ml (1 checked / 24 ordered)" with an OK history row and the line count up by one; the unknown code 9999999999999 produced a "Not on this purchase order" destructive row and an unknown-code history entry; the input cleared itself; and rapid repeats were correctly suppressed by lockMs.

And two compounding returns arrived at once

This round's agent also ⁠discovered, unprompted, both of the gaps filled in the previous two rounds⁠:

  • Delta: used for the over/short discrepancies⁠, no struggle. It rendered "23 short", "n over", and "matched" correctly in cells, footers, and the receiving dialog, with the tones override making overages a warning.
  • CardTitle as="h2": found and used⁠ to avoid skipping heading levels (confirmed: H1 then H2, no h1-to-h3 jump).

Delta came out of 's rule of three, and the docs visibility for CardTitle as landed in . ⁠Both were found unprompted by this round's blank-slate agent, and both did their jobs.⁠ The docs fix kept this round from stepping in the exact gap stepped in.

The cross-cutting set held as usual: EditableDataTable (received quantities, a reason Select, auto-calculated discrepancies, totals through renderFooterCell, cards at 375px), NumberInput (stepper), DatePicker (expiration dates), RevealSection (the discrepancy-handling panel), and formatCurrency (unit costs).

Logged, not built (not yet at three)

  • 🟡 ⁠A scan-history component⁠ (absorbed into ScanInput's showFeed for now, though the agent argued for a standalone ScanFeed). Treated as resolved by showFeed this round.
  • 🟡 DatePicker's value is a Date (designs that pass ISO strings around need conversions at the boundary). A string mode would remove that glue. First sighting.
  • 🟡 The Select inside EditableDataTable cells is tall at its default h-9 (worked around with h-8).

What I took away: five screens drew the retail map

#42 selling:   change, receipts, yen-or-percent discounts (POS composites)
#43 counting:  scanning (1st), variance display (1st), Progress naming
#44 closing:   over/short (3rd) so Delta shipped, payment-method summary
#45 rewinding: compounding return one (Delta excavated), CardTitle as made visible
#46 accepting: scanning (3rd) so ScanInput shipped, compounding return two (Delta plus CardTitle as)

Run one industry five screens deep with the jobs spread out, and four things become observable at once: (a) the cross-cutting components work on every screen, (b) each screen surfaces its own gaps, (c) a missing component that recurs three times grows into a real component, and (d) the components you grow get excavated by the next blank-slate agent. Retail closes with two components to its name, ScanInput and Delta. Money, grids, scanning, differences: the set that spans retail, fintech, accounting, and payroll is coming together.

Next up: build log

  • ⁠A new industry⁠ (medical, logistics, real estate, or another with industry-specific UI). One thing to watch: whether logistics screens (inbound, outbound, picking) discover the retail-grown ScanInput immediately.

The run

Gunjo was still in early alpha at the time; issues are welcome.

This series is co-created with AI (Claude and Codex). A human designs the experiments, makes the calls, and fact-checks before publishing; the AI does the hands-on work and drafting.

Components from @gunjo/ui

The components this screen's source imports directly.

The code the agent actually wrote

Click a file to expand its source.

業界別「AI指示書パック」、先行登録を受付中です。