Skip to content

feat(native): Swift ListItem draws an Item with its list's density, marker, dividers, and edge - #8370

Merged
cixzhang merged 1 commit into
mainfrom
native/swift-list-item
Nov 1, 2026
Merged

cixzhang merged 1 commit into
mainfrom
native/swift-list-item

Conversation

@cixzhang

@cixzhang cixzhang commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

User need

Bulleted steps, numbered instructions, settings lists, and divided lists are everywhere on iOS. With only Item, each app has to set the same density, marker, corners, and inset on every row by hand, and a bullet next to a wrapped row slides to the middle instead of staying by the first line. People get lists whose rows don't match each other or the web.

Problem and solution fit

  • ListItem view. Core's list row: an Item that takes its density, marker, dividers, and edge from the List around it, so every row of a list matches without the app repeating them.
  • Markers on the first line. A dot, a ring, or the row's number sits beside the label's first line for every alignment, as core's markers do.
  • Rows between dividers and in flush lists. Square corners between dividers; no inline inset in a flush list.
  • Numbers line up. A number and its period end at the marker column's end, in tabular figures, so periods line up down the list, as core's numbered markers do. A number wider than the column reaches into the row's inline inset; one wider than both widens the column on every row alike, so no number moves its row's words, overlaps them, or is cut.
  • Alone, an Item. Outside a list it draws exactly as Item.
  • Wire rows. A ListItem node sent by a logic core draws its start and end content nodes (AST-082 FR19).

Current specification

  • spec:AST-082 FR3, FR4, FR7, FR19.
  • spec:AST-093 FR1–FR5.
  • Swift library records: component API (A2–A4, A7), slots (S3), theme environment, accessibility (X5).
  • component:swift/Item, which draws the row; its DEC-1 keeps swipe actions web-only for now.
  • New: component:swift/ListItem at docs/specs/components/List/ListItem.swift.spec.md (draft, platform_of: component:ListItem), beside core's ListItem record per AST-093 FR1.

Public delta and caller burden

import Astryx

Astryx.List(listStyle: .decimal) {
  ListItem("Preheat the oven")
  ListItem("Mix the batter", description: "About five minutes")
}
ListItem("Wi-Fi", description: "Home") { Icon(.check) } onClick: { openWiFi() }
ListItem(ListItemProps(label: "Ada", isSelected: true))
  • New: ListItem (Astryx) and generated ListItemProps, ListItemAlign (AstryxNative).
  • The list look (density, markers, numbering, dividers, edge) and the Astryx.List(listStyle:) example above arrive with the Swift List, the next pull request in this stack; until it lands, a ListItem draws as an Item.
  • A single trailing closure is the start content; onClick comes last.
  • Not in Swift: swipeActions and swipeBehavior, web-only for now (component:swift/Item DEC-1), target, rel.
  • Item gains internal list hooks (marker on the first line, inline inset, square corners) that only ListItem sets, and shares its density inset rule with ListItem; an Item alone is unchanged.

Design and alternatives

  • A row learns its number from its list: each row reports its identity, and the list numbers its rows in the order they appear. Each row also reports its number's width, so the list widens one shared column for its widest number. Per-row environment values do not reach views that a container takes apart, so a list cannot hand each row its number directly.
  • The list's treatment applies to this row only, not to rows nested in its slots.

Evidence

Capture set: iPhone 16 simulator, iOS 18.5, Xcode 16.4, built from the List pull request's head (which adds the List these rows sit in, on top of this one), before the rebase onto the new spec layout and the number alignment; the number alignment, shared column, and wire slots are covered by the tests below. Each capture shows bullets on the first line for each alignment, circle and numbered markers, divided rows (selected with square corners, disabled), padded and flush edges on a filled container, compact and spacious density, and a row alone, in light and dark at the default and largest text sizes.

Light Dark Light, largest text Light, largest text, end Dark, largest text Dark, largest text, end
ListItem, Light ListItem, Dark ListItem, Light, largest text ListItem, Light, largest text, end ListItem, Dark, largest text ListItem, Dark, largest text, end

check:knowledge passes with the record at its new path.

Implementation choices under current rules

  • ListItem.swift sits in Sources/Astryx/List/ and its record in docs/specs/components/List/, mirroring core's component root: core keeps ListItem under List/, as it keeps AvatarStatusDot under Avatar/ (AST-093 FR1, as amended to mirror the core component root).
  • Only a centre-aligned row in a list with markers measures its words, to place the marker on the first line (PR1).
  • The number column grows with Dynamic Type so a number never runs into its label; it is the same width on every row, as core's.

Open-work search

Searched current records and open pull requests before creating component:swift/ListItem, by canonical id (component:swift/ListItem, component:ListItem), paths (packages/native/apple/Sources/Astryx/List/, docs/specs/components/List/, packages/native/src/components/ListItem.ts), symbols (ListItem, ListItemProps, ListContext), and terms (native, swift, list). No other record or open pull request owns this platform component; component:swift/Item (#8321) draws its row.

Scope

  • One capability is added.
  • Unrelated fixes, cleanup, and policy changes were removed or split.
  • Consumer docs, focused tests, and migration evidence ship with the feature.
  • Public text and artifacts contain no internal Meta context.

Writing rubric results

  • R1 narration: none; the record states what the view does.
  • R2 research: none.
  • R3 evidence: bases name specs, core's ListItem and ListContext, and tests; no pull request or commit.
  • R4 rejected alternatives: none.
  • R5 change references: none.
  • R6 open questions: none.

Testing

  • vitest run packages/native/src/contract: the ListItem contract (row slots and press; swipes, target, rel, and interactiveRef on the web) passes; the committed Swift binding matches the contracts.
  • swiftc -typecheck of AstryxNative, Astryx, and the tests passes. An immediate-mode render checks the row alone against Item, list density, flush inset, square corners between dividers, the number column, numbering from 3, periods lining up for "1." and "10.", the shared column for a wide number, a wire row's content slots, and the marker's center on the label's first line for center, start, and firstLine.
  • The Swift tests run in CI on macOS and on the iOS Simulator.
  • Screenshot states for the per-component screenshot target, once it lands: each marker, each alignment, dividers, flush, each density, alone, light and dark, and the largest text size.

Every changed file is under packages/native/, plus the new record in docs/specs/components/List/.

@vercel

vercel Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Nov 1, 2026 10:25pm UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 1, 2026, 11:03 PM UTC

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · 0b770d9

Storybook · Sandbox · built from this PR's head

Package size: measured; main has no measurement of the merge base to compare yet.

API changes: core +0 · ~1 · −0 (against the merge base's committed report)

Committed API report behind this build: core 1 row; run pnpm api:report.

1 API change (advisory)
Package Where Signature Usage
~ core ./Text/text.stylex · Values · const displayStyles const displayStyles: Readonly<{ readonly inline: Readonly<{ readonly display: stylex.StyleXClassNameFor<"display", "inline"> }>; readonly block: Readonly<{ readonly display: stylex. … #58a69ee2 → const displayStyles: Readonly<{ readonly inline: Readonly<{ readonly display: stylex.StyleXClassNameFor<"display", "inline">; readonly overflowWrap: stylex.StyleXClassNameFor<"overf … #81cb0621 import {displayStyles} from '@astryxdesign/core/Text/text.stylex'

Not run on this PR: add ci:run-visual, ci:run-a11y or ci:run-rtl to run them.


Generated by PR Enrichment workflow | View full report

@cixzhang
cixzhang force-pushed the native/swift-list-item branch from d7da379 to f4e2a69 Compare October 25, 2026 05:47
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 25, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds the Swift ListItem, together with #8321's Item, and approval is on hold for a spec decision. The new draft records component:swift/Item and component:swift/ListItem state requirements that no current spec does, so an engineering owner needs to approve them on this head by commenting /approve-spec f4e2a69c5c94fb85f97935b301413cd1e7d56448 (the author can do this if they are an engineering owner) or say which requirement should change. Independently of that decision, these need fixing:

  • A decimal number wider than space.s4, such as "10.", widens only its own row's column (ListItem.swift:118), so the labels leave the list's text edge, while ListItem FR2 and core use a fixed 16 pt column.
  • ListItemTests.swift needs the AST-093 FR5 checks for ListItem: an accessibility assertion for the number and the hidden dot or ring, a largest-text render, and a two-theme render.
  • The doc examples use a List and an Avatar that the package does not have yet, and a ListItem nested in a row's slot still takes the list's marker and inset, against FR4.

[Automated review]

.font(astryx.font(size: "text-body-size", 14, weight: "text-body-weight"))
.foregroundStyle(astryx.tokens.color.textPrimary)
.fixedSize()
.frame(minWidth: astryx.tokens.space.s4, alignment: .leading)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A number wider than space.s4 (16 pt), such as "10." at the 14 pt body size, widens only this row's column, so from row 10 on the label, description and start content start further right than in rows 1-9. ListItem FR2 says "a marker in a column space.s4 wide", and core keeps the number box at width: --spacing-4 so its text edge never moves. Framing the number at exactly space.s4, as the dot and ring already are, would match both; a two-digit case in aDecimalListNumbersItsRows would pin it.

/// draws as a plain `Item`.
///
/// ```swift
/// List(listStyle: .decimal) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The package has no List (or Avatar) view at this head, and SwiftUI's List has no listStyle: initializer, so this Quick Help example does not compile yet. Consider dropping it until List lands, or landing this with the List pull request.

HStack(alignment: rowAlignment, spacing: astryx.tokens.space.s2) {
markerSlot
// A list's treatment is for this row only, not for rows nested in its slots.
slot(startContent.environment(\.astryxItemListTreatment, nil))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This clears the treatment for nested Items, but a ListItem placed in a slot rebuilds its treatment from astryxListContext, which still flows in, so it takes the outer list's marker, density, corners and inset. ListItem FR4 says the treatment applies "not to rows nested in its start or end content". Clearing astryxListContext and astryxListNumbers for slot content, or narrowing FR4 to plain Items, would settle it.

import Testing
@testable import Astryx

// Swift ListItem spec (component:swift/ListItem): an Item that takes its

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AST-093 FR5 asks each native component for accessibility assertions, a largest Dynamic Type check, and a two-theme check. These tests have none for what ListItem draws itself: the number read as part of the row with the dot and ring hidden, the markers at .accessibility5, and the marker colour under a second theme.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds the Swift ListItem, together with #8321's Item, and approval is on hold for a spec decision: the draft records component:swift/Item and component:swift/ListItem state requirements no current spec does, so an engineering owner needs to approve them on this head by commenting /approve-spec 8a88a9ab8eb073a9d34463b9732784c89ba4c3b7 (the author may do so if they are an engineering owner) or say which requirement should change. Independently of that decision, these need fixing:

  • At the largest text sizes a numbered marker outgrows its fixed 16 pt column and draws over the start of its label (ListItem.swift:141); growing the column and gap with the text scale keeps core's geometry at the default size, and a decimal case at .accessibility5 would pin it.
  • ListItem.swift and ListItem.spec.md sit in Sources/Astryx/List/, while AST-093 FR1 places each native component's spec in its own folder named as the core component, ListItem/.
  • The largest-text captures show the earlier per-row number column, and Item's Quick Help example uses an Avatar view the package does not have yet.

[Automated review]

.font(astryx.font(size: "text-body-size", 14, weight: "text-body-weight"))
.foregroundStyle(astryx.tokens.color.textPrimary)
.fixedSize()
.frame(width: astryx.tokens.space.s4, alignment: .leading)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the largest text sizes the number's font grows 3.1 times (14 pt to 43.4 pt) while this frame stays 16 pt and the row adds only 8 pt before the label, so from "2." on the number draws over the start of the label; the largest-text capture in the description measures "2." at about 29 pt of ink. Growing the column and gap with the text scale, for example .frame(width: s4 * textScale + s2 * (textScale - 1), alignment: .leading), keeps core's 16 pt at the default size, and a decimal case with "10." in aMarkerStaysOnTheFirstLineAtTheLargestTextSize would pin it.

Comment thread packages/native/apple/Package.swift Outdated
path: "Sources/Astryx",
// Each component's spec lives beside its view (AST-093 FR1).
exclude: ["Icon/Icon.spec.md"],
exclude: ["Icon/Icon.spec.md", "Item/Item.spec.md", "List/ListItem.spec.md"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AST-093 FR1 puts a native component's spec in its own folder named as the core component, Sources/Astryx/ListItem/ListItem.spec.md; here ListItem's view and record share List/ with the coming List. Moving both files, this exclude and the README line to ListItem/, or amending FR1, would settle it.

///
/// ```swift
/// Item("Ada Lovelace", description: "Mathematician") {
/// Avatar("Ada Lovelace")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no Avatar view in Sources/Astryx yet, so this Quick Help example does not compile; an Icon, as in ListItem's example, would.

@cixzhang
cixzhang force-pushed the native/swift-list-item branch 2 times, most recently from 375794c to 1e6ac1a Compare October 25, 2026 11:07

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds the Swift ListItem, together with #8321's Item, and approval is on hold for a spec decision: the new draft records component:swift/Item and component:swift/ListItem state requirements no current spec does, so an engineering owner needs to approve them on this head by commenting /approve-spec 1e6ac1abc4df99049b0f3a93bb7ebb5c1bd9f6b2 (the author may do so if they are an engineering owner) or say which requirement should change. The decision covers Item FR5's relative-href stop and static-row height, the closure-taking props initializers (Item FR6, ListItem FR5), Item OQ1's web-only swipe props, ListItem FR2's number column grown with Dynamic Type, and ListItem FR4's nested-row scope, which core's nested ListItem does not share. Independently of that decision, ListItem.swift and ListItem.spec.md sit in Sources/Astryx/List/, while AST-093 FR1 puts a native component's view and spec in a folder named as the core component, ListItem/; moving both files with the Package.swift exclude and the README line, or amending FR1, would settle it. The largest-text captures now match the grown number column, and Item's doc example uses only shipped views.

[Automated review]

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds the Swift ListItem, together with #8321's Item, and approval is on hold for a spec decision: the new draft records component:swift/Item and component:swift/ListItem state requirements no current spec does, so an engineering owner needs to approve them on this head by commenting /approve-spec 1c513b21ff228f638f19d2b2237af5d43fbe4cf4 (the author may do so if they are an engineering owner) or say which requirement should change. The decision covers Item FR5's relative-href stop and static-row height, the closure-taking props initializers (Item FR6, ListItem FR5), Item OQ1's web-only swipe props, ListItem FR2's marker column grown with Dynamic Type, and ListItem FR4's nested-row scope, which core's nested ListItem does not share. Independently of that decision, two fixes are needed:

  • AstryxNodeView draws Item and ListItem nodes from their props alone (AstryxNodeView.swift:22-23), so a logic core's marker, startContent and endContent nodes are silently dropped, against AST-082 FR19; passing them through AstryxNodeList, as the Card case does, and sending a slot node in anItemNodeDrawsAsAnItem would settle it.
  • ListItem.swift and ListItem.spec.md sit in Sources/Astryx/List/, while AST-093 FR1 puts a native component's view and spec in a folder named as the core component, ListItem/; moving both files with the Package.swift exclude and the README line, or amending FR1, would settle it.

[Automated review]

Comment on lines +22 to +23
case .item(let item): Item(item.viewProps(send: send))
case .listItem(let row): ListItem(row.viewProps(send: send))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These build each row from viewProps alone, so the marker, startContent and endContent nodes that ItemNode.Props and ListItemNode.Props decode never draw: a logic core's Icon in a row's start slot silently disappears, where main draws the unsupported placeholder that names Item. AST-082 FR19 says a content slot carries child nodes and a host draws every node it can read. Passing each slot through AstryxNodeList, as the .card case does with children, and sending a slot node in anItemNodeDrawsAsAnItem would settle it.

Comment thread packages/native/apple/Package.swift Outdated
path: "Sources/Astryx",
// Each component's spec lives beside its view (AST-093 FR1).
exclude: ["Icon/Icon.spec.md"],
exclude: ["Icon/Icon.spec.md", "Item/Item.spec.md", "List/ListItem.spec.md"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AST-093 FR1 puts a native component's spec in its own folder named as the core component, Sources/Astryx/ListItem/ListItem.spec.md; here ListItem's view and record share List/ with the coming List. Moving both files, this exclude and the README line to ListItem/, or amending FR1, would settle it.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds the Swift ListItem together with #8321's Item, and approval is on hold for a spec decision: the new draft records component:swift/Item and component:swift/ListItem state requirements no current spec does, so an engineering owner needs to approve them on this head by commenting /approve-spec ec4f301e7ba10a7640e483b4a7064cab7ec456fd, which the author may do if they are an engineering owner, or say which requirement should change. The decision covers Item FR5's relative-href stop and static-row height, the closure-taking props initializers (Item FR6, ListItem FR5), Item OQ1's web-only swipe props, ListItem FR2's marker column grown with Dynamic Type, and ListItem FR4's nested-row scope, which core's nested ListItem does not share. Independently of that decision, two fixes are needed:

  • AstryxNodeView builds Item and ListItem from their props alone (AstryxNodeView.swift:23-24), so a logic core's marker, startContent and endContent nodes are silently dropped, against AST-082 FR19; passing each slot through AstryxNodeList, as the Card case does, and sending a slot node in anItemNodeDrawsAsAnItem would settle it.
  • ListItem.swift and ListItem.spec.md sit in Sources/Astryx/List/, while AST-093 FR1 puts a native component's view and spec in a folder named as the core component, ListItem/; moving both files with the Package.swift exclude and the README line, or amending FR1, would settle it.

[Automated review]

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds the Swift ListItem together with #8321's Item, and approval is on hold for a spec decision: the new draft records component:swift/Item and component:swift/ListItem state requirements no current spec does, so an engineering owner needs to approve them on this head by commenting /approve-spec 0765de58bb902461c3b9b18b5d8be25b7c98a044, which the author may do if they are an engineering owner, or say which requirement should change. The decision covers Item FR5's relative-href stop and static-row height, the closure-taking props initializers (Item FR6, ListItem FR5), Item OQ1's web-only swipe props, ListItem FR2's marker column grown with Dynamic Type, ListItem FR4's nested-row scope, which core's nested ListItem does not share, and ListItem PR1, which says only rows in a list with markers measure their words while Item measures every row. Independently of that decision, two fixes are needed:

  • AstryxNodeView builds Item and ListItem from their props alone (AstryxNodeView.swift:25-26), so a logic core's marker, startContent and endContent nodes are silently dropped, against AST-082 FR19; passing each slot through AstryxNodeList, as the Avatar case does, and sending a slot node in anItemNodeDrawsAsAnItem would settle it.
  • ListItem.swift and ListItem.spec.md sit in Sources/Astryx/List/, while AST-093 FR1 puts a native component's view and spec in a folder named as the core component, ListItem/; moving both files with the Package.swift exclude and the README line, or amending FR1, would settle it.

[Automated review]

Comment thread packages/native/apple/Package.swift Outdated
path: "Sources/Astryx",
// Each component's spec lives beside its view (AST-093 FR1).
exclude: ["Avatar/Avatar.spec.md", "Avatar/AvatarStatusDot.spec.md", "Icon/Icon.spec.md"],
exclude: ["Avatar/Avatar.spec.md", "Avatar/AvatarStatusDot.spec.md", "Icon/Icon.spec.md", "Item/Item.spec.md", "List/ListItem.spec.md"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AST-093 FR1 places a native component's view and record in a folder named as the core component, Sources/Astryx/ListItem/ListItem.spec.md; this exclude, the README line and both ListItem files sit in List/. Moving them to ListItem/, or an engineering owner amending FR1, would settle it.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds the Swift ListItem together with #8321's Item, and approval is on hold for a spec decision: the new draft records component:swift/Item and component:swift/ListItem state requirements no current spec does, so an engineering owner needs to approve them on this head by commenting /approve-spec a29c4059bbf5723816404a6ca37a6b927b94fcdb, which the author may do if they are an engineering owner, or say which requirement should change. The decision covers Item FR5's relative-href stop and static-row height, the closure-taking props initializers (Item FR6, ListItem FR5), Item OQ1's web-only swipe props, ListItem FR2's marker column grown with Dynamic Type, ListItem FR4's nested-row scope, which core's nested ListItem does not share, and ListItem PR1, which says only rows in a list with markers measure their words while Item measures every row. Independently of that decision, two fixes are needed:

  • AstryxNodeView builds Item and ListItem from their props alone (AstryxNodeView.swift:26-27), so a logic core's marker, startContent and endContent nodes are silently dropped, against AST-082 FR19; passing each slot through AstryxNodeList, as the Avatar case does, and sending a slot node in anItemNodeDrawsAsAnItem would settle it.
  • ListItem.swift and ListItem.spec.md sit in Sources/Astryx/List/, while AST-093 FR1 puts a native component's view and spec in a folder named as the core component, ListItem/; moving both files with the Package.swift exclude and the README line, or amending FR1, would settle it.

[Automated review]

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec a29c405

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval is on hold for a spec decision: the draft records component:swift/Item and component:swift/ListItem state Swift-only requirements no current spec does, and the earlier /approve-spec names the previous head, so an engineering owner needs to approve them on this head by commenting /approve-spec 31c6227cb93762d4fc92fd4ab6c13896e5d93f66 (the author may do so as an engineering owner) or say which requirement should change. Independently of that decision, these need fixing:

  • AstryxNodeView builds Item and ListItem from their props alone, so a logic core's marker, startContent and endContent nodes are silently dropped (AST-082 FR19); passing each slot through AstryxNodeList, as the Avatar case does, with a slot node in anItemNodeDrawsAsAnItem, would settle it.
  • ListItem.swift belongs in Sources/Astryx/ListItem/ under AST-093 FR1, or FR1 needs amending.
  • Item re-implements astryxBase and Avatar's isUnusableHref and linkURL; calling shared helpers keeps those paths from drifting.
  • ListItem PR1 says only rows in a list with markers measure their words, but Item measures every row; narrow the measurement or the wording.

[Automated review]

│ │ ├── Icon/ # Icon
│ │ ├── Avatar/ # Avatar
│ │ ├── Item/ # Item
│ │ ├── List/ # ListItem

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AST-093 FR1 puts a component's views in Sources/Astryx/<Name>/, named as the core component, so ListItem.swift belongs in Sources/Astryx/ListItem/; core's List/ folder name applies only to the record, which already sits at docs/specs/components/List/ListItem.swift.spec.md. Moving the file and this line, or amending FR1, would settle it.

Comment on lines +94 to +96
if props.base.isHidden != true {
control
.modifier(ItemIdentifier(id: props.base.id))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

astryxBase(_:) in Base/BaseProps.swift already removes the view for isHidden and maps id to the accessibility identifier, and Button, Card, Heading and Text call it. control.astryxBase(props.base) here would let the isHidden check and ItemIdentifier go, so a change to base props reaches Item too.

Comment on lines +112 to +116
nonisolated static func isUnusableHref(_ href: String?) -> Bool {
guard let href, !href.isEmpty else { return false }
guard let url = URL(string: href) else { return true }
return url.scheme == nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This repeats Avatar.isUnusableHref (Avatar.swift:111), and linkURL below repeats Avatar's; one helper in Base/ that both call keeps the rule for which URLs an app can open in one place.

// own disabled state).
words
.frame(maxWidth: .infinity, alignment: .leading)
.onGeometryChange(for: CGFloat.self) { $0.size.height } action: { wordsHeight = $0 }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every row measures its words and writes wordsHeight here, though only the center marker in a marked list reads it; ListItem PR1 says only rows in a list with markers measure. Measuring only when listTreatment?.markerOnFirstLine == true, or rewording PR1, would line them up.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval waits on an engineering owner's decision on the two draft records, component:swift/Item and component:swift/ListItem: their Swift-only requirements need approval at this exact head, recorded by commenting /approve-spec 6db4bc18131ae56a8e1b923766a40c7ae94f19bb (the author may do so as an engineering owner), or a note on which requirement to change. The Item wire slots and the shared destination rule are fixed; these still need changes after that decision:

  • AstryxNodeView builds ListItem from its props alone, so a logic core's startContent and endContent nodes never draw; pass them through AstryxNodeList as the Item case does.
  • ListItem.swift belongs in Sources/Astryx/ListItem/, the folder named after the core component, or the native guidelines need amending.
  • Item repeats the shared base-props modifier and Avatar's href check, and that check reads the raw href while the link reads the cleaned one, so " https://example.com" stops a debug build although the row opens it.
  • ListItem's PR1 says only marked rows measure their words, but every Item row does.

[Automated review]

} marker: {
AstryxNodeList(nodes: item.props.marker ?? [], send: send)
}
case .listItem(let row): ListItem(row.viewProps(send: send))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This builds the row from viewProps alone, so the startContent and endContent nodes ListItemNode.Props decodes never draw: a logic core's Icon in a row's start slot disappears with nothing to say so (AST-082 FR19). Passing both through AstryxNodeList, as the .item case above does, with a test like anItemNodeDrawsItsContentSlots, would fix it.

/// build with its value; a release build draws the row without a link.
private func checkInDevelopment() {
#if DEBUG
if Item.isUnusableHref(props.href) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This checks the raw href, while linkURL checks the trimmed destination from astryxSafeDestination, so " https://example.com" fires this assertion ("it draws without a link") although the row opens it. Checking the cleaned value, in one helper Avatar can share, would line the two up.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval waits on an engineering owner's decision on the two draft records, component:swift/Item and component:swift/ListItem: their Swift-only requirements need approval at this exact head, recorded by commenting /approve-spec b94da5d66297be0de0c786248b384566a1967c97 (the author may do so as an engineering owner), or a note on which requirement to change. Item's href check now matches its link; these still need changes after that decision:

  • AstryxNodeView builds ListItem from its props alone, so a logic core's startContent and endContent nodes never draw; pass them through AstryxNodeList as the Item case does, with a test.
  • Item repeats the shared base-props modifier (its isHidden check and a private identifier modifier); calling astryxBase(props.base) keeps base props on one path.
  • ListItem.swift belongs in Sources/Astryx/ListItem/, the folder named after the core component, or the native guidelines need amending.
  • ListItem's PR1 says only marked rows measure their words, but every Item row does.

[Automated review]

@cixzhang
cixzhang force-pushed the native/swift-list-item branch from b94da5d to 9a0c5a7 Compare November 1, 2026 14:20
@cixzhang
cixzhang marked this pull request as ready for review November 1, 2026 14:20

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval waits on an engineering owner's decision on the draft component:swift/ListItem record: its Swift-only requirements (the marker geometry, the nested-row scope, the closure initializers, the read-aloud markers, the performance notes and the native swipe exclusion) need approval at this exact head, recorded by commenting /approve-spec 9a0c5a74f2070680891d233c029e63b702162c4a (the author may do so as an engineering owner), or a note on which requirement to change. The ListItem wire slots now draw and the Swift tests pass; these still need changes after that decision:

  • Move ListItem.swift to Sources/Astryx/ListItem/ and update the README line, since AST-093 FR1 puts a component's views in a folder named for it, or amend FR1.
  • Measure the words at Item.swift:151 only for a centre-aligned row in a marked list, or reword the record's PR1, which says only those rows measure.
  • Say in the description that the list look and the Astryx.List(listStyle:) example arrive with the stacked Swift List.

[Automated review]

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval waits on an engineering owner's decision on the draft component:swift/ListItem record: its Swift-only requirements (the marker column and number geometry, the nested-row scope, the closure initializers, the number read aloud, PR1–PR2, and keeping swipe actions web-only on native) are stated by no current spec. An engineering owner records approval by commenting /approve-spec bfe451cd99163af21b3180a0e6b5fd02265114e6 on this pull request (the author may do so as an engineering owner), or says which requirement to change. The earlier folder, PR1 and description points are fixed, and nothing else needs to change.

[Automated review]

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval waits on an engineering owner's decision on the draft component:swift/ListItem record: its Swift-only requirements (the marker column and number geometry, the nested-row scope, the closure initializers, the number read aloud, PR1–PR2, and keeping swipe actions web-only on native) are stated by no current spec. An engineering owner records approval by commenting /approve-spec 0b770d970d82da29a84f66e89b11e40019b563d3 on this pull request (the author may do so as an engineering owner), or says which requirement to change. The view's move back to Sources/Astryx/List/ matches the amended AST-093 FR1, the Swift tests pass on macOS and the iOS Simulator, and nothing else needs to change.

[Automated review]

@cixzhang

cixzhang commented Nov 1, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 0b770d9

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks ready to merge: the Swift ListItem draws the Swift Item with its list's density, marker, dividers and edge, and the draft component:swift/ListItem record now has an engineering owner's /approve-spec on this exact commit. The tests cover every requirement in the record and pass on macOS and the iOS Simulator, and the committed Swift binding matches the generated contracts. One follow-up for the stacked Swift List rather than this change: no record yet says how VoiceOver learns that a row belongs to a list, which core's rows get from their <li>.

[Automated review]

@cixzhang
cixzhang merged commit b7acb43 into main Nov 1, 2026
59 checks passed
@github-actions
github-actions Bot deleted the native/swift-list-item branch November 2, 2026 06:04

This branch was successfully deployed

1 active deployment
Preview — 0b770d97 Deployed Nov 1, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant