Repository navigation
feat: ListItem, the read-only row of the List family — implements nuri-com/nuri-design-system#240 - #241
Conversation
Implements the contract admitted in #240 (comment) (sole-architect exception, independent review at #issuecomment-6744184207). Spec - components/list-item.ts: three singular text slots in authored order — textMuted (sm muted), text (md emphasis, required, fill shrink), textSuffix (md muted, fill hug) — inside an internal `content` column (gap xs) and `line` row (gap xs); root row paddingX md, paddingY sm, minHeight xl (60). No axes, no behaviour, no props beyond children. The frozen schema and the Guard F pin are unchanged. - list.ts / list-action.ts / list-switch.ts: header comments name the three rows; "every list row" reworded for the recorded 60 exception. Codegen and guards - descriptors.js roster + spec exports; regenerated the RN adapter, recipes.ts, the components index and the web twin (list-action and list-switch twins change in comments only). - docs-drift EXPECTED_DESCRIPTORS pins list-item's shape. RN - barrel exports ListItem, ListItemTextMuted, ListItemText, ListItemTextSuffix (+ Props); public-surface roster; exact-type fixtures (keyof ListItemProps = 'children'; no onPress, disabled, accessibilityLabel or accent). - render-smoke: structure by geometry on host nodes (60 row, content column, line row), no pressable/role/accessible host, value-first and suffix-first order, required Text and singular slots. Web - recipes/list-item.js (+ list.js import, playground bundle). - factory tests C13–C13d mirror the RN block on the web factory path. - legacy components/list-item/ pruned; legacy/README.md updated, so <nuri-list-item> is defined once. Playground - pages/component/list-item.html (+ index card). - operation.html: the six review sheets (boards 2, 5, 7, 9, 12, 14) use nuri-list-item inside nuri-list with nuri-list-separator between rows; the column drops padding-x lg, adds padding-top sm, padding-bottom lg → xl; the misfit-log line is closed. Nothing else on the page moves. Expo demo - ActivitySheet gains a read-only "Processing" list. Doc - shell nav, component-api-ir (List lead names the three rows; the list-item lead carries the admitted doc rules), doc drift pins, generated list.md and list-item.md. Evidence under docs/evidence/list-item/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
prompts/admission-list-item.md is the amended #240 record (it matches the issue body), with its title and a status paragraph set to: Admitted (sole-architect exception) · Ready · implemented in #241, independent verification pending. The #232 precedent: the admission ledger flips in the implementing PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Independent verification · #241 against the #240 decision recordThis is the independent verification that Inputs. I used only these:
I did not read the implementer's notes or use the PR body as proof. I read only the status line of Head verified: Result.
Acceptance criteria
Anti-scope
Gates (re-run on the head after
|
| Gate | Commands | Result |
|---|---|---|
| root | npm run build · npm test |
build exit 0, tree clean. npm test exit 1: the only failure is packages/gen-ui/live.test.js (see caveats). Every other suite passes. |
| spec | node --test scripts/*.test.js · node scripts/tokens-parser.js · git diff --exit-code packages/rn/generated packages/prototype/generated |
128/128 · clean |
| prototype | npm test -w @nuri/prototype · npm run build -w @nuri/prototype · git diff --exit-code packages/prototype/styles/ · npm run build -w @nuri/playground · git diff --exit-code packages/playground/generated/ |
134/134 · clean · clean |
| doc | npm test -w @nuri/doc · npm run build -w @nuri/doc · git diff --exit-code packages/doc/generated/ |
3/3 · clean |
| rn | npm test -w @nuri/rn · npm run typecheck -w @nuri/rn |
21 suites, 312 tests, 27 snapshots · tsc exit 0 |
| expo-demo | npm run typecheck -w @nuri/expo-demo |
tsc exit 0 |
| determinism | (npm run build && npm run build -w @nuri/doc) twice |
sha256 of the 97 files under rn/generated, prototype/generated, prototype/styles, playground/generated and doc/generated is identical across the two builds (manifest 253e9e44…). git diff --exit-code is clean, so generated ≡ committed. |
Composition and the mutation test
- Tests on both render paths. RN render-smoke has
ListItem — value-firstandListItem — suffix-first. The webfactory/factory.test.jshasC13b(value-first) andC13c(suffix-first). They assert the host children ofcontentandlinein order. - Mutation. In
packages/spec/composition/classify.jsI sorted theorderedplan by anatomy index before the singular check, so anatomy order beat authored order. I then regenerated, which copies the file toprototype/generated/composition-classify.jsfor web.- RN: value-first and suffix-first fail; the base and the required/singular tests pass.
- Web: C13b and C13c are not ok; C13 and C13d are ok.
- After the revert, all 4 pass on both paths and the tree is clean. The tests catch a broken order.
Rendered proof · web (iPhone SE, 375 × 667)
The PR head and git archive origin/main were each served statically. operation.html renders every board at data-device="iphone-se". Positions are relative to each device screen.
main (hand composition) |
#241 | |
|---|---|---|
| Rows | 72, split 13.9 / 3 / 13.9 | 60, split 7.9 / 3 / 7.9 (all 16) |
| Row tops | 353 · 426 · 499 | 359 · 432 · 505 (every 73) |
| Hairlines | y 425, 498 (3-row sheets), 498 (2-row), x 18–357 | identical |
| Text to hairline | 13.9 above and below | 13.9 above and below |
| Text x / hairline x | 18 / 18 | 18 / 18 |
| Suffix gap | 3 | 3 |
| Sheet top | 281 (boards 2, 5, 7, 9) · 354 (12, 14) | identical |
| Button | 589–649 on all six | identical |
| Sheet scrolls | no | no |
- Text rects. All 54 text nodes in the six sheets have the same x, y, height, colour, weight and font size on
mainand on the PR.- The only difference is the label box width: 339 in the PR, because
contentisalign: 'stretch', against the text's own width onmain. The glyphs start at the same x. - Board 7's From chip is at 239.4–275.4, so the sheet top at 281 is clear of it.
- The only difference is the label box width: 339 in the PR, because
- Structural parts. In every row,
nuri-list-item > nuri-view(363 × 60) holds exactly onecontentcolumn (339 × 44.2,data-gap="xs",data-fill="grow-shrink"). The column holds the label and thelinerow (339 × 21.9), and the line holds the value and the suffix. - Long value. In the DOM only, I appended a row to board 7's list with the value "₿ 22 450 000 … 000" and the suffix "€ 21.17". Then I removed it.
- The value box is 286.1 wide and one line high (21.9; scrollHeight 44,
-webkit-line-clamp: 1), and the ellipsis is drawn. - The suffix is 49.9 wide, the same as on the unmodified row, with scrollWidth = clientWidth.
- The gap stays 3, the line ends at 357, and the row stays 60.
- The value box is 286.1 wide and one line high (21.9; scrollHeight 44,
component/list-item.html. It renders 7 rows, all 60, each withcontentandline, including the long-value and long-label cases. The console has no errors.- Dark. With one board toggled, the label and suffix are
rgb(183,180,164), the valuergb(240,238,227)and the hairlinergb(61,59,46), onrgb(18,17,11). The rows are still 60.
Rendered proof · RN-web (Expo web preview, iPhone SE, Menu → Activity Sheet, the new "Processing" list)
- Rows are 60 at y 117.9, 190.9 and 263.9 (every 73), split 7.9 / 3 / 7.9, at x 6 and 363 wide.
- Hairlines are at y 183.9 and 256.9, x 18–357. The text is at x 18, with 13.9 between the text and the hairline on both sides, and the suffix 3 after the value.
content(2 children, 44.2 tall) andline(a View, 21.9 tall) render.- There is no role, tabindex or focusable inside the rows.
- Long value. I replaced the text of the "€ 12.00" Text in the DOM.
- The value is 260.1 wide, with scrollWidth 482 > clientWidth 260,
nowrapand an ellipsis. - The suffix "3433 Sats" stays at its natural 75.9, with
flexShrink 0. - The gap stays 3, the line ends at 357, and the row stays 60.
- The value is 260.1 wide, with scrollWidth 482 > clientWidth 260,
Could not check / caveats
- The Expo web bundle does not build at this head.
- Metro fails on
packages/rn/runtime/native-text-input-command.ts, which has a staticrequireofreact-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent("Importing react-native internals is not supported on web"). - The PR does not touch that file; it was last changed in Codex/textfield native authoritative reconciler #196.
- For the RN-web probe I added a local, uncommitted
native-text-input-command.web.tsstub with the same exports as no-ops. The original already returnsnullwhenPlatform.OS === 'web', so web behaviour is unchanged. I removed the stub afterwards. - This needs its own fix outside this PR.
- Metro fails on
packages/gen-ui/live.test.jsfails in the rootnpm test. It is a live test that calls an external MCP endpoint. It is not ingates.yml, and the PR touches nothing inpackages/gen-ui.- RN-web dark was not checked. The demo does not follow the system colour scheme, and its theme toggle sits behind the sheet. Web dark was checked.
- Device rendering (iOS, Android) and screen-reader speech cannot be checked in-session.
- The real-consumer proof (a nuri-com/nuri-expo PR that implements the Send review sheet from the playground link) is still open. It belongs to Verification/Done.
- Not contract changes, recorded for completeness:
doc/generated/components/list.mdgains one sentence namingListItemand the no-mixing rule;packages/spec/package.jsongains the./descriptors/list-itemexport that the generated RN adapter imports;docs/evidence/list-item/holds 8 screenshots, which I did not use as proof.
Native-only residue for the operator (device or simulator)
- VoiceOver (iOS) and TalkBack (Android): each row's texts are read one by one in screen order (label, value, suffix), with no stop on the row itself, as in today's composition.
- Android focus: a hardware keyboard or D-pad never lands on a
ListItemrow. - Android truncation: with a long value, the value ellipsizes and the suffix stays whole and is not pushed out. Check this on the
textSuffixhugpath (flexShrink 0) and the value'sflexShrink 1withminWidth 0. - Large text (Dynamic Type and Android font scale): the row grows past 60 without clipping, and the 3 gap and the hairline spacing hold.
- Dark mode on the native build of the expo-demo Activity sheet.
Implements #240:
ListItem/nuri-list-item, the read-only row of the List family, as admitted in the decision record. The admission used the sole-architect exception, with an independent review. This PR does not close the issue: Done needs independent verification and the native residue below.The contract as built
textMuted:sm, muted, one line, truncated;text:mdemphasis, one line, truncated,stack.fill: 'shrink',required: true;textSuffix:md, muted, one line,stack.fill: 'hug'.textandtextSuffixsit in an internallinerow (gapxs). The line sits inside acontentcolumn (gapxs,grow-shrink) withtextMuted.viewrow withpaddingX: 'md',paddingY: 'sm'andminHeight: 'xl'(60).themeScope.ListItemPropsis{ children?: React.ReactNode }. The frozen schema and the Guard F pin are unchanged.What changed
Spec
packages/spec/components/list-item.ts(new). The header records the 60 exception, the separator dependency, the screen's 6 above and below, no mixing, and the rule on identifiers.list.ts,list-action.tsandlist-switch.tsheaders name the rows of the family. The "every list row" floor comments now say "every pressable list row" and name thelist-itemexception. These are comments only.@nuri/specexports./descriptors/list-item.Codegen and guards
scripts/parsers/descriptors.jsroster.docs-drift.test.jsEXPECTED_DESCRIPTORSgainslist-item(partscontent · textMuted · line · text · textSuffix, no interactive). The Guard F pin does not move.rn/generated/components/list-item.ts(new) andindex.ts,rn/generated/data/recipes.ts(+list-item), and theprototype/generated/descriptors/list-item.jstwin (new).list-action.jsandlist-switch.jstwins change in comments only, because the twin carries the descriptor body.recipes.tsand the RN adapters walk the roster (scripts/tokens-parser.js). Palette, tokens, icons and CSS are untouched.RN
ListItem,ListItemTextMuted,ListItemText,ListItemTextSuffixand theirProps, andpublic-surface.test.tslists them.keyof ListItemPropsis exactly'children', and each slot's props are exactly'children';@ts-expect-errorononPress,disabled,accessibilityLabel,accent, a scalartext, andListItemTextwithout children.render-smoke:contentcolumn and thelinerow are asserted by geometry on host nodes;accessibleoronPressinside the rows;requires Text, and a repeated slot fails as singular;.snapdiff is +242 / −0.Web
prototype/recipes/list-item.js(new), imported byrecipes/list.jsand the playground bundle.factory.test.jsC13–C13d mirror the RN block on the web factory path:packages/prototype/legacy/components/list-item/is pruned, as icon-button and tab-bar were before it.legacy/README.md(the table and the "Pruned" note) is updated.<nuri-list-item>is now defined once, bydefineNuriComponentin the active recipe. The frozen legacy pages that mention the old tag stay as read-only specs.Playground
pages/component/list-item.html(new), with its card incomponent/index.html. Three boards: the review rows, identifiers and expiry, and a long value with a long label.pages/operation.html, the six review sheets (boards 2, 5, 7, 9, 12, 14):nuri-list-iteminsidenuri-list, with anuri-list-separatorbetween rows;padding-x="lg", addspadding-top="sm", and movespadding-bottomfromlgtoxl;send-wip.htmlround 21 (history).Expo demo
src/sheets/ActivitySheet.tsxgains a read-only "Processing" list: threeListItems with suffixes, in their ownList, insideView paddingY="sm". It is not mixed with theListActionlists.Doc
harness/shell.jsnav andpipeline/component-api-ir.js:list-itempage (nav 28) carries the doc rules from the decision record: read-only, never an action; authored order, value first then suffix; a short suffix; identifiers abbreviated with the ellipsis in the middle; 60 and 3 againstListAction's 72 and 0; separators and the screen's 6; no mixing; singular slots againstListAction's repeatable ones;pipeline/docs-drift.test.jspins both pages. Regeneratedgenerated/components/list.mdandlist-item.md.pipeline/docs.jsNAV_ORDERis not touched: the IR'snavwins, as forlist-switch.Admission ledger
prompts/admission-list-item.md: the DS admission: list-item, the read-only list row #240 record, as amended (it matches the issue body). Its title and a new status paragraph say Admitted · Ready · implemented in this PR, independent verification pending. This is the feat: IconAvatar initials + corner accessory (+ ListAction forwarding) — implements #231 #232 precedent.prompts/is the admission ledger, not contract surface. It is the home for the acceptance criterion "This issue's status line is flipped in the same PR".Verification
The 5 gates, run locally as in
gates.yml, on the committed tree:node --test scripts/*.test.js·node scripts/tokens-parser.js·git diff --exit-code packages/rn/generated packages/prototype/generatednpm test -w @nuri/prototype·npm run build -w @nuri/prototype·git diff --exit-code packages/prototype/styles/·npm run build -w @nuri/playground·git diff --exit-code packages/playground/generated/npm test -w @nuri/doc·npm run build -w @nuri/doc·git diff --exit-code packages/doc/generated/npm test -w @nuri/rn·npm run typecheck -w @nuri/rnnpm run typecheck -w @nuri/expo-demonpm run buildtwice. The 90 files underrn/,prototype/,playground/anddoc/generatedare hash-identical (sha256 of all files47bc4a8f…).git statusis clean afterwards.content's gap tosmin the descriptor fails one RN test (render-smoke) and one web test (C13). The change was reverted.Geometry on web, measured by probe in the iPhone SE frame (375×667), on all six
operation.htmlreview boards:Against
main. The same probe onmain@0cbb776's page gives the same tops, hairlines, gaps and button. All 48 measured items (texts, separators, buttons) keep position, colour, weight and size. The only difference is that label boxes now stretch to the content width (339) instead of hugging, the same aslist-action'salign: stretch, and the glyphs don't move. Board 7's screenshot is byte-identical tomain's. A guarded capture confirmed that the baseline page had the 16 hand rows and the new page has the 16nuri-list-items.Long values, web.
flex: 0 0 auto), sits 3 after the value, and ends at the line's end.RN-web (Expo web preview,
ActivitySheet, 375×667):contentcolumn (gap 3, grow and shrink) and thelinerow (gap 3) are present in the DOM;Evidence in
docs/evidence/list-item/:web-operation-review-boards-{light,dark}.png: the six sheets;web-operation-board7-pixel-identical-to-main.png;web-component-list-item-{light,dark}.png;rn-web-activity-sheet-{light,dark}.jpg;rn-web-long-value-probe.jpg.Native-only residue, for the operator
These can't be proven in-session (on a device or simulator):
hug→alignSelf: flex-start/maxWidth: 100%path). Also check a short value: no flexGrow inflation.ActivitySheetrows on device, light and dark.Not done here
🤖 Generated with Claude Code