Skip to content

DS admission: list-item textEnd, a text at the end of the value's line #242

Open

Description

@darioaschero

Admission class

Extension

Requesting actor / evidence owner

@darioaschero

Consumer owner / affected-surface owner

@darioaschero (nuri-expo send journey: the Lightning invoice review)

Request summary and current state

Amended 2026-11-10 after the independent review (#242 (comment)), following its claims, risks and recommendations; each requester choice below says how. In short:

  • the real failure past the budget is stated, and "short" gets a measurable budget (a third of the line on iPhone SE at default size);
  • a suffix and an end text on the same row are out of the contract;
  • the order rule is worded for textEnd, with tests for the wrong orders;
  • the plan gains a long end text, font scaling on iOS and Android, and a no-grow test;
  • the snapshot change, the recipe header, the "expiry as its own row" alternative and the nuri-expo commit are recorded.

The admission decision itself is not recorded here; it is @darioaschero's, in an architect-owned comment.

This is the later additive request named in the #240 decision record (#240 (comment), anti-scope: "no end-of-line text (textEnd) in v1").

Evidence at main@c69488b (PR #241, merged 2026-11-10): list-item.ts · operation.html board 9, L833-837.

  • Current surface. list-item (DS admission: list-item, the read-only list row #240, implemented in feat: ListItem, the read-only row of the List family — implements nuri-com/nuri-design-system#240 #241) has three slots: textMuted (the label), text (the value, required) and textSuffix.

    • text and textSuffix share the value's line (line, gap xs).
    • The suffix sits 3 after the value and never shrinks.
    • There is no way to set a text at the end of the value's line.
  • The need. On the Lightning invoice review (operation.html board 9), the operator wants the expiry aligned right, on the value's line:

    Lightning invoice
    lnbc47…k3pz                Expires in 9:41
    

    The operator showed this on 9 Nov with a screenshot of board 9, and asked for it again on 10 Nov: "puoi correggere Lightning invoice / lnbc47…k3pz / Expires in 9:41 con Expires in 9:41 allineato a destra".

  • Why it was left out of v1. On 9 Nov the operator made it conditional ("se rende tutto piu complicato no"). The independent review of DS admission: list-item, the read-only list row #240 then showed that the first design (textEnd with fill: 'grow' and align: 'end') was unsafe:

    • grow is flex: 1 0 auto, so the end text cannot truncate;
    • on web, a long end text collapsed the value to 0 px;
    • Android measures a flexGrow Text at the full available width (packages/spec/components/select-trigger.ts:50-51, packages/spec/axes/resolve-map.ts:102-104).
  • Why now. feat: ListItem, the read-only row of the List family — implements nuri-com/nuri-design-system#240 #241 implements v1, and board 9 is the one row that still differs from the operator's design.

Proposed contract delta (not admitted)

Proposed public/observable delta. list-item gains one optional, singular text slot, textEnd (choice 3). Its text sits at the end of the value's line and never shrinks within its budget (choice 1). To get there without flexGrow on a text, the value's line splits into two internal groups:

// packages/spec/components/list-item.ts · proposed, not admitted (the changed parts only)
anatomy: … content: { el: 'view', parts: {
  textMuted: { el: 'text' },
  line: { el: 'view', parts: {
    lead: { el: 'view', parts: { text: { el: 'text' }, textSuffix: { el: 'text' } } },  // NEW internal group
    textEnd: { el: 'text' },                                                           // NEW slot part
  } },
} },
base: {
  // the two groups go to the two ends; with no textEnd, lead sits at the start as today
  line: { stack: { direction: 'row', align: 'center', justify: 'between', gap: 'md' } },    // at least 12 between the groups (choice 2)
  lead: { stack: { direction: 'row', align: 'center', gap: 'xs', fill: 'shrink' } },        // the value group gives way (choice 1)
  text: …unchanged (fill 'shrink', md emphasis, truncate),
  textSuffix: …unchanged (fill 'hug', md muted),
  textEnd: { stack: { fill: 'hug' }, typography: { size: 'md', align: 'end', flow: 'truncate', lines: 1 }, palette: { muted: true } },  // never shrinks (choice 1)
},
api: { slots: { …, textEnd: { part: 'textEnd', kind: 'text', component: true } } },

Public names: RN ListItemTextEnd, web nuri-list-item-text-end.

How it behaves:

  • Placement. With justify: 'between', the value group stays at the start and the end text goes to the right edge. No text part on the value's line grows (text and lead shrink, textSuffix and textEnd hug), so the Android flexGrow measurement does not arise. A test pins this.

  • Rows without an end text. They look the same: the review measured identical boxes on web and RN-web, with short and long values. But every list-item row's tree gains the lead view, and line gains gap: 'md' and justify: 'between', so every list-item snapshot moves. That rows are unchanged is proven by geometry probes, not by snapshots.

  • Order. "End" holds only when textEnd is written after the value. The classifier places a group at its first-authored member (packages/spec/composition/classify.js:67-76), so:

    • written first on the value's line, the end text draws at the start and the value at the right edge;
    • written before the label, it also moves the value's line above the label;
    • written between the value and the suffix, the suffix still joins the value (the group wins).

    The docs say: write it last, after the value and the suffix. Tests on both render paths pin the right order and the two wrong ones, so the behaviour is documented, not accidental.

  • Truncation, and the budget.

    • The end text never shrinks, and the value gives way first.
    • hug caps the end text at the line's width (max-inline-size: 100%, packages/prototype/styles/stack.css:47; maxWidth: '100%', packages/rn/runtime/resolve.ts:102-105).
    • Past its budget, it fails. The value goes to 0, and the end text runs 12 past the content edge into the row's side padding, because lead plus the 12 gap cannot go below 12.
    • The budget: the end text fits in a third of the line on iPhone SE at default text size, that is at most 113 px. "Expires in 9:41" is 105.
  • Combinations. A suffix and an end text on the same row are out of the contract (choice 1). No row uses both, and with both, an amount is the first thing lost under large system text sizes.

  • Style. The end text is md, muted and regular, like the suffix.

    • On RN, hug adds alignSelf: 'flex-start', while web sets none. In the centred line, the end text sits at the top on RN and centred on web.
    • With equal md line heights the two are identical, as they are for the suffix today. Any later size or emphasis option for the end text must handle this.

Affected:

  • the list-item descriptor (one slot, one internal group);
  • the generated RN component and web twin;
  • every list-item snapshot, which gains the lead view and the line style;
  • the header comment of packages/prototype/recipes/list-item.js, which lists the slots;
  • the type-tests;
  • the doc page;
  • the playground component page;
  • board 9 of operation.html.

No other component, token, axis or schema change.

Anti-scope (proposed):

  • no end text over its budget: wider than a third of the line on iPhone SE at default size (choice 1);
  • no suffix and end text on the same row (choice 1);
  • no emphasis or size variant of the end text. nuri-expo's swap From/To puts its end figure in emphasis (nuri-expo@5c818c8b:components/ActivityDetailSheet.tsx:169-173), and it stays uncovered;
  • no grow on any part of the value's line, pinned by a test;
  • no end text on the label's line;
  • no icon or badge at the end;
  • no change to textSuffix or to the 60 / 73 geometry;
  • everything in DS admission: list-item, the read-only list row #240's anti-scope still holds.

Requester choices (10 Nov, amended after the independent review)

These are the requester's choices for the proposed contract. They are not an admission. Each one notes how the review's recommendation was followed.

1 · The end text never shrinks; the value group gives way. The operator chose this. The review says: keep, with conditions; they are followed.

  • The operator: "text end non si accorcia viene usato per casi particolari come l'expire in dove questo suffisso e' importante".

  • When the line is too long, lead shrinks and textEnd hugs. Inside lead, the value truncates first.

  • The failure past the budget, as the review measured it:

    • the value and then the suffix go to 0;
    • the end text runs 12 past the content edge.

    It is stated in the contract, and the budget is measurable: a third of the line on iPhone SE at default size.

  • The suffix-plus-end-text combination is out of the contract. It has no consumer, and it is where an amount disappears under large system text sizes. The review's RN-web emulation erases the value from ×2.5.

  • The cost accepted with the expiry whole. On board 9's invoice row, which has no suffix, large system text sizes truncate the identifier: the review emulated 125 of 195 px at ×2, and 28 of 290 at ×3. The device runs in the plan measure this on iOS and Android.

  • The fallback. If the device runs show this is not acceptable, the option not chosen (the end text also shrinking, in proportion) keeps the value visible and truncates the expiry instead. That would come back to the architect.

  • The options not chosen: both texts shrinking in proportion, or the end text giving way first.

2 · At least md (12) between the value group and the end text. This is the requester's recommendation, which the operator did not change. The review says: keep.

  • The review measured exactly 12 on web and RN-web.
  • justify: 'between' puts all free space between the groups, so the gap only applies when the line is full.
  • 12 keeps the end text from reading as a second suffix: inside the group, value and suffix are 3 apart.
  • The options not chosen were 3 and 6.

3 · The name textEnd, with the order rule worded for it. This is the requester's recommendation, which the operator did not change. The review says: keep the name and change the wording; the wording is changed.

  • "text" first, then the position, like textSuffix. It matches the schema's end. list-action's trailingText names a different place: a column centred on the whole row.
  • "End" holds only when the end text is written last. The docs say so, and tests pin the wrong orders (see "Order").
  • Authored order still applies, as everywhere in list-item. The engine has no fixed-position slot, and adding one would be a wider change than this request.
  • The options not chosen were textTrailing and trailingText.

Consumer need, gap, and current workaround (before admission)

Consumer / owner. nuri-expo send journey, owned by @darioaschero. The row is the Lightning invoice review: "Lightning invoice" over the invoice, with its expiry.

Gap. list-item v1 can only put the expiry next to the value, as a suffix. That is board 9 at c69488b: operation.html:833-837, "lnbc47…k3pz Expires in 9:41".

Workaround available today. Compose that one row by hand in pure DS: the label over a nuri-view direction="row" justify="between" holding the value and the expiry, at list-item's geometry. It is not drawn: the operator chose this request over the hand-made row.

Available consumer-local prototype or evidence (before admission)

  • The design: the operator's screenshot of board 9 (9 Nov) and the request of 10 Nov quoted above.
    • The screenshot is not in the repository.
    • The review reproduced the look in the browser: the value at 18–121.5, and the end text at 252.1–357, its right edge on the content edge.
  • The row today: operation.html board 9 at c69488b. The same invoice row is in send-wip.html round 21, which is history and stays as it is.
  • The rejected first design and why: the DS admission: list-item, the read-only list row #240 review (DS admission: list-item, the read-only list row #240 (comment), claim 1 and R3).
  • The mechanism:
    • justify: 'between' is in the schema's StackNS (packages/spec/components/schema.ts:126) and is mapped on both engines (packages/prototype/styles/stack.css:35, packages/spec/axes/resolve-map.ts:81).
    • No shipped descriptor uses it yet. Rows use it in pure DS on both targets: packages/playground/pages/unified-balance.html:229, packages/expo-demo/src/screens/Move.tsx:101, and nuri-expo@5c818c8b:components/ActivityDetailSheet.tsx:168.
  • Missing: no device rendering of the proposed anatomy. It is shown during implementation, on iOS and Android, with font scaling.

Class-scoped Contraction / Mixed evidence

No response

Class-scoped Extension / Mixed evidence

Recurrence. Honestly narrow: one row on one board (the invoice expiry). nuri-expo's swap row has the same shape but a different style, and it is anti-scope. The case rests on the operator's design for the review sheet and on the low cost, not on recurrence.

DS ownership argument. The row is a list-item. Composing that one row by hand next to five DS rows would split the sheet between DS and copy, which is the drift list-item exists to stop.

Alternatives / trade study.

Alternative Requester's reading (not a decision)
The suffix, as in v1 Already there; not the operator's design.
The expiry as its own v1 row ("Expires in" over "9:41") No contract cost; one more row (73) on the sheet. It does not match the operator's design.
textEnd with fill: 'grow' and align: 'end' (the first design) Refused by the #240 review: the value can collapse, and Android measures a flexGrow Text at full width.
An axis that moves the suffix to the end One fewer slot, but the expiry could not sit at the end of a row that also has a suffix, should one ever need it.
The hand-made row on board 9 Works today, in pure DS; it is the copy list-item replaces.

Contract cost.

  • one optional text slot and one internal group;
  • every list-item snapshot moves;
  • no axis, token or schema change.

Requested DS architect / admission recorder

@darioaschero · sole-architect exception · independent review: #242 (comment)

Architect decision record location (architect-owned)

No response

Proposed bounded execution scope if admitted

In scope:

  • packages/spec/components/list-item.ts: the lead group, the textEnd slot, and the header comment;
  • the regenerated RN and web outputs, and the updated list-item snapshots;
  • the header comment of packages/prototype/recipes/list-item.js;
  • packages/rn type-tests, render-smoke and public-surface;
  • composition tests on both render paths:
    • end text written last (the design);
    • end text written first (it draws at the start);
    • end text written before the label (the value's line moves above it);
    • end text without a suffix;
  • a test that no part of the value's line has grow;
  • the doc pipeline and the list-item page:
    • the end text;
    • its order rule (write it last);
    • its truncation rule (it never shrinks, and the value gives way);
    • the budget (a third of the line);
    • the failure past it;
    • no suffix together with an end text;
    • the 12 minimum;
  • pages/component/list-item.html: boards with an end text, with a long value, and with an end text at its budget;
  • operation.html board 9: the invoice row uses textEnd;
  • the expo-demo sheet: one row with an end text.

Out of scope:

  • nuri-expo;
  • any other component;
  • send-wip.html history;
  • the single-element accessibility reading, which is still its own request.

Downstream verification plan (before Ready)

  • Gates: the full suite, including @nuri/doc. The re-emit is deterministic, and the Guard F pin is unchanged.
  • Existing rows, by geometry: rows without textEnd keep their boxes on the six review boards, compared with main@c69488b. The sheet tops are 281 and 354, the hairlines come every 73, and the value and suffix boxes are identical. Snapshots move by construction.
  • Board 9 on iPhone SE: the end text's right edge equals the row's content edge (357), and the value starts at 18.
  • Long value, on web, RN-web and Android: the value truncates while the end text stays whole.
  • End text past its budget, on web and RN-web: the documented failure is shown and measured (the value at 0, the end text 12 past the edge), so the docs match the engines.
  • Font scaling, on iOS and Android at their largest system text sizes: on board 9's invoice row, the identifier keeps a visible part and the expiry stays whole. Record the measures. If the identifier disappears, stop and bring it back to the architect (choice 1, fallback).
  • No grow: the test pinning no grow on the value's line passes and is mutation-tested.
  • Order: the order tests pass on both render paths.
  • Device: VoiceOver and TalkBack read the end text after the value.
  • Evidence: screenshots under docs/evidence/list-item-text-end/.

Acceptance criteria and Verification/Done evidence

  • <ListItemTextEnd> and <nuri-list-item-text-end> render at the end of the value's line on both targets, when written last.
  • Rows without it are unchanged on screen, by geometry.
  • Within its budget, the end text never shrinks and the value gives way first, on web, RN-web and Android.
  • The failure past the budget, the order rule and the excluded combination are documented and pinned as above.
  • Board 9 matches the operator's design.
  • The full gate suite is green.
  • This record's status line is flipped in the implementing PR.

Owning repository and base branch

nuri-com/nuri-design-system @ main

Dependencies and human decisions

Risk class

Low

Authority, responsibility, and accountability evidence

This request: drafted by an agent session at the requester's direction. That session only read the repository. The operator chose the end text's truncation rule, asked for the request to be posted after #241's merge, and confirmed every correction after the independent review (10 Nov).

May change (after Ready):

  • packages/spec/components/list-item.ts;
  • the generated outputs and the list-item snapshots;
  • the header comment of packages/prototype/recipes/list-item.js;
  • packages/rn and packages/prototype tests;
  • packages/doc;
  • packages/playground;
  • packages/expo-demo.

Must not change:

  • nuri-expo;
  • any other component's contract;
  • the frozen schema.

Approval boundaries: no merge or tag without the operator, and Project Ready is set by a human.

Release, migration, and documentation plan

  • Release. It is an additive slot, so no migration is needed. The release note reads: "ListItem: textEnd, a text at the end of the value's line".
  • Docs. The list-item page gains:
    • the end text;
    • its order rule (write it last);
    • its truncation rule and budget (a third of the line, for short, important information like an expiry);
    • the failure past the budget;
    • no suffix together with an end text;
    • the 12 minimum.
  • Rollback. Revert the PR.

Admission-request acknowledgements

  • This submission requests independent DS contract admission; it does not grant admission, execute the change, or authorize Project Ready.
  • The requested contract delta is proposed until a named independent human DS architect records an Admit decision for a specific accepted contract and anti-scope.
  • Initial submission remains Pending/request-only until an architect-owned decision record exists; this form cannot be used to select or self-attest Admit.
  • Admission independence is from the requester/consumer; the admitting architect may implement later, but independent verification remains required.
  • If one human occupies both requester/consumer and architect roles, a second named human architectural reviewer is required in the architect-owned decision record.
  • Class-specific evidence is provided for the selected class above; the named human DS architect enforces selected-class completeness because GitHub issue forms cannot conditionally require those fields.
  • Any Extension or Mixed request includes recurrence or system-invariant evidence, DS ownership rationale, alternatives/trade study, contract cost, and anti-scope.
  • Any Contraction or Mixed request includes proportional affected-consumer/non-use evidence, migration/deprecation/rollback implications, and ambiguity, misuse, duplication, or maintenance burden removed; pure Contractions are not forced through Extension prompts.
  • Any Mixed request includes both Extension admission evidence and Contraction evidence.
  • Before Project Ready, this issue must have the architect-accepted contract, bounded execution scope, and downstream verification plan.
  • Executed Expo/device/runtime/compatibility proof is collected after implementation before Verified/Done, not treated as pre-Ready admission evidence.
  • No form field, label, assignment, branch, PR, implementation, or green gate grants Project Ready; Project Ready remains the sole execution authorization.
  • No public/observable DS growth is being routed through a maintenance form, bounded Experiment, playground, prototype, Expo demo, or other fast lane.
  • All issue and PR references use canonical owner/repo#number form.

All fourteen apply as written. This is a request only (no Admit, no Ready). The roles overlap, and the second-reviewer box is met through the sole-architect exception (docs/design-system-admission.md, #229): an independent review session posts its findings on this issue before the decision is recorded.

Activity

  1. darioaschero commented on Nov 10, 2026

    @darioaschero
    CollaboratorAuthor

    Independent review · list-item textEnd (Extension request)

    This is the independent review session of docs/design-system-admission.md, "Named exception · sole DS architect". It is a fresh agent session that had no part in drafting the request, the evidence or the brief. It informs the decision and records no outcome.

    Inputs.

    • This issue as filed.
    • The policy and the repository at c69488b. The packages/ tree was exported with git archive to a scratch folder and served statically.
    • The two DS admission: list-item, the read-only list row #240 comments the request cites: the decision record and the independent review.
    • Probes, all outside the repository. No file, branch or issue body changed.
      • Web: operation.html in the iPhone SE frame. Rows were rebuilt in the browser into the proposed anatomy with the factory's own stack attributes: data-justify="between" and data-gap="md" on line, a lead view with data-fill="shrink", and the end text as a muted md typography with data-fill="hug".
      • RN-web: the proposed tree server-rendered with the repository's installed react-native-web 0.21.2, in a 375 frame. It uses the RN renderer's resolved styles from the committed list-item snapshot (packages/rn/__tests__/__snapshots__/render-smoke.test.tsx.snap). The new parts are mapped through the same FILL table (packages/spec/axes/resolve-map.ts:107-113, packages/rn/runtime/resolve.ts:95-107).
      • Classifier: the shared classifier (packages/spec/composition/classify.js), run in Node on the proposed anatomy.

    Not used: other sessions' notes, local drafts, nuri-expo (this request cites no nuri-expo commit), and the operator's screenshot.

    Paths are at c69488b. Measures are in px on iPhone SE (375), relative to the device viewport. The row's content edge is 18–357.


    1 · Claims I could not confirm

    1. "The suffix and the end text stay whole" (Truncation; choice 1).

      • This holds only while the value can still give way. Once the value is at 0, the suffix gives way too:
        • hug caps a part at 100% of its parent (max-inline-size: 100%, packages/prototype/styles/stack.css:47; maxWidth: '100%', packages/rn/runtime/resolve.ts:102-105);
        • the suffix's parent is now lead, and lead shrinks to 0.
      • Probe: a value and a suffix ("₿ 4 760 € 4.49" on board 9's "They get" row on web, "₿ 4 802 € 4.53" on RN-web), with "Die Rechnung läuft in 58 Minuten ab, bitte bestätigen" as the end text. Both give the same numbers:
        • value: 0 px;
        • suffix: 0 px;
        • end text: 30–369, truncated at 339.
      • The line overflows. The end text runs 12 past the content edge (357), into the row's side padding, because lead plus the md gap cannot go below 12. The request does not say this.
    2. "No long end text" as the bound. The threshold is lower than "long" suggests, and nothing checks it.

      • At default text size. "Expires in 9 minutes and 41 seconds" (35 characters, 264 px) on "₿ 22 450 € 21.17" leaves the value 9.7 px wide on web and on RN-web. It draws as a bare "₿" with no ellipsis: on a payment review, the amount reads as missing.
      • The budget. The value starts to truncate once the end text passes 339 − 12 − the value group's width: about 205 on that row, about 224 on board 9's.
      • With OS font scaling, board 9's own "Expires in 9:41" reaches the same failure. List-item texts scale with the system text size on RN: allowFontScaling: false is set only for decorative descriptors (packages/rn/runtime/renderer.tsx:802).

      RN-web, every font size × k, "You pay ₿ 4 802 € 4.53 … Expires in 9:41":

      × value suffix end text
      1 59.7, whole whole 104.9
      1.35 79.2, whole whole 136.6
      2 28.3 of 118 whole 201.7
      2.5 0 of 146 76.7 of 117 250.3
      3 0 of 175 28.2 of 139 298.8
      • Board 9's invoice row, which has no suffix, keeps part of the identifier: 125 of 195 at ×2, and 28 of 290 at ×3.
      • This is an emulation, not a device run. Android's largest font setting is 200%, and iOS's accessibility sizes go further.
    3. "Rows without textEnd are unchanged"; "only the snapshot structure (lead) changes".

      • On screen: confirmed on web and RN-web, with a short and a long value. v1 and the proposed anatomy draw identical boxes:
        • short value: value 18–87.2, suffix 90.2–140.2;
        • long value: value truncated at 18–304.1, suffix 307.1–357.
      • In the tree, more moves than the lead group.
        • Every list-item row, with or without textEnd, gains one host view on both targets.
        • The line style changes too: gap goes from 3 to 12, and justifyContent: 'space-between' is added.
        • So every list-item snapshot moves (packages/rn/__tests__/render-smoke.test.tsx:916 and :997), not only the new ones.
      • The snapshot is updated in the same PR, so it cannot show "unchanged". Only the geometry check in the plan can.
    4. "It follows the author's order, as everywhere in list-item." This is only partly true: the classifier places a group at its first-authored member (classify.js:67-76). Run on the proposed anatomy:

      • textMuted, text, textSuffix, textEnd → content[textMuted line[lead[text textSuffix] textEnd]]: the design.
      • textMuted, textEnd, text → line[textEnd lead[text]]: with justify: 'between', the end text draws at the start (18–122.9) and the value at the right edge (253.5–357), on web and on RN-web.
      • textEnd, textMuted, text → content[line[…] textMuted]: the value's line also moves above the label.
      • textMuted, text, textEnd, textSuffix → lead[text textSuffix] textEnd: the authored interleaving is not kept, because the group wins.

      The planned test "end text written last" pins only the first case.

    5. "End text alone" (execution scope, composition tests).

      • text is required: the web factory throws without it (packages/prototype/factory/factory.js:1112-1113).
      • If the case means "without a suffix", it is fine. If it means "without the value", it cannot render on web.
      • Without lead, justify: 'between' with a single child would also put the end text at the start (classifier: content[textMuted line[textEnd]]).
    6. Not checkable from these inputs (no finding either way):

      • the operator's quotes of 9 and 10 Nov, and the 9 Nov screenshot (none is in the repository at c69488b);
      • nuri-expo:components/ActivityDetailSheet.tsx:168 and :169-173 (this request cites no nuri-expo commit; the DS admission: list-item, the read-only list row #240 review read :169-173 at nuri-expo@5c818c8b);
      • Android and iOS rendering, TalkBack and VoiceOver, and dark mode.
    Checked and confirmed
    • Line references.
      • packages/spec/components/list-item.ts is as described: the slots textMuted, text (required) and textSuffix; line with gap xs; the suffix with fill: 'hug'.
      • operation.html:833-837 is board 9's invoice row, with "Expires in 9:41" as a suffix.
      • select-trigger.ts:50-51 and resolve-map.ts:102-104 carry the Android flexGrow note.
      • schema.ts:126 is StackNS.justify, which includes between.
    • fill values.
      • grow is flex: 1 0 auto (stack.css:43, resolve-map.ts:108).
      • shrink is flex: 0 1 auto with min-inline-size: 0 (stack.css:45). On RN it is flexGrow 0, flexShrink 1, minWidth 0.
      • hug is flex: 0 0 auto, fit-content and max-inline-size: 100% (stack.css:47). On RN it is alignSelf: 'flex-start' and maxWidth: '100%' (resolve.ts:102-105).
    • No text grows.
      • Every part on the proposed value's line has grow: 0: text and lead are shrink, and textSuffix and textEnd are hug.
      • line has no fill, and only content grows (a view, unchanged).
      • So the flexGrow Text case of select-trigger.ts:50-51 does not arise.
    • justify: 'between'.
      • It is mapped on both engines (stack.css:35, resolve-map.ts:81).
      • No shipped descriptor in packages/spec/components/ uses it.
      • The cited board 2 use is a column (operation.html:133), as the request says. Row uses exist in pure DS on both targets, for example packages/playground/pages/unified-balance.html:229 and packages/expo-demo/src/screens/Move.tsx:101.
    • Board 9 as designed. On web and RN-web: value 18–121.5, end text 252.1–357 (its right edge on the content edge), row 60.
    • The 12 minimum. A long value with a suffix and "Expires in 9:41", on both:
      • value truncated at 18–190.3;
      • suffix whole at 193.3–240.1;
      • end text at 252.1–357, exactly 12 after the group.
    • Today's board 9. Value 18–121.5 and suffix 124.5–229.3, as the DS admission: list-item, the read-only list row #240 review measured.
    • No schema change. lead is a descriptor-local part, and Guard F pins the frozen schema shape (scripts/docs-drift.test.js:505), not descriptor data.
    • Internal names stay internal. packages/doc/generated/components/list-item.md names only the slots.
    • trailingText is another place. list-action's trailing is a column with align: 'end', centred on the row (list-action.ts:40,62-66), as the request says.
    • Round 21. send-wip.html:1563-1565 has the expiry as a muted md text next to the value, with gap xs.

    2 · Risks to the contract

    R1 · The lead group reaches every existing row.

    • Every list-item row gains one view on both targets:

      • 16 rows in operation.html;
      • 7 in pages/component/list-item.html;
      • 3 in packages/expo-demo/src/sheets/ActivitySheet.tsx;
      • plus the tests.
    • The geometry holds on web and RN-web (claim 3).

    • Not probed:

      • Android Text measurement inside the new nested shrinking group, one level deeper than v1's value;
      • the VoiceOver and TalkBack order.

      The plan covers both.

    • Authored order between the two lines now also depends on textEnd: writing it first moves the value's line above the label (claim 4).

    R2 · justify: 'between' on both engines.

    • Mapping. It maps identically on both engines. With one child it behaves as start on both; I probed that rows without textEnd are unchanged.
    • First descriptor use. The engines agree on between here, but not on hug:
      • RN adds alignSelf: 'flex-start', while web sets no align-self;
      • so, in the line row with align: 'center', the end text sits at the top on RN and is centred on web. The suffix already does this.
      • With equal md line heights the difference is invisible today. A later size or emphasis option for the end text would show it.

    R3 · "The end text never shrinks."

    • It is bounded. hug caps it at 100% of the line, and it truncates there (claim 1).
    • The failure is the DS admission: list-item, the read-only list row #240 review's for the first design (the value at 0), with three differences:
      • there is no Android flexGrow risk;
      • the suffix also goes to 0;
      • the end text overflows by 12 instead of 3.
    • The threshold is lower than "long". The request names the cost as "a long end text". The measures put it at about 205 px of end text at default size. At large system text sizes, the operator's own "Expires in 9:41" first cuts an amount, then erases it (claim 2).
    • What disappears is the required part. This row is a payment review, and a value cut to "₿" shows no ellipsis.
    • Nothing checks "keep it short". It is a doc rule with no measurable budget.

    R4 · The order rule against the name.

    • textEnd names a position, but the order rule makes the position the author's: written first, it draws at the start of the line (claim 4).
    • textSuffix has the same property, and DS admission: list-item, the read-only list row #240 accepted it by documenting it. Here the mismatch shows at the opposite edge of the row, not 3 px away.

    R5 · The 12 minimum.

    • It only shows when the line is full. Then it is the only cue between a muted md suffix and a muted md end text ("€ 4.53 Expires in 9:41").
    • The suffix-plus-end-text combination has no consumer: board 9 has no suffix, and the request's "₿ 4 802 € 4.53 … Expires in 9:41" is illustrative.
    • It is also the combination that collapses first under font scaling (claim 2).

    R6 · The name.

    • Position words in the List family would be:
      • leading and trailing (list-action: leadingAvatar, trailingText, trailingTextMuted, trailIcon);
      • Suffix;
      • now End.
    • "End" fits the schema. It matches the logical align / justify: 'end'. "Trailing" would point at list-action's row-centred column, so the request's reason holds.
    • The internal group is called lead, next to list-action's and topbar's leading. It is internal and undocumented, so this is cosmetic.
    • Decision 84's family vocabulary was deferred again in DS admission: list-item, the read-only list row #240, and this request adds a word to that set.

    R7 · Recurrence.

    • One row on one board. nuri-expo's swap row is anti-scope (emphasis), so the contract would have exactly one consumer site, and the suffix-plus-end-text combination would have none.
    • A zero-cost alternative is missing from the trade study: the expiry as its own v1 list-item row ("Expires in" over "9:41"). It costs one more row (73) on the sheet and does not match the operator's design.

    R8 · What the gates would not catch. The 5 gates and the guards stay green with any of these:

    • a long end text erasing the value and the suffix, and the 12 px overflow (render-smoke does no layout);
    • font scaling erasing an amount (claim 2);
    • textEnd written first: it draws at the start and lifts the value's line above the label;
    • a later edit giving a text part grow: no test pins "no flexGrow on a text part" for this descriptor;
    • the RN/web align-self difference of hug (R2);
    • "rows without textEnd unchanged": the snapshot of every row moves in the same PR (claim 3);
    • Android measurement of the nested group.

    R9 · Gaps in the verification plan and the scope.

    • The plan's long-text check covers a long value only. It has no long end text, no font-scale case and no end-first case.
    • The header of packages/prototype/recipes/list-item.js:4-6 lists the slots. The file is in neither the execution scope nor the "May change" list, which names only packages/prototype tests.
    • "End text alone" needs defining (claim 5).

    3 · Recommendation for each requester choice

    1 · The end text never shrinks; the value group gives way: keep, with conditions.

    • Why keep. The operator's reason holds: the expiry must stay readable. The mechanism also avoids the Android flexGrow case.
    • Conditions.
      • State the real failure in the accepted contract. Past the threshold, the value and the suffix go to 0, and the end text runs 12 past the content edge (claim 1).
      • Give "short" a measurable budget, for example "fits in a third of the line on iPhone SE at default size".
      • Keep the suffix-plus-end-text line out of the contract until a consumer needs it. It has none, and it is where an amount goes to 0 under font scaling (claim 2, R5).
      • Add to the plan:
        • a long end text;
        • the end text at the largest system text sizes on iOS and Android;
        • a test that no part on the value's line has grow.
    • If the font-scale result is not acceptable on a payment review, the option not chosen, both texts shrinking in proportion (shrink on the end text too), keeps the value visible. It truncates the expiry instead.

    2 · At least md (12) between the groups: keep.

    • The probe confirms exactly 12 on web and RN-web, and the gap only acts when the line is full.
    • It is the only cue between a muted suffix and a muted end text. That combination has no consumer, so either keep it out of the contract (as in 1) or pin it with a probe in the plan.

    3 · The name textEnd: keep the name, but change how the order rule is stated for it.

    • Why keep. It follows textSuffix's text-first pattern and the schema's end, and trailingText would mislead (R6).
    • Why change the order rule's wording. Under "the author's order", textEnd can draw at the start and lift the value's line above the label (claim 4). Either:
      • document that "end" holds only when the end text is written after the value, and pin the end-first and label-after cases with tests on both render paths; or
      • record why authored order should not apply to this slot.

    This review records no outcome. The decision, and the answer to each point above, belong to the architect.

  2. darioaschero commented on Nov 10, 2026

    @darioaschero
    CollaboratorAuthor

    Request amended after independent review

    The issue body is amended to answer the independent review. No decision is recorded here, and no Project state moved. The admission decision remains @darioaschero's, in an architect-owned comment.

    Independence note. The session that drafted the request made this amendment, at the architect's direction, after the review was posted. It applies the review's claims, risks and recommendations, and nothing else. Any further independent review of the amended text needs a new session.

    Claims the review could not confirm

    Review item How the request answers it now
    1 · "The suffix and the end text stay whole" Corrected under "Truncation, and the budget". Within the budget, the end text never shrinks and the value gives way. Past it, the value goes to 0 and the end text runs 12 past the content edge. The suffix is no longer in play, because a suffix together with an end text is out of the contract.
    2 · "No long end text" had no measurable bound "Short" is now a budget: a third of the line on iPhone SE at default size (at most 113 px; "Expires in 9:41" is 105). The font-scale cost on the invoice row is stated in choice 1, with the review's figures. The device runs in the plan measure it, and a fallback goes back to the architect if the identifier disappears.
    3 · More moves than lead in the tree "Rows without an end text" now says that every row gains lead, that line gains gap: 'md' and justify: 'between', and that every list-item snapshot moves. "Unchanged" is proven by geometry probes against c69488b.
    4 · Authored order breaks "end" "Order" now says that "end" holds only when the end text is written last, and describes the three wrong orders. The scope adds tests for end-first and before-the-label on both render paths.
    5 · "End text alone" Renamed "end text without a suffix", because text is required.
    6 · Not checkable The nuri-expo lines are now cited at nuri-expo@5c818c8b. The operator's quotes and screenshot stay as the requester's evidence. The look was reproduced by the review in the browser.

    Risks

    Review item How the request answers it now
    R1 · lead reaches every row Recorded under "Rows without an end text" and in the contract cost. Android measurement of the nested group and the reading order are in the device plan.
    R2 · between and hug on both engines The RN/web align-self difference of hug is recorded under "Style": it is invisible with equal line heights, and any later size or emphasis option must handle it.
    R3 · "Never shrinks" is bounded, and the threshold is low The budget, the failure past it, and the font-scale cost are stated. The plan adds a long end text and the largest system text sizes on iOS and Android.
    R4 · The order rule against the name The name is kept, and the rule is worded for it, with tests for the wrong orders (claim 4).
    R5 · The 12 minimum and the suffix-plus-end combination The 12 is kept. The combination is out of the contract, since it has no consumer.
    R6 · The name Kept: it follows textSuffix and the schema's end, while trailingText names another place. lead is internal.
    R7 · Recurrence, and a missing alternative The trade study adds "the expiry as its own v1 row": no contract cost, one more row, not the operator's design. The recurrence stays stated as one row.
    R8 · What the gates would not catch The plan probes or tests each one: long end text, font scaling, wrong orders, a no-grow test (mutation-tested), the align-self note, geometry for unchanged rows, and Android on device.
    R9 · Gaps in the plan and the scope The plan adds a long end text, font scaling and the order cases. The scope and "May change" add the header of packages/prototype/recipes/list-item.js. "End text alone" is defined.

    Recommendations for the three choices

    Choice Review Now
    1 · Never shrinks; the value gives way Keep, with conditions Kept, with every condition: the real failure stated, a measurable budget, no suffix together with an end text, and plan additions (long end text, font scaling, no-grow test). The proportional option is recorded as the fallback.
    2 · At least md (12) Keep Kept. The combination it was meant to separate is out of the contract.
    3 · The name textEnd Keep the name; change the order wording Kept, with "end" defined as "written last", and the wrong orders pinned by tests.
  3. darioaschero commented on Nov 10, 2026

    @darioaschero
    CollaboratorAuthor

    DS admission decision

    Outcome: Admit
    Admission class: Extension
    Architect: @darioaschero
    Second human architectural reviewer, if requester/consumer and architect overlap: none · sole-architect exception (@darioaschero) · independent review: #242 (comment)
    Requester / consumer owner: @darioaschero (nuri-com/nuri-expo, the send review sheet: the Lightning invoice review)
    Evidence considered:

    Rationale:

    • The expiry is the information that must stay whole. On a Lightning invoice the expiry decides whether the payment can go through. I want it at the end of the value's line, readable, and never cut.
    • The design avoids the refused mechanism. The value's line splits into a value group and the end text with justify: 'between'. No text grows, so the Android flexGrow case of the first design does not arise.
    • The cost is small. It is one optional slot and one internal group. Every list-item snapshot moves, but the review confirmed that rows without an end text keep their boxes on web and RN-web.
    • The recurrence is one row, and I accept it as such. The row is part of a list-item sheet, and a hand-made row next to five DS rows is the drift list-item was admitted to stop.

    Accepted contract:

    • list-item gains textEnd: optional and singular, md, muted and regular. RN ListItemTextEnd, web nuri-list-item-text-end.
    • Anatomy. The value's line holds an internal lead group (text, textSuffix; gap xs, fill: 'shrink') and textEnd (fill: 'hug', align: 'end'). The line has justify: 'between' and gap md, so there are at least 12 between the two groups.
    • No grow on any part of the value's line, pinned by a test.
    • Truncation. Within its budget the end text never shrinks, and the value gives way first.
    • The budget. The end text fits in a third of the line on iPhone SE at default text size (at most 113 px).
    • Past the budget, the value goes to 0 and the end text runs 12 past the content edge. This is documented as the failure, not supported.
    • Order. "End" holds only when the end text is written last, after the value. The docs say so, and tests on both render paths pin the right order and the two wrong ones (first on the line; before the label).
    • Rows without an end text keep their geometry, proven by geometry probes. Their snapshots move.
    • Docs. The list-item page states:
      • the end text;
      • the order rule;
      • the truncation rule;
      • the budget;
      • the failure past it;
      • that a suffix and an end text are not used together;
      • the 12 minimum.

    Anti-scope:

    • no end text over its budget;
    • no suffix and end text on the same row;
    • no emphasis or size variant of the end text (nuri-expo's swap row stays uncovered);
    • no grow on the value's line;
    • no end text on the label's line;
    • no icon or badge at the end;
    • no change to textSuffix or to the 60 / 73 geometry;
    • everything in the DS admission: list-item, the read-only list row #240 anti-scope.

    Answers to the independent review:

    • Claims 1–2 (the failure and the budget): accepted.
      • I keep the expiry whole and accept that the value gives way.
      • The failure past the budget is now in the contract, and "short" has a measurable budget.
      • Font scaling. On board 9's invoice row, large system sizes truncate the identifier (the review emulated 125 of 195 px at ×2, and 28 of 290 at ×3). I accept that cost for v1. The device runs measure it on iOS and Android.
      • The stop condition. If the identifier disappears on a device, implementation stops and the choice comes back to me. The fallback is the end text also shrinking in proportion.
    • Claim 3 (snapshots): accepted. "Unchanged" is proven by geometry, and the snapshot move is recorded.
    • Claim 4 and R4 (order against the name): accepted.
      • I keep authored order, as everywhere in list-item, and define "end" as "written last".
      • The wrong orders are documented and pinned by tests.
      • A fixed-position slot would need an engine change, and that is not worth it for one slot.
    • Claim 5 ("end text alone"): accepted. It means "without a suffix".
    • Claim 6 (inputs it could not check): the quotes and the screenshot are mine, and I stand by them. The nuri-expo lines are now cited at a commit.
    • R1 (lead in every row): accepted. Android measurement of the nested group and the reading order are in the device plan.
    • R2 (hug alignment on RN and web): accepted, and recorded. It is invisible with equal line heights, and any later size or emphasis option must handle it.
    • R3 (the bound and the low threshold): accepted. See claims 1–2.
    • R5 (the 12, and the suffix-plus-end combination): accepted. The 12 stays, and the combination is out of the contract until a consumer needs it.
    • R6 (the name): kept. textEnd follows textSuffix and the schema's end, while trailingText names list-action's centred column. Decision 84's family vocabulary stays deferred, as recorded on DS admission: list-item, the read-only list row #240.
    • R7 (recurrence; the expiry as its own row): accepted as stated. Its own row costs nothing in the contract, but it is not the design I want for this sheet.
    • R8 (gate-blind cases): accepted. Each one is probed or tested in the plan.
    • R9 (plan and scope gaps): accepted. The long end text, font scaling, the order cases and the recipe header are now in scope and in the plan.

    Process note:

    • A fresh session wrote the review, read-only, with only the issue, the repository at c69488b, the two DS admission: list-item, the read-only list row #240 comments the request cites, and the policy.
    • The drafting session made the amendment afterwards, at my direction. It applies the review's findings and nothing else.
    • I accept the amended text without a second review.

    Contraction evidence considered, if applicable: none. This is a pure Extension.
    Bounded execution scope reference: #242, "Proposed bounded execution scope if admitted" (as amended)
    Downstream verification plan reference: #242, "Downstream verification plan (before Ready)" (as amended)

  4. self-assigned this
    on Nov 10, 2026
  5. removed their assignment
    on Nov 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent-readyComplete contract eligible for bounded agent execution

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions