Repository navigation
feat(native): Swift Item draws core's row with typed slots, as a link or a button - #8321
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report🔎 Preview ·
|
| Package | Where | Signature | Usage | |
|---|---|---|---|---|
| + | core |
. · Types · interface FieldStatusProps |
message: string |
import type {FieldStatusProps} from '@astryxdesign/core'; const x: FieldStatusProps = {message: …} |
| + | core |
. · Types · interface FieldStatusProps |
ref?: React.Ref<HTMLDivElement> |
import type {FieldStatusProps} from '@astryxdesign/core'; const x: FieldStatusProps = {ref: …} |
| + | core |
. · Types · interface FieldStatusProps |
type: InputStatusType |
import type {FieldStatusProps} from '@astryxdesign/core'; const x: FieldStatusProps = {type: …} |
| + | core |
. · Types · interface FieldStatusProps |
variant?: FieldStatusVariant = 'attached' |
import type {FieldStatusProps} from '@astryxdesign/core'; const x: FieldStatusProps = {variant: …} |
| + | core |
. · Types · interface FieldStatusProps |
xstyle?: StyleXStyles |
import type {FieldStatusProps} from '@astryxdesign/core'; const x: FieldStatusProps = {xstyle: …} |
| + | core |
. · Types · interface FieldStatusProps |
inherits AriaAttributes, DOMAttributes, HTMLAttributes |
import type {FieldStatusProps} from '@astryxdesign/core' |
| + | core |
. · Types · interface SearchContext |
interface SearchContext<T extends SearchableItem = SearchableItem> |
import type {SearchContext} from '@astryxdesign/core' |
| + | core |
./Typeahead · Also exported from an earlier entry point · SearchContext |
SearchContext (.) |
import {SearchContext} from '@astryxdesign/core/Typeahead' |
| ~ | core |
. · Components · component ComplexSelector |
prop statusVariant?: FieldStatusVariant = 'attached' for input selectors; 'detached' for secondary and ghost selectors → prop statusVariant?: FieldStatusPlacement = 'attached' for input selectors; 'detached' for secondary and ghost selectors |
<ComplexSelector statusVariant="attached" /> |
| ~ | core |
. · Components · component DateInput |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<DateInput statusVariant="attached" /> |
| ~ | core |
. · Components · component DateRangeInput |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<DateRangeInput statusVariant="attached" /> |
| ~ | core |
. · Components · component Field |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<Field statusVariant="attached" /> |
| ~ | core |
. · Components · component FieldStatus |
component FieldStatus(props: FieldStatusProps) → component FieldStatus @deprecated |
<FieldStatus /> |
| ~ | core |
. · Components · component FieldStatus |
prop variant?: FieldStatusVariant = 'attached' → prop variant: FieldStatusVariant = 'attached' |
<FieldStatus variant="attached" /> |
| ~ | core |
. · Components · component FileInput |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<FileInput statusVariant="attached" /> |
| ~ | core |
. · Components · component MultiSelector |
prop statusVariant?: FieldStatusVariant = 'attached' for input selectors; 'detached' for secondary and ghost selectors → prop statusVariant?: FieldStatusPlacement = 'attached' for input selectors; 'detached' for secondary and ghost selectors |
<MultiSelector statusVariant="attached" /> |
| ~ | core |
. · Components · component NumberInput |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<NumberInput statusVariant="attached" /> |
| ~ | core |
. · Components · component PowerSearch |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<PowerSearch statusVariant="attached" /> |
| ~ | core |
. · Components · component Selector |
prop statusVariant?: FieldStatusVariant = 'attached' for input selectors; 'detached' for secondary/ghost selectors → prop statusVariant?: FieldStatusPlacement = 'attached' for input selectors; 'detached' for secondary/ghost selectors |
<Selector statusVariant="attached" /> |
| ~ | core |
. · Components · component TextArea |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<TextArea statusVariant="attached" /> |
| ~ | core |
. · Components · component TextInput |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<TextInput statusVariant="attached" /> |
| ~ | core |
. · Components · component TimeInput |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<TimeInput statusVariant="attached" /> |
| ~ | core |
. · Components · component Tokenizer |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<Tokenizer statusVariant="attached" /> |
| ~ | core |
. · Components · component Typeahead |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusPlacement = 'attached' |
<Typeahead statusVariant="attached" /> |
| ~ | core |
. · Types · interface FieldStatusProps |
interface FieldStatusProps extends BaseProps<HTMLDivElement> (props of FieldStatus) → interface FieldStatusProps extends BaseProps<HTMLDivElement> |
import type {FieldStatusProps} from '@astryxdesign/core' |
| ~ | core |
. · Types · interface FieldStatusVariantMap |
tooltip: true → tooltip: true @deprecated |
import type {FieldStatusVariantMap} from '@astryxdesign/core'; const x: FieldStatusVariantMap = {tooltip: …} |
| ~ | core |
. · Types · interface SearchSource |
search: (query: string) => Promise<T[]> | T[] → search: (query: string, context?: SearchContext<T>) => Promise<T[]> | T[] |
import type {SearchSource} from '@astryxdesign/core'; const x: SearchSource = {search: …} |
| ~ | core |
. · Types · interface UseInputStatusIconOptions |
statusVariant?: FieldStatusVariant → statusVariant?: FieldStatusPlacement |
import type {UseInputStatusIconOptions} from '@astryxdesign/core'; const x: UseInputStatusIconOptions = {statusVariant: …} |
| ~ | richtext |
. · Components · component RichTextEditor |
prop statusVariant?: FieldStatusVariant = 'attached' → prop statusVariant?: FieldStatusVariant | 'tooltip' = 'attached' |
<RichTextEditor statusVariant="tooltip" /> |
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
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on the new draft record component:swift/Item: no current spec yet states its requirements (FR1–FR6, AV1–AV2, ORD1–ORD2, PR1, AR1–AR4) or OQ1's choice to keep swipeActions and swipeBehavior out of the Swift contract. An engineering owner records that decision by commenting /approve-spec <full head SHA> on the head that carries the fixes below, and the author may do so as an engineering owner. Please also fix these on that head:
align: .firstLineleaves a slot taller than one label line top-aligned (Item.swift:181–182), so it draws likestartinstead of centering on the first line as core Item's FR2–FR4 require; your own capture shows it with the 24 pt avatar.- A static disabled row exposes no disabled state to VoiceOver (
Item.swift:116–119), while core marks every disabled rowaria-disabledand the record's AR2 says it reads as dimmed. ItemTests.swiftneeds the trait and state assertions and the largest Dynamic Type check that AST-093 FR5 requires, plus a first-line case with a slot taller than one line.
[Automated review]
| var resolvedAlign: ItemAlign { props.align ?? .center } | ||
|
|
||
| private var rowAlignment: VerticalAlignment { | ||
| resolvedAlign == .center ? .center : .top |
There was a problem hiding this comment.
With firstLine this aligns the row .top, so a slot taller than the 20 pt first line keeps its top on the words' top and its center lands (height − 20) / 2 pt low; core Item's first-line (FR2–FR4) centers every slot on the first line and moves the text down. A custom VerticalAlignment guide that the words set at bodyLineHeight / 2 and each slot sets at its own center would match core.
| } else { | ||
| row(minHeight: nil) | ||
| .accessibilityElement(children: .combine) | ||
| .modifier(ItemAccessibility(isSelected: isSelected)) |
There was a problem hiding this comment.
The static branch never applies .disabled(isDisabled), so a static disabled row is dimmed but exposes no disabled state to VoiceOver. Core sets aria-disabled on every disabled row, and the record's AR2 says a disabled row reads as dimmed.
| if let url = linkURL { | ||
| SwiftUI.Link(destination: url) { row(minHeight: Item.minimumTarget) } | ||
| .buttonStyle(ItemPressStyle(radius: astryx.tokens.radius.element, pressed: astryx.tokens.color.overlayHover)) | ||
| // `onClick` runs as the link opens, as core's does. |
There was a problem hiding this comment.
Core does not do this: with both href and onClick, core's anchor navigates and its container click handler skips onClick for clicks inside the anchor (packages/core/src/Item/Item.tsx:735). Running onClick on open is new behavior under FR5 for the owner to approve, so this comment should not claim parity.
| } | ||
|
|
||
| @MainActor | ||
| @Test func alignPlacesTheSlotsOnTheWords() { |
There was a problem hiding this comment.
This covers start and center only. Please add a firstLine case with a slot taller than one line (for example 40 pt), with and without a description, plus the trait and state assertions and the .accessibility5 render that AST-093 FR5 requires.
6b7509d to
5820427
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on the new draft record component:swift/Item: no current spec yet states its requirements (FR1–FR6, AV1–AV2, ORD1–ORD2, PR1, AR1–AR4) or OQ1's choice to keep swipeActions and swipeBehavior out of the native contracts. The earlier first-line and disabled-state issues are fixed on this head, and no other change is required. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner; the owner should know that FR5 runs onClick on every link open and FR4 dims the start content, which core's web Item does not do.
- Optional: the comment at
Item.swift:104still says core runsonClickas the link opens, and the package README's "Not here yet" list still says no view uses the generated contract types.
[Automated review]
fdd7c64 to
2ffff02
Compare
4a735d1 to
93a9ebf
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on the new draft record component:swift/Item: no current spec yet states its requirements (FR1–FR6, AV1–AV2, ORD1–ORD2, PR1, AR1–AR4) or OQ1's choice to keep swipeActions and swipeBehavior out of every native contract. An engineering owner records that decision by commenting /approve-spec <full head SHA> on the head that carries the fix below, and the author may do so as an engineering owner. The owner should know that FR5 never runs onClick beside a URL href and stops development builds on a web path such as /calendar, and that FR4 paints a pressed row with color.overlayHover where core uses color.overlayPressed. Please also fix this on that head:
layout: .inlinewithout a description fixes the label at full width (Item.swift:188–190), so long labels never truncate where core's row does (packages/core/src/Item/Item.tsx:689); take the inline branch only when a description is present and add a render test for it.
[Automated review]
| .modifier( | ||
| ItemLeading(extra: extraLeading(astryx.tokens.font.supportingLeading, size: astryx.tokens.font.supportingSize))) | ||
| } | ||
| if (props.layout ?? .stacked) == .inline { |
There was a problem hiding this comment.
This branch also runs when there is no description, so the label is fixed at its full width and a long label never truncates; it pushes the end content past the row's edge. Core applies inline only with a description (packages/core/src/Item/Item.tsx:689), and the record gives layout "with a description". Consider if (props.layout ?? .stacked) == .inline, props.description != nil { plus a render test with a long label and no description.
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on the new draft record component:swift/Item: no current spec yet states its requirements (FR1–FR6, AV1–AV2, ORD1–ORD2, PR1, AR1–AR4) or OQ1's choice to keep swipeActions and swipeBehavior out of every native contract, and FR5 departs from core's web Item by never running onClick beside a URL href and by stopping development builds on a web path such as /calendar. An engineering owner records that decision by commenting /approve-spec <full head SHA> on the head that carries the fix below, and the author may do so as an engineering owner. The earlier inline-layout and pressed-colour issues are fixed on this head; please also address:
- In the Neutral theme a selected link or button row shows no press, because
ItemPressStylepaintsoverlayPressedbehind the row's opaqueaccentMutedfill (Item.swift:293–298), while core's touch press paints above the selected colour. - Optional:
anInlineLabelWithoutADescriptionStillTruncatesalso passes on the pre-fix code; asserting that an end-content swatch stays inside the 160 pt frame would make it guard the fix.
[Automated review]
| configuration.label | ||
| .background( | ||
| RoundedRectangle(cornerRadius: radius, style: .continuous) | ||
| .fill(configuration.isPressed ? pressed : .clear) | ||
| ) |
There was a problem hiding this comment.
This press fill sits behind configuration.label, and the label is the row, whose own fill is accentMuted when selected. In the Neutral theme that fill is opaque (0xF1F1F1FF light, 0x262626FF dark), so a selected link or button row shows no press; core paints its touch press above the selected colour. Drawing the state fill and the press fill together here, behind the label, would match core.
| #expect(inline.width == 160) | ||
| #expect(inline.height == stacked.height) |
There was a problem hiding this comment.
With .frame(width: 160) the width is 160 whatever the label does, and a one-line label is as tall when it overflows as when it truncates, so this also passes on the pre-fix code. Asserting that an end-content swatch is drawn inside the 160 pt frame would fail there.
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on the new draft record component:swift/Item: no current spec yet states its requirements (FR1–FR6, AV1–AV2, ORD1–ORD2, PR1, AR1–AR4) or OQ1's choice to keep swipeActions and swipeBehavior out of every native contract. The press now shows above a selected row's fill and the inline-layout test guards its fix, so no code change is required on this head. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner; the decision should weigh that FR5 never runs onClick beside an absolute-URL href and stops development builds on a web path such as /calendar, which core's web Item does not do.
[Automated review]
| | FR2 | The label MUST draw in the body type and `color.textPrimary`, the description in the supporting type and `color.textSecondary`, each limited to `labelLines` or `descriptionLines` lines (one by default) and truncating after them. Each line takes the theme's leading, grown with Dynamic Type. With `inline` and a description, the description sits beside the label with a gap of `space.s1` and the label does not shrink; without a description the label keeps its line limit. | core `Item` styles | verify | | ||
| | FR3 | `center` MUST center each slot on the words; `start` MUST put each slot's top on the words' top; `firstLine` MUST center each slot on the label's first line. | core `Item` `align` | verify | | ||
| | FR4 | `isSelected` MUST paint `color.accentMuted` behind the row and `isHighlighted` `color.overlayHover`, with selected winning, in the element radius. `isDisabled` MUST draw the words and the end content at half opacity, as core does; the marker and start content keep their own look. A pressed button or link row paints `color.overlayPressed` above its selected or highlighted fill, as core does. | core `Item` styles | verify | | ||
| | FR5 | With an `href` that is an absolute URL (it has a scheme), the row MUST be a SwiftUI `Link`, and `onClick` does not run, as core's link row skips it. An `href` that is not an absolute URL, such as a web path, MUST stop a development build with a message naming it; a release build draws the row without a link. With only `onClick`, a SwiftUI `Button`. A tappable row is at least 44 points tall; a static row keeps core's height. A disabled link or button stays reachable, reads as dimmed, and does nothing. | Swift component API A4; Swift accessibility X2, X7 | verify | |
There was a problem hiding this comment.
Core skips onClick only for a tap inside the label's link (packages/core/src/Item/Item.tsx:730–739); a tap on the start content, end content or padding still runs it. So "as core's link row skips it" holds only for the words, and approving FR5 decides that the Swift row drops onClick for the whole row.
| | FR3 | `center` MUST center each slot on the words; `start` MUST put each slot's top on the words' top; `firstLine` MUST center each slot on the label's first line. | core `Item` `align` | verify | | ||
| | FR4 | `isSelected` MUST paint `color.accentMuted` behind the row and `isHighlighted` `color.overlayHover`, with selected winning, in the element radius. `isDisabled` MUST draw the words and the end content at half opacity, as core does; the marker and start content keep their own look. A pressed button or link row paints `color.overlayPressed` above its selected or highlighted fill, as core does. | core `Item` styles | verify | | ||
| | FR5 | With an `href` that is an absolute URL (it has a scheme), the row MUST be a SwiftUI `Link`, and `onClick` does not run, as core's link row skips it. An `href` that is not an absolute URL, such as a web path, MUST stop a development build with a message naming it; a release build draws the row without a link. With only `onClick`, a SwiftUI `Button`. A tappable row is at least 44 points tall; a static row keeps core's height. A disabled link or button stays reachable, reads as dimmed, and does nothing. | Swift component API A4; Swift accessibility X2, X7 | verify | | ||
| | FR6 | Both initializers MUST build the same `ItemProps`. Slots are generic `@ViewBuilder` closures with empty defaults, never `AnyView`; a single trailing closure is the start content and `onClick` comes last. Base props come through `ItemProps`: with `isHidden` the view draws nothing; `id` becomes the accessibility identifier. | Swift component API A2–A4, A7; Swift slots S3 | verify | |
There was a problem hiding this comment.
FR6 cites component API A3 and slots S3 as its basis, but both draft notes give the props initializer the slot's generated node type, while Item.swift:79–89 takes @ViewBuilder closures. FR6 or those notes needs to change so the two agree.
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on the new draft record component:swift/Item: no current spec yet states its requirements (FR1–FR6, AV1–AV2, ORD1–ORD2, PR1, AR1–AR4) or OQ1's choice to keep swipeActions and swipeBehavior out of every native contract. No code change is required on this head: the new line-count clamp is tested and the earlier fixes hold. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner; the decision should weigh that FR5 never runs onClick beside an absolute-URL href and stops development builds on a web path such as /calendar, which core's web Item does not do.
[Automated review]
1d579a8 to
0208f56
Compare
2eb45d5 to
27271c8
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on the new draft record component:swift/Item: no current spec yet states its requirements (FR1–FR6, AV1–AV2, ORD1–ORD2, PR1, AR1–AR4) or OQ1's choice to keep swipeActions and swipeBehavior out of every native contract, and FR5 departs from core's web Item by never running onClick beside a URL href, refusing relative paths such as /calendar, and turning javascript: into a link. An engineering owner records that decision by commenting /approve-spec <full head SHA> on the head that carries the fix below, and the author may do so as an engineering owner. The latest push leaves this fix open:
AstryxNodeViewbuilds an Item wire node from its props alone (AstryxNodeView.swift:25), so itsmarker,startContentandendContentchildren are never drawn, againstspec:AST-082FR19; pass them throughAstryxNodeListas the Avatar case does, and add a wire test whose node carries a content slot.
[Automated review]
| case .avatar(let avatar): | ||
| Avatar(avatar.viewProps(send: send)) { AstryxNodeList(nodes: avatar.props.status ?? [], send: send) } | ||
| case .avatarStatusDot(let dot): AvatarStatusDot(dot.viewProps(send: send)) | ||
| case .item(let item): Item(item.viewProps(send: send)) |
There was a problem hiding this comment.
This passes only the node's props, so an Item node's marker, startContent and endContent children (Generated/Item.swift:52–55) are never drawn, against spec:AST-082 FR19 ("A content slot (FR4) carries child nodes"; "A host draws every node it can read"). Passing each slot through AstryxNodeList, as the Avatar case above does for its status slot, fixes it:
case .item(let item):
Item(item.viewProps(send: send)) {
AstryxNodeList(nodes: item.props.startContent ?? [], send: send)
} endContent: {
AstryxNodeList(nodes: item.props.endContent ?? [], send: send)
} marker: {
AstryxNodeList(nodes: item.props.marker ?? [], send: send)
}
Please add a wire test whose node carries a content slot; anItemNodeDrawsAsAnItem sends a label only.
| | FR2 | The label MUST draw in the body type and `color.textPrimary`, the description in the supporting type and `color.textSecondary`, each limited to `labelLines` or `descriptionLines` lines (one by default) and truncating after them. Each line takes the theme's leading, grown with Dynamic Type. With `inline` and a description, the description sits beside the label with a gap of `space.s1` and the label does not shrink; without a description the label keeps its line limit. | core `Item` styles | verify | | ||
| | FR3 | `center` MUST center each slot on the words; `start` MUST put each slot's top on the words' top; `firstLine` MUST center each slot on the label's first line. | core `Item` `align` | verify | | ||
| | FR4 | `isSelected` MUST paint `color.accentMuted` behind the row and `isHighlighted` `color.overlayHover`, with selected winning, in the element radius. `isDisabled` MUST draw the words and the end content at half opacity, as core does; the marker and start content keep their own look. A pressed button or link row paints `color.overlayPressed` above its selected or highlighted fill, as core does. | core `Item` styles | verify | | ||
| | FR5 | With an `href` that is an absolute URL (it has a scheme), the row MUST be a SwiftUI `Link`, and `onClick` does not run, as core's link row skips it. An `href` that is not an absolute URL, such as a web path, MUST stop a development build with a message naming it; a release build draws the row without a link. With only `onClick`, a SwiftUI `Button`. A tappable row is at least 44 points tall; a static row keeps core's height. A disabled link or button stays reachable, reads as dimmed, and does nothing. | Swift component API A4; Swift accessibility X2, X7 | verify | |
There was a problem hiding this comment.
Core skips onClick only for a tap inside the label's link (packages/core/src/Item/Item.tsx:730–739, anchor at :766–779); a tap on the start content, end content or padding still runs it. So "as core's link row skips it" holds only for the words, and approving FR5 also decides that a relative href is refused and that any scheme, javascript: included, becomes a Link, where core's row follows the navigation-destinations rules.
| var linkURL: URL? { | ||
| // Only an absolute URL the system can open; a malformed or relative | ||
| // `href` leaves the row static or a button. | ||
| guard let href = props.href, !href.isEmpty, let url = URL(string: href), url.scheme != nil else { return nil } |
There was a problem hiding this comment.
Any URL with a scheme passes this guard, so javascript:, vbscript: and data:text/html become a Link handed to the app's openURL, while core's row renders them inert (useLinkComponent.ts:33). FR5 should say whether the Swift row applies that rule; the owner's FR5 decision settles it.
27271c8 to
0b070d5
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on the new draft record component:swift/Item: no current spec yet states its requirements (FR1–FR6, AV1–AV2, ORD1–ORD2, PR1, AR1–AR4) or OQ1's choice to keep swipeActions and swipeBehavior out of every native contract, and FR5 departs from core's web Item by never running onClick beside a URL href and by refusing relative paths such as /calendar. An engineering owner records that decision by commenting /approve-spec <full head SHA> on the head that carries the fixes below, and the author may do so as an engineering owner. Please fix these on that head:
Item.swift:265turnsjavascript:,vbscript:anddata:text/htmlinto a SwiftUILink, butspec:AST-005FR3 and FR6 require those destinations to render inert, as core's row does; refuse them after FR2's normalization and add a test per prefix.AstryxNodeView.swift:26builds an Item wire node from its props alone, so itsmarker,startContentandendContentchildren are never drawn, againstspec:AST-082FR19; pass them throughAstryxNodeListas the Avatar case does and add a wire test with a content slot.
[Automated review]
| var linkURL: URL? { | ||
| // Only an absolute URL the system can open; a malformed or relative | ||
| // `href` leaves the row static or a button. | ||
| guard let href = props.href, !href.isEmpty, let url = URL(string: href), url.scheme != nil else { return nil } |
There was a problem hiding this comment.
Any URL with a scheme passes this guard, so javascript:, vbscript: and data:text/html (in any letter case) become a SwiftUI Link handed to the app's openURL. spec:AST-005 FR1, FR3 and FR6 require Astryx to render those destinations inert, as core's useLinkComponent does, and a native spec cannot change the core contract (spec:AST-093 FR1), so approving FR5 does not settle it. Normalizing as AST-005 FR2 says and refusing the three prefixes here, with a test per prefix, fixes it; the row then draws as its Button when onClick is set.
|
/approve-spec 0b070d5 |
0b070d5 to
d38db0e
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on the draft component:swift/Item record: the earlier /approve-spec named the previous head, so an engineering owner (the author may, as one) records it again by commenting /approve-spec <full head SHA> on the head that carries the fixes below. Independently of that decision, this head needs changes:
- Keep
javascript:,vbscript:anddata:text/htmlhrefs inert (Item.swift:265), as SwiftButton.safeDestinationalready does, ideally through one shared helper rather than a copy of Avatar's check. - Draw an Item wire node's
marker,startContentandendContent(AstryxNodeView.swift:28) and add a wire test with a content slot. - Say in FR5 and the description that core runs
onClickfor a tap outside the label, point the description at the new record path and DEC-1, and set the record'sschema_versionandtemplate_versionto 3 and 7.
[Automated review]
| var linkURL: URL? { | ||
| // Only an absolute URL the system can open; a malformed or relative | ||
| // `href` leaves the row static or a button. | ||
| guard let href = props.href, !href.isEmpty, let url = URL(string: href), url.scheme != nil else { return nil } |
There was a problem hiding this comment.
Any URL with a scheme passes this guard, so javascript:, vbscript: and data:text/html become a SwiftUI Link handed to the app's openURL (spec:AST-005 FR6). Button.safeDestination (Button.swift:91–105) already applies core's normalization; routing href through it, or one shared helper, keeps those destinations inert.
| Avatar(avatar.viewProps(send: send)) { AstryxNodeList(nodes: avatar.props.status ?? [], send: send) } | ||
| case .avatarStatusDot(let dot): AvatarStatusDot(dot.viewProps(send: send)) | ||
| case .thumbnail(let thumbnail): Thumbnail(thumbnail.viewProps(send: send)) | ||
| case .item(let item): Item(item.viewProps(send: send)) |
There was a problem hiding this comment.
Only the props reach the view, so an Item node's marker, startContent and endContent children are decoded and never drawn (spec:AST-082 FR19). Passing each slot through AstryxNodeList, as the Avatar case above does, fixes it.
| @@ -0,0 +1,192 @@ | |||
| --- | |||
| schema_version: 5 | |||
There was a problem hiding this comment.
No v5 knowledge schema exists and the component template is schema_version: 3, template_version: 7, as every sibling Swift record uses.
d38db0e to
c6fec79
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on the draft component:swift/Item record (FR1–FR6, AV1–AV2, ORD1–ORD2, PR1, AR1–AR4 and DEC-1, including FR5's whole-row link that never runs onClick and its refusal of relative hrefs); the earlier /approve-spec named a previous head, so an engineering owner, the author included, records it by commenting /approve-spec <full head SHA> on the head that carries the fix below. Blocked href schemes now stay inert and Item wire nodes draw their slots, as asked last time.
Item.swift:101–114copies Avatar'sisUnusableHrefand development check; please move that check besideastryxSafeDestinationinBase/SafeDestination.swift, call it from both Avatar and Item, and give Item's call the same normalizedhrefits link uses (todayhttps://example.comopens as a link while a development build stops on it).
[Automated review]
| private func checkInDevelopment() { | ||
| #if DEBUG | ||
| if Item.isUnusableHref(props.href) { | ||
| assertionFailure("Item href \"\(props.href ?? "")\" is not an absolute URL an app can open; it draws without a link.") | ||
| } | ||
| #endif | ||
| } | ||
|
|
||
| /// A non-empty `href` that is not an absolute URL. | ||
| 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 | ||
| } |
There was a problem hiding this comment.
This check and isUnusableHref repeat Avatar.isUnusableHref and Avatar's development check line for line (Avatar.swift:78–83, :111–115). They also read the raw href, while linkURL (:267) reads the trimmed, control-stripped value, so https://example.com draws a working link while a development build stops on it. Could this move beside astryxSafeDestination in Base/SafeDestination.swift as one function both views call, with Item passing it props.href.flatMap(astryxSafeDestination)?
c6fec79 to
0d6475f
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on the draft component:swift/Item record (FR1–FR6, AV1–AV2, ORD1–ORD2, PR1, AR1–AR4 and DEC-1, including FR5's whole-row link that never runs onClick and the refused relative href), which no current spec yet states. Separately, the Swift tests no longer compile: aWebPathIsAnItemHrefTheAppCannotOpen in ItemTests.swift builds an Item without @MainActor, so test-native-swift fails before running any test. Once that is fixed, an engineering owner records the decision by commenting /approve-spec <full head SHA> on the fixed head, and the author may do so if they are an engineering owner.
[Automated review]
| // The check reads the href as the link does: a padded absolute URL opens | ||
| // as a link and is not flagged. | ||
| #expect(!Item<EmptyView, EmptyView, EmptyView>.isUnusableHref(" https://example.com ")) | ||
| #expect(Item("Ada", href: " https://example.com ").linkURL == URL(string: "https://example.com")) |
There was a problem hiding this comment.
This test is not main-actor isolated, and Item(...) and linkURL are, so under Swift 6 this line does not compile and test-native-swift stops before running any Swift test. Adding @MainActor to aWebPathIsAnItemHrefTheAppCannotOpen (line 172), as the other view-building Item tests have, or moving this line into hrefMakesALinkAndStatesReadFromTheContract, fixes it.
0d6475f to
737417c
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on the draft component:swift/Item record (FR1–FR6, AV1–AV2, ORD1–ORD2, PR1, AR1–AR4 and DEC-1), including FR5's two departures from core's link row: a row with an absolute-URL href never runs onClick, and a relative href is refused. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner. Before that, please map Item's id and isHidden through the shared astryxBase in Base/BaseProps.swift instead of the private ItemIdentifier copy, so a later fix to base-prop mapping also reaches Item rows. The Swift test compile error is fixed, and all Item tests now pass on macOS and the iOS Simulator.
[Automated review]
| public var body: some View { | ||
| if props.base.isHidden != true { | ||
| control | ||
| .modifier(ItemIdentifier(id: props.base.id)) |
There was a problem hiding this comment.
This and ItemIdentifier (lines 366–377) repeat the base-prop mapping that astryxBase(_:) in Base/BaseProps.swift:7 already owns, which Button, Card, Heading and Text call. Could body draw control.onAppear(perform: checkInDevelopment).astryxBase(props.base) and drop ItemIdentifier, so a later fix to how id and isHidden map reaches Item rows too?
737417c to
c42ee83
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on the draft component:swift/Item record (FR1–FR6, AV1–AV2, ORD1–ORD2, PR1, AR1–AR4 and DEC-1), including FR5's two departures from core's link row: a row with an absolute-URL href never runs onClick, and a relative href is refused. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner. Before that, please limit label and description lines through the shared astryxLineLimit in Text/TypeStyle.swift instead of Item's own lines copy, so labelLines: 0 shows every line as core's row does and a later fix to line counts reaches Item too. Base props now go through the shared astryxBase, which resolves the earlier finding.
[Automated review]
| nonisolated static func lines(_ value: Double?) -> Int { | ||
| guard let value, value >= 1 else { return 1 } | ||
| return Int(min(value, Double(maximumLines))) | ||
| } |
There was a problem hiding this comment.
This repeats the line-count conversion astryxLineLimit already owns (Text/TypeStyle.swift:140, called by Text and Heading), and it has drifted: lines(0) is one line, while core's Item clamps only when labelLines > 1 and draws every line for 0 (packages/core/src/Item/Item.tsx:739–741). Could lines 179 and 185 call .lineLimit(astryxLineLimit(props.labelLines ?? 1)) and .lineLimit(astryxLineLimit(props.descriptionLines ?? 1)), with lines and maximumLines deleted and lineCountsComeFromTheContract asserting the 0 case against core?
c42ee83 to
1f7ab52
Compare
There was a problem hiding this comment.
No code changes are needed on this head: line counts now go through the shared astryxLineLimit, so labelLines: 0 shows every line as core's Item does. Approval waits on an engineering owner's decision on the draft component:swift/Item record (FR1–FR6, AV1–AV2, ORD1–ORD2, PR1, AR1–AR4 and DEC-1), including FR5's two departures from core's link row: a row with an absolute-URL href never runs onClick, and a relative href such as /calendar is refused. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as one), or names the requirement that should change.
[Automated review]
|
/approve-spec 1f7ab52 |
There was a problem hiding this comment.
This is ready to approve: an engineering owner approved the component:swift/Item record on this exact head, and the Swift Item view, its generated binding and its wire case match that record and the current native specs. CI is green, including the Swift tests on macOS and the iOS Simulator, and no changes are needed.
[Automated review]
User need
Almost every iOS screen is rows: people lists, settings, menus, search results. There is no Swift
Item, so each app builds its own row with its own padding, truncation, selected color, and tap handling. The person using the app gets rows that don't line up with each other or with the web, tap targets that are too small, and VoiceOver reading a row as several unrelated pieces.Problem and solution fit
Itemview. Core's row: marker, start content, the label over or beside its description, end content, with core's density padding, alignment (center,start,firstLine), line limits, and the theme's highlighted, selected, and disabled looks.hrefthe row is a SwiftUILinkthe system opens; with onlyonClicka SwiftUIButton. It draws the same either way, is at least 44 pt tall when tappable, and a disabled link or button stays reachable, reads as dimmed, and does nothing (component API A4).@ViewBuilderclosures with empty defaults (slots S3), so anAvatar, anIcon, or a plain view fits withoutAnyView. An Item node sent by a logic core draws its marker, start content, and end content nodes the same way (AST-082 FR19).javascript:,vbscript:, ordata:text/htmlhrefleaves the row inert, never a link, through the same check SwiftButtonuses (core's navigation destinations FR4). The development check that stops on a web-pathhrefnow lives beside it and is shared with SwiftAvatar; Item's check reads thehrefas its link does (trimmed). Avatar's behavior is unchanged: it renders and links exactly as before for everyhref.Current specification
spec:AST-082FR3 (only props a platform honors), FR4 (typed slots), FR7.spec:AST-093FR1–FR5.component:swift/Itematdocs/specs/components/Item/Item.swift.spec.md(draft,platform_of: component:Item), beside core's Item record per AST-093 FR1. Its DEC-1 records that swipe actions are web-only for now.Public delta and caller burden
Item(Astryx) and generatedItemProps,ItemAlign,ItemDensity,ItemLayout(AstryxNative).onClickcomes last, by label or as a trailingonClick:closure. A caller who setsmarkerpasses every slot by label.swipeActionsandswipeBehavior, which are web-only for now (DEC-1: SwiftUI's swipe actions work only inside a SwiftUIList, so a row elsewhere could not honor them),target,rel,data-testid(the baseidis the accessibility identifier).hrefmust be an absolute URL; a web path stops a development build with its value and draws without a link in release. A link row does not runonClick. In core the link is the label and a click elsewhere on the row runsonClick; a SwiftUILinktakes the whole row, so the Swift row has no such area (FR5).Design and alternatives
Listdoes not fit: it is a scrolling container that owns its rows, while core's Item sits in any container (AST-093 FR2). The row usesLinkandButtonfor taps and draws its own layout.Evidence
Capture set: iPhone 16 simulator, iOS 18.5, Xcode 16.4, built from this PR's head, with
IconandAvatartaken from their open pull requests for the slot content. Each capture shows people rows (button, selected, disabled), settings rows (highlighted, link), the three densities, the three alignments, the inline layout, a two-line label, and two themes.Captures are from before the rebase onto the new spec layout; the views draw the same, and the record's location, DEC-1, the safe-destination check, and the wire slots are covered by the tests below.
Open-work search
Searched current records and open pull requests before creating
component:swift/Item, by canonical id (component:swift/Item,component:Item), paths (packages/native/apple/Sources/Astryx/Item/,docs/specs/components/Item/,packages/native/src/components/Item.ts), symbols (Item,ItemProps,List,ListItem,swipeActions), and terms (native,swift,item). No other record or open pull request owns this platform component; the Swift views moved onto core names in #8299 and the Icon slot in #8302 are its neighbors.Scope
Writing rubric results
List, which AST-093 FR2 requires the spec to name.Testing
check:knowledgepasses with the record at its new path.vitest run packages/native/src/contract: the Item contract (text and content slots, press, link, swipes and HTML attributes on the web) passes; the committed Swift binding matches the contracts.swiftc -typecheckofAstryxNative,Astryx, and the tests passes. An immediate-mode render checks density padding, the 44 pt tappable rows, selected over highlighted in the default and two other themes, the dimmed disabled row,startandcenteralignment, a three-line label, and that a hidden row draws nothing. Tests also check that refusedhrefs never make a link and that an Item wire node draws its content slots.Every changed file is under
packages/native/, plus the new record indocs/specs/components/Item/.