Repository navigation
Description
Admission class
Extension
Requesting actor / evidence owner
Consumer owner / affected-surface owner
@darioaschero (nuri-com/nuri-expo: send step S1, the search mode, the address book)
Request summary and current state
Amended 2026-10-24 after the independent review
(#231 (comment)), following the
architect's direction on the open calls. The admission decision itself is not recorded here; it is
@darioaschero's, in an architect-owned comment.
Current surface. IconAvatar (packages/spec/components/icon-avatar.ts) shows exactly one of two
contents: a glyph (icon) or an image (source, which wins when both are given). An internal
mode axis (glyph | image, derived by propMaps.contentMode) mutes the root's variant paint in
image mode. Sizes: md 48px (default), sm 24px. Decorative (hidden from assistive tech).
ListAction's leadingAvatar part nests an icon-avatar and forwards name → icon, variant
(default outline), accent and source. The DS has no way to put text in an avatar and no way to
lay one part over another (the schema has no positioning; BleedNS exists precisely so that
descriptors and View cannot opt into overlap).
Consumer need. The send step S1 (recipient list), search results and the address book list
people as flat contacts. The row subtitle names the identifier (@emil, N26 ···4521, bc1q…9m7t).
The avatar should carry the person's initials and a small accessory at the bottom-right that
shows the payment rail (a Nuri user vs a bank, Bitcoin, Lightning, card or mobile-money destination).
Actions in the same list (Scan or paste, All contacts, New recipient) keep the outline glyph avatar.
Why now. The need is nuri-expo's. Its address book, its suggestions and the send step draw every
contact as a generated wordpet PNG passed as source, with coloured PNG logos for rails (§ Consumer
need), and the intended look cannot be built from DS parts: IconAvatar takes no text,
ListActionLeadingAvatar accepts only IconAvatar props, and nothing in the DS can overlay a part.
The playground boards (send.html, pure DS; send-recipient.html, outside the DS) show the target
and the gap; they are evidence, not the source of the need.
Proposed contract delta (not admitted)
Two public props on IconAvatar (initials, accessory), one public axis (accessorySurface),
their forwarding through ListActionLeadingAvatar, one chrome slot (inverse), one size leaf
(2xs), and the schema, codegen and engine rules they need. Nothing public is removed or narrowed
(the one internal reshape is under Class-scoped Contraction / Mixed evidence).
Public surface (what a consumer writes)
| RN | Web | |
|---|---|---|
| Initials content | <IconAvatar initials="EB" /> |
<nuri-icon-avatar initials="EB"> |
| Accessory glyph | accessory="nuri" (an IconName) |
accessory="nuri" |
| Accessory surface | accessorySurface="canvas" | "inverse" (default canvas) |
accessory-surface="…" |
| In a list row | <ListActionLeadingAvatar initials="EB" accessory="bank" /> |
<nuri-list-action-leading-avatar initials="EB" accessory="bank"> |
A Nuri contact: initials="EB" accessory="nuri" accessorySurface="inverse". Every other rail:
initials="CM" accessory="bank" (the canvas default). A photo: source={…} accessory="nuri" accessorySurface="inverse". Existing icon, source, variant, size, accent are unchanged.
A · Initials
A1 · Content. A new initials part (el: 'text') in the icon-avatar anatomy and an initials
slot with a scalar prop. 1–2 letters, derived and cased by the caller; the DS does not parse names.
The text leaf is single-line with tail truncation, so a longer string cannot break the circle (three
or more letters truncate; that is not a feature). Presence = a non-empty string: initials=""
behaves as absent (no initials mode, no text, no dev warning).
A2 · Treatment, locked by mechanism. The internal mode axis gains initials. In that mode the
root paints soft (bg-strong fill, text-primary letters) whatever variant/accent say, the
same way image mode already mutes the variant. variant and accent are documented no-ops in
initials mode. Type: md (17px), regular, on the md avatar (chosen by the requester on the board,
§B4); xs (13px), regular, on the sm avatar (not yet seen on a board).
Why lock it: ListAction forwards variant with an outline default, so without the lock every
person row would draw outline initials (transparent, muted letters) unless the caller remembered
variant="soft"; and variant="solid" accent="orange" would bring back per-person colour, which
the requester dropped.
Letter-spacing: the initials use their step's own letter-spacing (md −0.02em, xs 0). No tracking
token, no letter-spacing rule (dropped after review). On web the second letter's position does not
depend on letter-spacing (centred text, spacing trails every glyph), so the board's ring contact
(§B4) carries over unchanged; verified by probe at both values.
OS text size: the initials do not follow the OS text size. The avatar is decorative and a
fixed-size circle; the row's name, which is text, scales. Without this, large accessibility sizes
would truncate the initials inside the 48px circle and push them into the accessory. Mechanism (RN):
text leaves inside a decorative descriptor render with allowFontScaling={false}. icon-avatar is
the only decorative descriptor and the initials its only text leaf; the rule keys on existing
descriptor data (decorative, decision 50), not on the slot kind, so slot kinds carry no
presentation. Web: no rule (browser zoom scales the whole avatar; the type step is rem, so a larger
browser default font size grows the initials inside the px circle: accepted).
A3 · Precedence + the contentMode reshape. source > initials > icon. Today
contentMode = { slot, axis, present, absent } handles one slot. Proposed:
contentMode = { axis, present: [{ slot, value }, …], absent }; the first present slot wins (array
order is the precedence). icon-avatar: present: [{ slot: 'source', value: 'image' }, { slot: 'initials', value: 'initials' }], absent: 'glyph'. The generated one-time dev warning generalises
from "exactly one of icon or source" to "exactly one of icon, source or initials" (initials=""
counts as absent, A1).
A4 · Slot kind. SlotSpec.kind gains a scalar kind for this content, working name 'initials'.
prop is legal only on icon-name and image-source slots (Channel 7); rich text is
composition-only (the 2026-07-01 rule). A dedicated kind keeps that rule: only this kind of text
gets a prop. New guard: a text slot must be component or default. Today the RN codegen
turns a non-component, non-default kind: 'text' slot into a slotName?: string prop and routes it
as content (scripts/parsers/components-api.js:379, 492); no descriptor uses that path and no guard
forbids it. After this change the initials kind is the only scalar text prop in the DS, by
mechanism. The kind carries routing only, no presentation (no letter-spacing, no font-scaling rule).
Q1 · Initials shape · resolved after review. Scalar prop + new kind + mode lock, as above;
letter-spacing dropped (the step's own value); presence = non-empty string; the bare-textguard
closes the latent scalar path. Rejected: (a) a composition marker
(<IconAvatarInitials>EB</IconAvatarInitials>):contentModerequires a scalar prop slot, and
ListActionforwards nested-component props, not composition children, so it costs more engine
work for a worse 2-letter API; (b) lettingpropappear on anytextslot: it would undo the
composition-only rule for every component; (c) initials that followvariant: the two failure
modes in A2; (d) caps letter-spacing in any form (atracking.capstoken keyed on the kind, or a
TypographyNSflag that every Text would gain): dropped; (e) the two letters as separate nodes
with a gap: the smallest space step (3px) is about 0.18em at 17px, it does not scale with the text,
and splitting breaks single letters, accents, emoji and non-Latin names (and would make the DS
split names, which A1 rules out).
B · Accessory
B1 · Anatomy. Two parts: accessory (a view host: the disc) and accessoryIcon (an icon
leaf inside it). Slot accessory: { part: 'accessoryIcon', kind: 'icon-name', prop: 'accessory' }.
Public axis accessorySurface: 'canvas' | 'inverse', default canvas. No accessory accent.
The disc has no width or height: it sizes from its content, the 15px glyph plus space.xs (3px)
padding, so 21px in both surfaces (accessory: box: { padding: 'xs', radius: 'full' };
accessoryIcon: box: { width: '2xs', height: '2xs' }). Only existing BoxNS fields; the
canvas hairline takes no layout (B2); §B4 has the numbers.
B2 · Placement: a new, descriptor-only overlay. PartAnatomy gains
overlay?: 'corner-accessory' (named for the mechanism, not a corner coordinate). One value, which
bundles:
- the part leaves the flow; its box is pinned to the parent host's bottom-end corner and pushed out
byspace.xs(3px) on both axes, drawn above the parent's content; - a 2px knockout ring in
bg-canvas, outside the disc, taking no layout space; - a 1px
border-subtleseparation hairline inside the disc, taking no layout space, drawn when the
disc paints the ring's own colour (chrome: canvas) so a canvas disc stays distinct from its
canvas ring; aninversedisc has none. The hairline is the accessory's, like the image element's
ring: no chrome row gains a border, and no resolver change is needed; - content gating: an overlay host renders only when its slot has content (hosts always render today;
without this rule every avatar would draw an empty disc).
An overlay part is a non-interactive view host (no pressable, no interactive, no hit
target). Allowlist guard: only icon-avatar.accessory may declare overlay; any other use fails
CI and is a new Extension. This narrowly revisits the BleedNS position ("descriptors and View
cannot opt into overlap"): one descriptor part may overlap, View and every other descriptor still
cannot. Engines realise it generically, keyed off overlay, with no component logic:
- RN:
position: 'absolute'; the ring as an absolutely-positioned bordered sibling and the hairline
as an absolutely-positioned bordered child (inset 0), both the focus-ring technique already in
runtime/renderer.tsx. - Web:
position: absolute;box-shadow: 0 0 0 2px var(--nuri-bg-canvas)(+inset 0 0 0 1px var(--nuri-border-subtle)on a canvas disc);position: relativeon the root painting node,
not on thedisplay: contentscomponent host.
Q2 · Where overlap lives · resolved after review. A
PartAnatomyfield, as above: structure,
invariant per descriptor (no variant can move it) and invisible to the open primitives. Value
renamed from'bottom-end'to'corner-accessory'; ring, outset, hairline and gating stay bundled
in the one value (separate fields would be more surface for one consumer); the allowlist guard and
the non-interactive rule bound the precedent. Rejected: aBoxNSfield (box.anchor). Every
BoxNSfield becomes a flat prop on View, Pressable and Image (Object.keys(BOX_FIELDS)), which
would hand absolute positioning to every agent-written layout.
B3 · The two surfaces: chrome slots, accent-invariant by construction.
accessorySurface |
Paints | Realisation |
|---|---|---|
canvas (default · every other rail) |
canvas disc, text-primary glyph, 1px border-subtle hairline |
the existing chrome canvas (bg-canvas + text-primary); the hairline is the overlay's, no layout (B2) |
inverse (Nuri users) |
inverse disc (dark in light theme, light in dark), contrasting glyph | new chrome inverse = bg-inverse + text-on-inverse |
- Named for what it paints, following the
surfaceaxis on topbar and tab-bar (surface: canvas | transparent, the chrome slot each value paints). bg-inverse/text-on-inverseresolve to exactly the neutralaccent-solid/accent-on-solid
values in both themes (neutral-1-dark/-light,neutral-12-dark/-light). Soinversepaints
the requester's "solid + accent neutral" pixel for pixel, but under the lilac or orange accent it
stays neutral. A lilac Nuri badge cannot happen.- The existing
outlinevariant cannot serve: it paintstransparent+text-muted. Over a
photo the disc would be see-through, and the glyph would be the 5.85:1 muted the requester
rejected (text-primaryis 16.03:1 on canvas). inverse-mutedis already a chrome slot;inverseitself is not, though its roles exist. Only
inversebecomes a new public chrome slot. It also becomes achrome=value on View and
Pressable: admitted for the accessory; that availability sets no precedent for further chrome rows.accessorySurfacewithoutaccessoryis a no-op.
Q3 · Accessory shape · resolved after review. The narrow pair
accessory(glyph) +
accessorySurface(two values, defaultcanvas), no accent;canvasreuses the existing chrome
row, onlyinverseis new; the hairline is the accessory's, drawn without layout, so both surfaces
measure 21×21 like the board. Web attributeaccessory-surfacevia a generic camelCase→kebab-case
mapping for axis attributes in the web factory (today it reads axis names raw:factory.js:1008, 1116; single-word axes are unaffected). Rejected: (a) a nestedicon-avataras the accessory
(self-referencing component part at a new size): it exposes all five variants plus accent
(coloured rail badges by the back door), itsoutlinepaints the wrong pair, and a
self-referencing component part is untested in codegen; (b)solidvariant + a per-part
accent: 'neutral'pin: no descriptor pinspalette.accentper part today, and RN resolves accent
only as an upstream scope (resolve.ts· SEED-4), so it needs new engine work to avoid one palette
row; (c) a standaloneBadgecomponent: consumers still cannot overlap it, and it invites
count/status badges; (d)accessoryVariant: 'solid' | 'outline': reuses the variant words while
painting different pairs; (e) acanvas-outlinechrome row with a palette border: the border takes
layout on both engines, so the disc would be 23px, not the chosen 21px.
B4 · Geometry, as chosen on the board. The requester's first stated target was "22px disc, −3px,
2px ring, glyph about 16px". Measured on the board's first version (commit bb33e6e in
#228, by probe, not by eye): disc 22×22, outset 3/3, ring 2px, glyph
12.96px (<nuri-icon size="sm"> = 18px × scale(.72)); that page CSS never drew a 16px glyph. The
requester then iterated on the board (2026-10-24). Every row below was measured by probe on the 48px
avatar, light and dark (the fixed 24/18 row on the scratch comparison, the rest on the board), all
with a 3px outset (space.xs) and the 2px ring:
| Badge | Glyph | Padding | Initials | Verdict |
|---|---|---|---|---|
22, fixed (first version: commit bb33e6e in #228) |
12.96 | — | 13px / 400 (the dropped rule) | first reviewed look; off-scale |
24, fixed size.sm |
18 size.xs |
— | — | glyph nearly touches the hairline: reject |
| 24, fixed | 12 | — | 14.5px / 600 | balanced, but needs a 12px leaf |
18, fixed size.xs |
12 | — | 14.5px / 600 | lighter; glyph 2px from the hairline |
| 24, from content | 18 size.xs |
3 space.xs |
17px / 400 | covers about 9×5px of the second initial: reject |
| 27, from content | 15 | 6 space.sm |
17px / 400 | too big: reject |
| 21, from content | 15, new size.2xs |
3 space.xs |
17px / 400 | chosen by the requester; the evidence at main@d06e2cc |
- New size leaf
2xs= 15px (packages/spec/tokens/dimensions.ts), the step belowxs(18).
It sits on the 3px grid the space scale uses, not the size scale's own steps (18, 24, 36…). Cost:
SizeLeafwidens, sowidth="2xs"becomes legal on every box-bearing surface (doc rows move).
2xsis not an icon size. The token-free options were the 18px glyph rows above, both rejected. - The 2px ring stays a literal, mirroring
FOCUS_RING_WIDTH(the focus ring's 2px is a design
constant, not a token). Mintingborder.2for one ring is the alternative. The 1px hairline uses
the existingborder.1. - Known contact: at 17px the initials reach the ring, which touches the lower-right corner of a
wide second letter by about 1px (independent review probe: the second letter's bounding box
against the ring circle, Chromium/macOSsystem-ui: about 1.1px for M, 0.6–0.7px for C and K,
about 0 for B). Font-dependent. Seen and accepted on the board (14.5px initials would clear it); the
device check is in the native residue (§ Downstream verification plan). - The evidence board draws this geometry (
packages/playground/pages/send-recipient.htmlon
main@d06e2cc, feat(playground): Send + Send-to-bank boards — the nuri-expo send journey as DS evidence #228; page CSS, still outside the DS; X17, X19 and X16
share the classes). Probed: all 12 badges (6 inverse, 6 canvas) measure 21×21 with no width or
height set, a 15px glyph, 3px padding, 3/3 outset, the ring and the canvas hairline as box-shadows
(no layout), and 17px / 400 initials; the row titles keep md's −0.34px. The board sets the initials
at +0.05em (0.85px); the contract uses the step's −0.02em (A2), which does not move the second
letter. - The
nuriglyph looks optically larger than the outline glyphs at this size; that is a separate
Correction for the DS maintenance form (noted in the five-glyph request), not a geometry question.
Q4 · Sizes · resolved after review. The accessory is specified for
md(48) only. Onsm(24)
a proportional badge would be about 10px, too small for a legible glyph. Enforcement:
documentation, stated in two places: the generated API table's Notes foraccessoryand
accessorySurface(NOTE_BY_PROP,packages/doc/pipeline/component-api-ir.js) and the
icon-avatarpage lead (same file). The RN codegen emits no per-prop TSDoc today, and this request
adds no channel for it. The one forwarding consumer (ListAction) always rendersmd. A generated
dev warning would need aSlotSpecfield: more schema for one warning. Not proposed.
C · ListAction forwarding
leadingAvatar.props gains initials: '$slot.initials', accessory: '$slot.accessory',
accessorySurface: '$slot.accessorySurface'. The generated ListActionLeadingAvatar marker gains
the three props / attributes. Nothing else changes in list-action. Because of the mode lock (A2),
<ListActionLeadingAvatar initials="EB" /> paints soft despite the row's outline default.
SelectTrigger / SelectField avatars do not forward (no consumer).
Codegen: forwarded marker props are typed from the target component's api, generically
(public axis → its value union; slot kind → IconName / ImageSourcePropType / string; accent →
Accent), replacing the hand special-cases in slotPropTs (components-api.js:318-325, which today
types only accent, variant and the SLOT_PROP_TS map, and would emit unknown for the three new
props).
D · Accessibility
IconAvatar stays decorative (decision 50): initials and the accessory are hidden from assistive
tech along with the circle.
Contract: the accessory is never the only carrier of the rail. The rail is named in visible
row text or in the row's accessibilityLabel; a handle is not a name. Today send.html labels the
bank row "Claude M., N26 account ending 4521" but a Nuri row "Emil Berg, at emil" (send.html:67),
which implies the rail without naming it. The rebuilt send.html Nuri rows read "Emil Berg, Nuri
user, @emil".
Q5 · Labels · resolved after review. No
accessoryLabelprop; the contract sentence above. A
labelled standalone avatar is anti-scope until a consumer shows one.
Schema / codegen / engine ledger (what moves deliberately)
| # | Where | Change | Guard |
|---|---|---|---|
| 1 | schema.ts PartAnatomy |
overlay?: 'corner-accessory' |
Guard-F pin moves · new overlay allowlist guard (icon-avatar.accessory only; non-interactive view host) |
| 2 | schema.ts SlotSpec.kind |
+ 'initials' (scalar, prop-legal) |
Guard-F pin moves · Channel 7 admits the kind · new guard: a text slot is component or default |
| 3 | schema.ts ComponentApi.propMaps.contentMode |
single slot → ordered present list |
Guard-F pin moves |
| 4 | schema.ts PaletteChrome + palette-surface.ts |
+ 'inverse' |
Guard-F pin moves (leafUnions) · palette guards (web Guard D · RN palette.js PALETTE_CONTRACT cell check) |
| 5 | tokens/dimensions.ts size |
+ 2xs: 15px |
dimension-token oracle |
| 6 | scripts/parsers/components-api.js |
forwarded marker props typed from the target's api (generic) | component-api tests + RN type-tests |
| 7 | web factory.js |
axis attributes camelCase → kebab-case (generic; single-word axes unchanged) | factory test |
| 8 | RN renderer.tsx + web factory.js / CSS |
the corner-accessory overlay: placement, ring, canvas hairline, content gating |
render-smoke + factory test |
| 9 | RN renderer.tsx |
text leaves in a decorative descriptor: allowFontScaling={false} |
render-smoke |
Guard F moves for four frozen surfaces (rows 1–4), in one deliberate, commented move. No tracking
token, no kind-keyed presentation rule, no resolver change.
Proposed anti-scope (refused generalisation)
- No person colour palettes of any kind (pastel, Radix, hue wheel, hash-to-colour); initials are
alwayssoft.variantandaccentare documented no-ops in initials mode. - No coloured rail badges and no per-rail colour table; colour is not a rail code. Exactly two
accessory surfaces; no accessory accent; no lilac Nuri badge. - No KYC / "verified" / status marks, no count or notification badges, no online dots. The
accessory is a rail glyph from the DS icon set: no image and no initials in the accessory, and no
status or motion glyphs as accessory content (check-circle,warning-circle,cross-circle,
spinner). The type staysIconName: which glyphs are rails is product knowledge. - No
overlayvalue other than'corner-accessory'; nooverlayon any part other than
icon-avatar.accessory(allowlist guard; another use is a new Extension); nooverlayon
View/Pressable/Image; overlay parts are non-interactive; no general positioning vocabulary. - No surface-adaptive ring: it is
bg-canvas. On a non-canvas surface (astrongcard, a pressed
bg-subtlerow while the finger is down) it shows as a canvas ring. No ring width or colour option. inverseis admitted for the accessory; its availability aschrome=on View and Pressable sets
no precedent for further chrome rows.- No accessory on
sm; no new avatar size. nuri-expo's 72px "hero" avatar
(components/SendRecipientContact.tsx:16"the DS avatar only comes in list sizes") stays app-owned. 2xsis a size leaf, not an icon size: no new icon step.- No name parsing, casing or grapheme logic in the DS; no image fallback to initials (no load or
error states: the image-mode anti-scope stands; the caller passes one content). Three or more
letters truncate; that is not a feature. - No letter-spacing option or token anywhere: initials use their step's letter-spacing. No
text-transform: the caller supplies capitals. - Initials do not follow the OS text size (RN); no font-scaling option.
accessorySurfacewithoutaccessoryis a no-op.- No generated avatars: nuri-expo's wordpets stay an app decision.
- No forwarding through
SelectTrigger/SelectField.
Consumer need, gap, and current workaround (before admission)
- Consumer / owner: @darioaschero, for nuri-com/nuri-expo: send S1 (recipient list), the search
mode, the address book. - Concrete use case: a row "Emil Berg · @emil" with EB initials and an inverse Nuri accessory;
"Claude M. · N26 ···4521" with CM initials and a canvasbankaccessory; the same accessory over a
profile photo. - Gap:
IconAvatartakes a glyph or an image only;ListActionLeadingAvatarforwards only
name/variant/accent/source; nothing in the DS can overlay a part. - Current workaround (DS evidence page):
packages/playground/pages/send-recipient.htmlon
main@d06e2cc(feat(playground): Send + Send-to-bank boards — the nuri-expo send journey as DS evidence #228). Every person row is hand-built: anuri-viewrow- a
<span class="xav">circle + a<span class="xbadge">badge, styled by a
<style data-outside-ds>block, with the chevron copied by hand.
- a
- Current workaround (the app): nuri-expo (
feat/send-shortcut-1741@bdf020b1) draws every
contact as a generated wordpet PNG passed assource:
screens/addressBook/AddressBookScreen.tsx:140andcomponents/AddressBookSuggestions.tsx:106
(<ListActionLeadingAvatar source={contactAvatar(…)} />), plus a raw 72pxImagein
components/SendRecipientContact.tsx. Rail marks are coloured PNG logos
(lib/addressBook/kindImages.ts).
Available consumer-local prototype or evidence (before admission)
- Boards on
main@d06e2cc(feat(playground): Send + Send-to-bank boards — the nuri-expo send journey as DS evidence #228):
packages/playground/pages/send-recipient.html(outside the DS, with disclaimer): X17 target
(initials + accessory), X19 the same accessory on photos (randomuser.me placeholder portraits,
inlined), X16 what nuri-expo draws today (vendored wordpets, for reference); its closing GAP LOG
lists what this contract must cover.packages/playground/pages/send.html(pure DS): the decided
send boards, whose person rows this contract would rebuild. - App code: nuri-expo
feat/send-shortcut-1741@bdf020b1, the files named above; the app already
derives letters (lib/addressBook/initial.ts,initialOf, for index grouping). - External references (Mobbin, iOS). The pattern of a network/rail badge at the bottom-right of a
person avatar recurs across fintech send flows:- Revolut: "contacts on Revolut" list, coloured initials + a black "R" badge with a canvas ring
(screen); the same badge on a
photo (screen) and on search
results (screen). - Wise: request list with neutral initials + a Wise badge, beside an outline-glyph action row
"Anyone" (screen); search
"On Wise" (screen). Closest
to this target: neutral initials, glyph actions in the same list. - Monzo: split-bill contacts, initials and photos with an "M" badge
(screen); an account-type
glyph badge on initials (screen). - Honest limit: in these references the badge mostly marks in-network users (Revolut,
Wise, Monzo). Badging every external rail with a monochrome canvas glyph is Nuri's own
extension of the pattern, and Revolut and Monzo use per-person colours, which this request drops.
- Revolut: "contacts on Revolut" list, coloured initials + a black "R" badge with a canvas ring
- Corrections found while preparing this request (by probe, on the board's first version:
commitbb33e6ein feat(playground): Send + Send-to-bank boards — the nuri-expo send journey as DS evidence #228): the accessory glyph was 12.96px, not ~16px
(§B4). The initials rendered at 13px / weight 400: the page rulefont: 600 15px/1 inheritwas
an invalid shorthand (afontvalue cannot end ininherit), so the browser dropped it. The "600,
size-scaled" treatment in the first decision was therefore never on screen; once the rule was fixed
and compared, the requester chosemd(17px) regular (§A2, §B4). - Corrections from the independent review (DS admission: IconAvatar initials + corner accessory (+ ListAction forwarding) #231 (comment)):
the outline disc as first proposed (a chrome row with a palette border) would measure 23px, not
21px; Guard F covers four surfaces, not three; the web axis attribute and the forwarded-prop types
needed generic changes; the initials-to-ring contact is about 1px, not ~2px. All are answered in
the amended text above. - Missing evidence and owner: an exact nuri-com/nuri-expo issue or PR for the send S1 /
address-book avatar change; owner @darioaschero.
Class-scoped Contraction / Mixed evidence
Filed as an Extension. One internal reshape is listed here in case the architect reads it as a
narrowing: ComponentApi.propMaps.contentMode changes from one slot to an ordered list (§A3). Its
only user is icon-avatar, and no descriptor is authored outside this repo (contentMode appears
only in schema.ts, icon-avatar.ts, its generated twin
packages/prototype/generated/descriptors/icon-avatar.js, scripts/parsers/components-api.js,
packages/prototype/factory/factory.js and the Guard-F pin). The new bare-text guard (A4) forbids
a slot shape no descriptor uses. Affected consumers: none; migration: the one in-repo descriptor, in
the same PR; rollback: revert that PR.
Class-scoped Extension / Mixed evidence
- Recurrence: inside nuri-expo, three list surfaces render person avatars today (address book
list, address-book suggestions, the activity sheet's contacts viacontactAvatar), and send S1 +
search add two more. Outside: three of the reference fintech apps above show initials and an
in-network badge. The overlap itself (a glyph badge on a circle) is the one composition no consumer
can build from DS parts. Product decision, not recurrence: showing the rail on every external
contact (thecanvasaccessory) is the consumer owner's product decision for Nuri; the references
badge only in-network users. - DS ownership: the look (circle, initials type step, accessory disc, ring, hairline, the two
surfaces) and its cross-platform mechanics (out-of-flow placement, knockout ring, content gating,
fixed-size decorative text) are presentation vocabulary. Which rail a contact uses, the letters,
and the row's label stay consumer-owned. - Trade study:
- Keep it consumer-owned (an app wrapper around
IconAvatarwith an absolute badge): works in
nuri-expo's RN code, butListActionLeadingAvataronly acceptsIconAvatarprops, so the app
must hand-build rows and loses the list-row contract (72px floor, press, a11y, trail icon).
Rejected. - A nested
icon-avataras the accessory, aBadgecomponent, a per-part accent pin, a
composition initials marker, a borderedcanvas-outlinechrome row: rejected in Q1 and Q3. - Off-scale literals (22/13) baked into the engines: rejected; hand numbers in the engines are
exactly what the token discipline andrn-token-escape-hatchguard exist to stop. - Existing leaves only (an 18px glyph, in a fixed or a padded 24px badge): rejected on the
board (§B4). overlayas aBoxNSfield: rejected (Q2).- Caps letter-spacing (
tracking.caps): dropped after review (Q1).
- Keep it consumer-owned (an app wrapper around
- Contract cost: 4 frozen-schema surfaces (Guard-F pin, one move), 1 chrome slot, 1 size leaf,
1 generic overlay realisation per engine (placement, ring, canvas hairline, gating), 1 RN
font-scaling rule for decorative text, 1 new slot kind, 2 new guards (overlay allowlist; bare
textslots), generic forwarded-prop typing in the codegen, generic axis-attribute kebab mapping in
the web factory, thecontentModereader reshaped in 2 places,icon-avatar+list-action
descriptor data, doc pages. - Compatibility: additive for consumers. With none of the new props set,
IconAvatarand
ListActionrender as today: theinitialsleaf has no content, and the overlay host is gated.
The axis-attribute mapping changes nothing for today's single-word axes. Existing render-smoke
snapshots should not move; any that do must be justified. Generated deltas expected: RN
icon-avatar.ts/list-action.tsadapters,recipes.ts,palette.ts,tokens.ts/
token-paths.ts(the2xsleaf); web descriptor twins,palette.css(1 row), token CSS (2xs) +
box.css(the2xsleaf), the overlay rule; doc pages (icon-avatar,list-action, the palette
and dimension foundations, every page whosewidth/height/chromeunion widens). - Anti-scope: see "Proposed anti-scope" under the contract delta.
Requested DS architect / admission recorder
@darioaschero · second human architectural reviewer: none · sole-architect exception (@darioaschero) · independent review: #231 (comment) (docs/design-system-admission.md, "Named exception · sole DS architect (@darioaschero)", #229)
Architect decision record location (architect-owned)
(Leave empty on initial request: architect-owned.)
Proposed bounded execution scope if admitted
- Spec:
components/schema.ts(deltas 1–4),components/icon-avatar.ts(anatomyinitials,
accessorywithoverlay: 'corner-accessory',accessoryIcon;modegainsinitials;
accessorySurfaceaxis + defaultcanvas;contentModelist; slots),components/list-action.ts
(3 forwarded props),tokens/dimensions.ts(2xs),axes/palette-surface.ts(theinverse
chrome row). - Codegen / guards:
scripts/parsers/components-api.js(the scalarinitialskind →string
prop / attribute;contentModelist with presence = non-empty for initials; the generalised
warning; forwarded marker props typed from the target's api),scripts/parsers/palette.js
(PALETTE_CONTRACTrow),scripts/docs-drift.test.js(Guard-F pin for the four surfaces, moved
once, the move commented),scripts/component-api.test.js(Channel 7 admits theinitialskind;
the bare-textguard; the overlay allowlist + non-interactive guard), the dimension and palette
oracles. - RN:
runtime/renderer.tsx(overlay placement + ring + canvas hairline + content gating;
initials routed to the text leaf;allowFontScaling={false}on text leaves of a decorative
descriptor), regeneratedgenerated/, render-smoke + type-tests (IconAvatarProps,
ListActionLeadingAvatarPropsexact). - Web:
factory/factory.js(overlay, ring, hairline, gating; camelCase→kebab-case axis
attributes), the overlay CSS rule, regenerated styles + descriptor twins,factory.test.js. - Playground:
component/icon-avatar.htmlandcomponent/list-action.htmlproofs;send.html
person rows rebuilt aslist-actionrows withinitials+accessory(Nuri rows labelled "Emil
Berg, Nuri user, @emil"); deletesend-recipient.htmland its card inpages/index.html
(line 51 onmain@d06e2cc). - Expo-demo: one committable screen or section with the person rows (never the operator-local
screens/index.tsx/Overview.tsx). - Doc: generated pages + the IR notes (
packages/doc/pipeline/component-api-ir.js: the
icon-avatarlead and theNOTE_BY_PROPnotes foraccessory/accessorySurface,mdonly) +
the drift pins, kept in sync. - NOT in scope: the five-glyph request (its own issue), nuri-expo changes (the consumer's PR),
SelectTrigger/SelectFieldforwarding, a hero size, a search trigger, the merge.
Downstream verification plan (before Ready)
- Repository checks / CI: root
npm run build+npm test; the 5 gates (spec·prototype
·doc·rnrender-smoke + tsc ·expo-demotsc), including@nuri/doc(npm test+
build +git diff --exit-code packages/doc/generated/); re-emit determinism (two builds,
identical hashes); only the generated files listed above differ. - Contract / type proof:
IconAvatarPropsandListActionLeadingAvatarPropsexact (the three
new keys,initials: string,accessory: IconName,accessorySurface: 'canvas' | 'inverse',
nounknown, stillchildren?: never); the Guard-F pin moved once for the four surfaces;
the overlay allowlist and bare-textguards fail on a synthetic violating descriptor;
existing snapshots unchanged or justified. - Rendered proof, web (by probe, not by eye): at 48px, for initials, image and glyph, each
with no accessory,canvasandinverse, light and dark: both surfaces measure 21×21
with no width/height set (15px glyph + 3px padding), outset 3/3, ring 2px inbg-canvas, the
canvashairline 1pxborder-subtlewith no layout, glyph 15×15, initials 17px / 400 with
the step's letter-spacing (−0.34px); theinversedisc colour equals--nuri-accent-solid
underdata-accent="neutral"and stays equal under lilac and orange; no accessory node
exists whenaccessoryis absent;initials=""renders glyph mode with no text node; the
overlay's containing block is the root painting node, not thedisplay: contentshost;
accessory-surfaceis observed and live-updates; the host staysaria-hidden. - Rendered proof, RN-web (Expo web preview, by probe): the same geometry on host nodes (both
surfaces 21×21); the accessory is drawn above an image;variant/accentare no-ops in
initials mode; the initialsTextcarriesallowFontScaling={false}(render-smoke). - Native residue (operator, device): Android drawing the accessory 3px outside the 48px
avatar (RN Android overflow), the ring and hairline at device pixel densities, VoiceOver/
TalkBack reading the row label (including "Nuri user") and not the letters, the 17px initials
against the ring with wide second letters (B, M: the accepted ~1px contact, §B4) at the
device's system font, and OS text size at the largest accessibility setting (iOS Larger
Accessibility Sizes, Android font scale max): initials stay fixed and untruncated, the row
name scales. - Real consumer: an exact nuri-com/nuri-expo PR replacing the wordpet
sourcein
AddressBookScreen.tsx:140andAddressBookSuggestions.tsx:106withinitials+accessory.
Attached during Verification/Done, never before Ready.
Acceptance criteria and Verification/Done evidence
send.htmlperson rows are pure DSlist-actionrows, Nuri rows labelled with the rail ("Emil
Berg, Nuri user, @emil");send-recipient.htmland its index card are gone; nodata-outside-ds
CSS remains in the playground.- The geometry and colour probes above pass on web and RN-web, light and dark; both surfaces 21×21.
- Accent invariance: the accessory never changes colour with the accent; initials never leave
soft. - Existing
IconAvatar(glyph, image) andListActionrenders unchanged. - Full gate suite green, including
@nuri/doc; regeneration deterministic; the two new guards
present and proven to fail on a violation. - Doc pages generated for the new props, the
inversechrome slot and the2xsleaf; the
anti-scope sentences on theicon-avatarpage (decorative accessory, the rail always named in row
text or label,mdonly, canvas ring, fixed-size initials, step letter-spacing) andmdonly in
the API table notes. - The schema deltas recorded in the Guard-F pin comment + this record (the decisionlog is archived).
- This record's status line flipped in the SAME PR that ships the implementation.
Owning repository and base branch
nuri-com/nuri-design-system @ main (baseline main@d06e2cc)
Dependencies and human decisions
- Evidence: feat(playground): Send + Send-to-bank boards — the nuri-expo send journey as DS evidence #228 (merged as
d06e2cc; the boards cited above). - Precedents: the image-mode arc (IconAvatar
source); decision 50 (decorative avatar); the
surfaceaxis on topbar and tab-bar (axis named for the chrome slot it paints). - Companion request (independent, filed separately): the five icon glyphs
DS admission: six icon glyphs (address-book, id-card, selfie, lock, lock-open, phone) + the nuri mark at 22/32 #230. - Independent review posted:
DS admission: IconAvatar initials + corner accessory (+ ListAction forwarding) #231 (comment) (verdict "Admit
with changes"). Q1–Q5 are answered in this amended text following the architect's direction. - Decision Needed: the admission decision by @darioaschero under the sole-architect exception
(docs/design-system-admission.md, "Named exception · sole DS architect (@darioaschero)",
docs(admission): named sole-architect exception for @darioaschero #229), recorded in an architect-owned comment that answers each concern
the review raised. The geometry was chosen by the requester on the board (§B4: a 21px badge from a
15px glyph + 3px padding, 17px regular initials).
Risk class
Product
Authority, responsibility, and accountability evidence
- May change if admitted: only the artifacts under "Proposed bounded execution scope".
- Excluded: the five-glyph request, nuri-expo code (the consumer's own PR),
SelectTrigger/
SelectFieldforwarding, a hero avatar size, a search trigger (evaluated separately against
SelectField). - Approval boundaries: the requester decided the target on 2026-10-24; the drafting agent may
not admit, record decisions, move Project state, tag or merge. Admission is @darioaschero's record
under the sole-architect exception, informed by the independent review session; execution starts
only at ProjectReady. Amended 2026-10-24 after the independent review, following the
architect's direction on the open calls. - Independent verification: the probes, gates and device checks in the verification plan, plus
the nuri-com/nuri-expo PR attached at Verification/Done.
Release, migration, and documentation plan
- Release note: the two props +
accessorySurface, theListActionforwarding, theinverse
chrome slot, the2xs= 15px size leaf, the descriptor-onlyoverlay: 'corner-accessory',
fixed-size initials on RN, camelCase→kebab-case axis attributes on web. - Migration: none for consumers (additive). Inside the repo:
send-recipient.htmland its index
card deleted,send.htmlperson rows rebuilt aslist-actionrows. - Documentation: generated pages for
icon-avatar,list-actionand the palette and dimension
foundations; the Guard-F pin comment records the schema deltas. - Rollback: revert the single 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
Admitdecision 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; ProjectReadyremains 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#numberform.
Tick all fourteen boxes. All 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 on this issue and its link goes into the decision record; the Extension evidence is above;
executed Expo/device proof comes after implementation; references use canonical owner/repo#number
form.
Activity
darioaschero commentedon Oct 24, 2026
Independent review (sole-architect exception)
Fresh session, no part in drafting the request, the evidence or any brief. Inputs: this issue, the repository at
main@45918bb(d06e2ccplus the policy text from #229; no code or data differs from the cited baseline), anddocs/design-system-admission.md.prompts/was not read. Read-only: no files, branches, decisions or Project state touched. The evidence page was served frommainand probed in Chromium on macOS (light theme). This review informs the decision; it does not make it.1 · Claims
Confirmed against
mainicon/sourceslots,sourcewins, internalmode: glyph | imageviapropMaps.contentMode, image mode mutes the root toghost,md48 /sm24,decorative: true(packages/spec/components/icon-avatar.ts:98-130). The RN dev warning text matches (packages/rn/generated/components/icon-avatar.ts:41).name → icon,variant|outline,accent,source, and never forwardssize, so it always rendersmd(packages/spec/components/list-action.ts:24-31).contentMode = { slot, axis, present, absent }, one slot (schema.ts:471), and the codegen requires a scalarpropslot (scripts/parsers/components-api.js:160).propis legal only onicon-name/image-source(Channel 7,scripts/component-api.test.js:515). The BleedNS comment says descriptors and View cannot opt into overlap (schema.ts:174-184).packages/rn/primitives/View.tsx:20,Pressable.tsx:29,Image.tsx:41;BOX_KEYS = Object.keys(BOX_FIELDS)). Text takes all of TypographyNS (Text.tsx:17), so rejected option (d) is costed correctly.soft=bg-strong+text-primary, both accent-invariant chrome roles, so the initials mode lock does makeaccenta no-op (palette-surface.ts:86,colours.ts:273-296).outline= transparent +text-muted+border-subtle(:89).bg-inverse/text-on-inverseequal neutralaccent-solid/accent-on-solidin both themes (colours.ts:279,284vs:314,316).inverse-mutedis a chrome slot andinverseis not (schema.ts:223).resolvePalettedropsborderon chrome slots (packages/rn/runtime/resolve.ts:219). Web palette CSS emits a border for any row that has one (packages/prototype/pipeline/parsers/palette-css.js:103).FOCUS_RING_WIDTH = 2(packages/rn/runtime/renderer.tsx:51, 512-537). No descriptor pinspalette.accentper part. Accent is an upstream scope in RN (resolve.ts:203-207).space.xs3 /sm6,size18/24/36/…,borderhas only1, typemd17 / −0.02,sm−0.01,xs13 / 0 (tokens/dimensions.ts,tokens/typography.ts).text-primaryon canvas 16.03:1,text-muted5.85:1.nuri,bank,bitcoin,lightning,cardexist. No mobile-money glyph (the companion DS admission: six icon glyphs (address-book, id-card, selfie, lock, lock-open, phone) + the nuri mark at 22/32 #230 covers glyphs).packages/playground/pages/send-recipient.html), by probe: all 12 badges (6 solid, 6 outline) are 21×21 with no width or height set, with a 15×15 glyph, 3px padding, 3/3 outset, a 2pxbg-canvasring, and initials at 17px / 400 with 0.85px letter-spacing. Row titles keep −0.34px. The index card sits atpages/index.html:51.send.htmlperson rows are rail glyphs today (nurisoft,banksolid lilac, the Bitcoinsource), andsend.htmlhas no page CSS.Wrong or incomplete
inset 0 0 0 1pxbox-shadow, which takes no layout space (send-recipient.html:31-32). B3 realises it as a chrome row with aborderchannel. That border takes layout on both engines (webborder: var(--nuri-border-1) solid …,palette-css.js:103; RNborderWidth: PALETTE_BORDER_WIDTH,resolve.ts:325-327). On a disc sized by its content that gives 15 + 6 + 2 = 23px. I probed it: a 1px layout border on the board's outline badge measures 23×23 even underborder-box, and its leading edge moves 2px further into the avatar. Every non-Nuri row would differ from the chosen look and would push further into the second initial. The verification probe "badge 21×21 …outline" would fail as specified.PaletteChromeis pinned member-for-member in Guard F'sleafUnions(scripts/docs-drift.test.js:595), so ledger row 4 also moves the Guard-F pin. The changed frozen surfaces are PartAnatomy,SlotSpec.kind,contentModeandPaletteChrome.accessory-variant="…"does not work on<nuri-icon-avatar>today. The factory reads public axes by raw name:observed = [...axisNames, …](factory.js:1008) andthis.getAttribute(axis)(:1116). No public axis is multi-word yet, soaccessoryVariantwould observe a lowercasedaccessoryvariant, notaccessory-variant. The kebab form works only on the ListAction marker, whose attributes are converted kebab→camel (factory.js:322-324). This needs a generic factory change that is not among the "four generic rules", or a one-word axis name.unknown.slotPropTstypes marker props only foraccent,variant(from the target'svariantaxis) and theSLOT_PROP_TShand map (components-api.js:318-325). Soinitials,accessoryandaccessoryVariantwould emitunknownuntil the codegen derives them from the target's api. That change is not in the execution scope, and the exact-type proof in the verification plan depends on it.prop, but not as a mechanism. The RN codegen already turns a non-component, non-defaultkind: 'text'slot into aslotName?: stringprop and routes it as content (components-api.js:379, 492). No descriptor uses that path and no guard forbids it. The composition-only rule is convention plus Channel 7, not an enforced invariant.contentModeoccurrence list leaves out the generated twinpackages/prototype/generated/descriptors/icon-avatar.js.Could not confirm
AddressBookScreen.tsx:140,AddressBookSuggestions.tsx:106,SendRecipientContact.tsx:16,initial.ts,kindImages.ts), and the in-app recurrence count. These are outside this review's inputs.fontshorthand). Commitbb33e6eis not onmain(feat(playground): Send + Send-to-bank boards — the nuri-expo send journey as DS evidence #228 was squash-merged). The page's own comment records the same history.system-ui) gives about 1.1px into the ring for "CM", 0.6–0.7px for "SC" and "AK", and about 0 for "EB". The order of magnitude agrees, the figure does not, and it depends on the font. Dark theme was not probed; the geometry does not depend on theme.mainoutsideprompts/. Under the policy, the consumer need is nuri-expo's; the playground's gap is not evidence by itself.2 · Risks to the shared contract
chrome=values, also public on View and Pressable. One size leaf, which widenswidth,height,minWidthandminHeighton every box surface. A new token family (tracking). Four Guard-F surfaces. New engine semantics: out-of-flow placement, a knockout ring, a content-gated host (hosts always render today,schema.ts:317-338), and presentation keyed on a slot kind. Plus the two codegen and factory changes found above. A alone (initials + mode lock + forwarding) touches 2 Guard-F surfaces and no layout vocabulary.PartAnatomy.overlayis the first time a descriptor can lay one part over another, which narrows the BleedNS position. As one enum value,'bottom-end'reads like general positioning vocabulary and invitestop-endnext, while it actually bundles corner, outset, ring, z-order and content gating. Without a mechanical bound, the next status dot or tab-bar count will cite it.tracking.capson theinitialskind is a new coupling that no gate sees.packages/rn/runtimeorprimitivessetsallowFontScalingormaxFontSizeMultiplier. At large text sizes, 17px initials grow inside a fixed 48px circle, truncate ("E…") and run deeper into the accessory. This is not in the verification plan.display: contents.aria-label="Emil Berg, at emil"(send.html:67) and"Emil Berg, @emil"(:213). The handle implies Nuri; it does not say it. The bank rows do name it. The proposed contract sentence would be failed by the evidence it cites.variant="solid" accent=…, which is exactly howsend.html:74drawsbankin solid lilac today. "Colour is not a rail code" stays guidance, not mechanism, for glyph avatars.3 · Open questions
Q1 · Initials shape. Agree: a scalar prop, a dedicated kind and the mode lock. The mode lock is the most valuable part, because it removes the outline-default footgun and the colour back door by mechanism. The new kind is acceptable and is actually narrower than today, provided the same change closes the latent bare-
textscalar path (a guard: atextslot must becomponentordefault). For letter-spacing, the smallest contract ships initials at the step's own −0.02em and dropstracking.caps. If the architect keeps it (the requester chose it on the board), record that a slot kind now carries presentation for this one kind only, and add a two-engine guard that only initials-kind content leaves its step's letter-spacing. Also define presence as a non-empty string.contentModetests!= null, soinitials=""would draw a blank soft circle.Q2 · Where overlap lives. Agree: PartAnatomy, not BoxNS. BoxNS would hand positioning to View, Pressable and Image, which I verified. Add:
icon-avatar.accessorymay declareoverlayand a second use fails CI until it is admitted;viewhost;Keep the ring and outset bundled; separate fields would add surface for one consumer. Consider a value name that reads as this mechanism rather than as a corner coordinate.
Q3 · Accessory shape. Agree with the narrow pair: a glyph, two treatments, no accent, painted by chrome slots. Changes:
solid/outlinereuse the variant words but paint different pairs: accessorysolidis notaccent-solid, and accessoryoutlineis not the outline variant. Agents will assume they follow the variant rows. The existing precedent issurface: canvas | transparenton topbar and tab-bar, an axis named for the chrome slot it paints (topbar.ts:109-112,tab-bar.ts:69-72). I recommend values named for what they paint.inverseneeds to become a public chrome slot.Q4 · Sizes. Agree:
mdonly, enforced by documentation, since the single forwarding consumer is alwaysmd. Put the sentence in the generated prop docs as well as the doc page; agents choose by the type surface, andsize="sm" accessory=…typechecks. A schema field for one warning is not worth it.Q5 · Labels. Agree: no
accessoryLabel. Sharpen the contract text to say the accessory is never the only carrier of the rail: the rail must be named in visible row text or in the row'saccessibilityLabel. A handle is not a name. The rebuiltsend.htmlshould model this on Nuri rows.4 · Anti-scope: missing or too loose
accessory: IconNameadmits every glyph, includingcheck-circle,warning-circleandcross-circle, which is a verified mark by the back door, andspinner, the motion glyph. Name these as refused accessory content. Do not narrow the type to a rail list; which glyphs are rails is product knowledge.2xs: not an icon size, so no new icon step.tracking: one row, not a scale, so notightorwideto follow.variantandaccentare documented no-ops in initials mode.accessoryVariantwithoutaccessoryis a no-op.5 · Verdict: Admit with changes
The initials half is sound and well bounded. The accessory half is the right shape, but its stated realisation does not reproduce the evidence, and its execution scope omits work the plan depends on. Before an
Admitrecord:components-api.jsto the scope.PaletteChrome.textscalar slots, and the kind-keyed letter-spacing guard if tracking is kept.initials="", the web overlay anchoring to the painting node, and both treatments measuring 21×21.These are the architect's calls: whether to drop
tracking.caps(the smaller contract), and whether to admit A + C-initials now and B after change 1 is settled.darioaschero commentedon Oct 24, 2026
Request amended after independent review
The issue body is amended to answer the independent review ("Admit with changes"), following the architect's direction on the open calls. No decision is recorded here and no Project state moved. The admission decision remains @darioaschero's, in an architect-owned comment.
Review §1 · claims found wrong or incomplete
box-shadow, RN an absolutely positioned bordered child (the focus-ring technique). It appears when the disc paints the ring's colour (chrome: canvas). No chrome row gains a border and no resolver change is needed. Both surfaces measure 21×21 (B1, B2, B3), and the probe checks both.SlotSpec.kind,contentMode,PaletteChrome(leafUnions), in one commented move.accessory-variantdoes not work on<nuri-icon-avatar>accessorySurface. A generic camelCase→kebab-case mapping for axis attributes in the web factory is in scope (ledger row 7,accessory-surface); single-word axes are unchanged.unknowncomponents-api.js(C, ledger row 6). The type proof requiresinitials: string,accessory: IconName,accessorySurface: 'canvas' | 'inverse', and nounknown.textscalar path is unguardedtextslot must becomponentordefault(A4, ledger row 2). After it, theinitialskind is the only scalar text prop, by mechanism.contentModeoccurrences omit the generated twinpackages/prototype/generated/descriptors/icon-avatar.js(Contraction section).Could not confirm
bb33e6e: unchanged. They remain the requester's evidence, outside the review's inputs.Review §2 · risks
inverse;canvasreuses the existing row) instead of two. Notrackingtoken family, no kind-keyed presentation rule, no RN resolver change. Added: two guards, generic codegen typing, the factory axis mapping, and one RN font-scaling rule.overlay: 'corner-accessory'. An overlay part is a non-interactiveviewhost, rendered only when its slot has content. An allowlist guard admits onlyicon-avatar.accessory; any other use fails CI and is a new Extension. B2 states that this narrowly revisits the BleedNS position.tracking.capsis dropped, and initials use the step's letter-spacing. The kind carries routing only.allowFontScaling={false}; web unaffected), stated in A2 and the anti-scope, and tested at the largest accessibility size in the native residue.position: relativegoes on the root painting node, not thedisplay: contentshost, and the probe checks it.accessibilityLabel, and a handle is not a name. The rebuilt Nuri rows read "Emil Berg, Nuri user, @emil" (scope, acceptance 1).variant="solid" accent=…, so "colour is not a rail code" stays guidance there. The rebuiltsend.htmlperson rows no longer use glyph avatars.Review §3 · open questions (each now marked "resolved after review" in the body)
tracking.capsdropped. Presence is a non-empty string (initials=""behaves as absent). Bare-textguard added.PartAnatomy.overlay: 'corner-accessory', with ring, outset, hairline and gating bundled. Allowlist plus non-interactive rule. BleedNS revisit recorded.accessorySurface: 'canvas' | 'inverse', defaultcanvas, named for what it paints after thesurfaceaxis on topbar and tab-bar. Onlyinverseis a new chrome slot.accessoryVariantand a borderedcanvas-outlinerow are listed as rejected.mdonly, stated in the generated API table's Notes (NOTE_BY_PROP) and on theicon-avatarpage lead, both inpackages/doc/pipeline/component-api-ir.js.accessoryLabel; the sharpened contract sentence in D.Review §4 · anti-scope (all added)
check-circle,warning-circle,cross-circle,spinner); the type staysIconName.inverseis admitted for the accessory, and its availability on View and Pressable sets no precedent.2xsis not an icon size.variant/accentare documented no-ops in initials mode.accessorySurfacewithoutaccessoryis a no-op.Review §5 · change list
accessorySurface, ledger row 7.text(rows 1–2). The tracking guard is moot because tracking is dropped.initials=""; the web overlay's containing block is the painting node;accessory-surfaceobserved.Choices made while amending (for the architect to confirm in the decision record)
allowFontScaling={false}to text leaves inside adecorativedescriptor, not to theinitialsslot kind. That way slot kinds carry no presentation, and the rule matches the stated reason (the avatar is decorative).icon-avataris the only decorative descriptor, and the initials are its only text.chrome: canvas). This keeps it generic, with no axis-value logic in the engines.mdonly lives in the generated doc API notes and the page lead. The request adds no TSDoc channel.remwhile the circle ispx, so a larger browser default font size grows the initials inside the circle. This is noted as accepted; browser zoom scales the whole avatar uniformly.darioaschero commentedon Oct 24, 2026
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: #231 (comment)
Requester / consumer owner: @darioaschero (nuri-com/nuri-expo, send S1 and the address book)
Evidence considered: this issue as amended ("Request amended after independent review", #231 (comment)); the independent review; the evidence boards on
main@d06e2cc(#228,packages/playground/pages/send-recipient.htmlX17, X19, X16); the companion request #230; the policy atmain@45918bb(#229).Rationale: People in Nuri's send and contact lists need initials and a rail mark that no DS part can express today: IconAvatar takes a glyph or an image only, and nothing can lay one part over another. The initials half is small and closes two failure modes by mechanism (outline initials from ListAction's default, per-person colour through
variant/accent). The accessory half is the right shape once its hairline takes no layout, which reproduces the reviewed board exactly. The overlap mechanism is bounded to this one part by a guard, so it does not become general positioning vocabulary.Accepted contract:
initials(string). A newinitialstext part and a scalarinitialsslot kind. Precedencesource>initials>icon; presence is a non-empty string. In initials mode the root paintssoft(bg-strong, text-primary) whatevervariant/accentsay. Type: md 17px regular onmd, xs 13px onsm, each step's own letter-spacing; single line, tail truncation. RN: text inside adecorativecomponent does not follow the OS font scale (IconAvatar is the only decorative component), so slot kinds carry no styling. Web: text is sized in rem, so a larger browser default size grows the initials inside the px circle; accepted.accessory(IconName) and axisaccessorySurface: 'canvas' | 'inverse', defaultcanvas, onmdonly. Partsaccessory(non-interactive view host, rendered only whenaccessoryis set) andaccessoryIcon. Disc sized by content: 15px glyph (new size leaf2xs= 15px) + 3px padding = 21px for both surfaces; outset 3px; 2px knockout ring in bg-canvas; the hairline (border-subtle) is owned by the accessory part, takes no layout, and is drawn when the disc paintschrome: canvas(the disc matches the ring), with no axis-specific engine logic.canvasreuseschrome=canvas;inverseis a new chrome slot (bg-inverse + text-on-inverse), accent-invariant.mdonly, stated in the generated doc page's API notes and intro (the RN codegen emits no per-prop doc comments).PartAnatomy.overlay: 'corner-accessory', descriptor-only, with an allowlist guard: onlyicon-avatar.accessorymay declare it.leadingAvatarforwardsinitials,accessory,accessorySurface, typed from the target's api (generic codegen change).accessory-surface).textslot must becomponentordefault.accessibilityLabel; a handle is not a name.Anti-scope: no person colour palettes; no coloured or accent rail badges; no accessory accent; no status, check, warning, cross or motion glyph as accessory content; no KYC or verified marks, counts or online dots; no overlay on any other part, on View, Pressable or Image, and no general positioning vocabulary (a second use is a new Extension);
inverseon View sets no precedent for further chrome rows;2xsis not an icon size; no letter-spacing token or option; no accessory onsmand no new avatar size; no name parsing or casing in the DS, three or more letters truncate and that is not a feature; no image-to-initials fallback; no surface-adaptive ring; no forwarding through SelectTrigger / SelectField; no generated avatars.Answers to the independent review:
accessory-varianton the web → axis renamedaccessorySurface, generic camel→kebab attribute mapping in scope.unknown→ generic forwarded-prop typing in scope; the exact-type proof stays.overlay→ allowlist guard, non-interactive host, renamed value, BleedNS narrowing recorded here.tracking.capsis dropped.tracking.caps→ dropped (the smaller contract); a later request if initials read cramped on device.Process note: the session that wrote the independent review also applied the amendment, at the architect's direction and after the review was posted (stated in the amendment comment). The review itself was written before and independently of the amendment. The amendment only implements the review's own change list and the architect's calls above; the architect accepts it without a second review of the amended text.
Contraction evidence considered, if applicable:
contentModereshape from one slot to an ordered list; its only user is icon-avatar and no descriptor is authored outside this repo. No consumer migration.Bounded execution scope reference: #231, "Proposed bounded execution scope if admitted" (as amended)
Downstream verification plan reference: #231, "Downstream verification plan (before Ready)" (as amended)
feat: IconAvatar initials + corner accessory (+ ListAction forwarding…