ScanInput shipped on its third sighting while two earlier bets paid off
/receivingCaptured 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 (#42), counting (#43), closing (#44), and rewinding (#45), 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, noonScan, no match and no-match handling, no scan history. The whole loop is hand-built. Retail needs this repeatedly (the POS in #42, the stocktake in #43, returns in #45, and receiving here all rebuild it).
That made three sightings in the issue #220 lineage (stocktake SKUs in #43, receipt lookup in #45, 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),
lockMssuppresses double fires, andshowFeedkeeps a capped newest-first history. - The barcode icon, label and description accessibility,
aria-invalidsupport, 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 thetonesoverride 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 #44's rule of three, and the docs visibility for CardTitle as landed in #45. 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 #45 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'sshowFeedfor now, though the agent argued for a standaloneScanFeed). Treated as resolved byshowFeedthis round. - 🟡
DatePicker's value is aDate (designs that pass ISO strings around need conversions at the boundary). A string mode would remove that glue. First sighting. - 🟡 The
SelectinsideEditableDataTablecells is tall at its defaulth-9(worked around withh-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 #47
- 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.