@astryxdesign/native 0.7.5-canary.10ca8ea → 0.7.5-canary.116f54b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/apple/Package.swift +7 -1
  2. package/apple/README.md +37 -20
  3. package/apple/Sources/Astryx/Banner/{AstryxBanner.swift → Banner.swift} +6 -6
  4. package/apple/Sources/Astryx/Base/BaseProps.swift +16 -0
  5. package/apple/Sources/Astryx/Button/{AstryxButton.swift → Button.swift} +8 -7
  6. package/apple/Sources/Astryx/Card/{AstryxCard.swift → Card.swift} +1 -1
  7. package/apple/Sources/Astryx/Exports.swift +8 -0
  8. package/apple/Sources/Astryx/Heading/Heading.swift +117 -0
  9. package/apple/Sources/Astryx/Icon/Icon.spec.md +186 -0
  10. package/apple/Sources/Astryx/Icon/Icon.swift +155 -0
  11. package/apple/Sources/Astryx/Text/Text.swift +102 -0
  12. package/apple/Sources/Astryx/Text/TypeStyle.swift +143 -0
  13. package/apple/Sources/Astryx/TextInput/{AstryxTextInput.swift → TextInput.swift} +4 -4
  14. package/apple/Sources/Astryx/Theme/AstryxIcons.swift +72 -1
  15. package/apple/Sources/Astryx/Theme/Generated/ButterTheme.swift +34 -98
  16. package/apple/Sources/Astryx/Theme/Generated/ChocolateTheme.swift +34 -98
  17. package/apple/Sources/Astryx/Theme/Generated/DefaultTheme.swift +34 -98
  18. package/apple/Sources/Astryx/Theme/Generated/GothicTheme.swift +34 -98
  19. package/apple/Sources/Astryx/Theme/Generated/MatchaTheme.swift +34 -98
  20. package/apple/Sources/Astryx/Theme/Generated/NeutralTheme.swift +34 -98
  21. package/apple/Sources/Astryx/Theme/Generated/StoneTheme.swift +34 -98
  22. package/apple/Sources/Astryx/Theme/Generated/Tokens.swift +246 -244
  23. package/apple/Sources/Astryx/Theme/Generated/Y2kTheme.swift +34 -98
  24. package/apple/Sources/Astryx/Theme/Theme.swift +14 -11
  25. package/apple/Sources/AstryxNative/Generated/AstryxNode.swift +3 -0
  26. package/apple/Sources/AstryxNative/Generated/Button.swift +14 -2
  27. package/apple/Sources/AstryxNative/Generated/Icon.swift +106 -0
  28. package/apple/Sources/AstryxNative/Generated/IconName.swift +47 -0
  29. package/apple/Tests/AstryxTests/BannerTests.swift +4 -4
  30. package/apple/Tests/AstryxTests/ButtonTests.swift +13 -13
  31. package/apple/Tests/AstryxTests/CardTests.swift +1 -1
  32. package/apple/Tests/AstryxTests/EnvironmentTests.swift +5 -5
  33. package/apple/Tests/AstryxTests/HeadingTests.swift +41 -13
  34. package/apple/Tests/AstryxTests/IconTests.swift +188 -0
  35. package/apple/Tests/AstryxTests/NamesTests.swift +28 -0
  36. package/apple/Tests/AstryxTests/Resources/astryx-test-icon.png +0 -0
  37. package/apple/Tests/AstryxTests/TextInputTests.swift +3 -3
  38. package/apple/Tests/AstryxTests/TextTests.swift +92 -8
  39. package/apple/Tests/AstryxTests/ThemeTests.swift +2 -2
  40. package/apple/Tests/AstryxTests/TwoThemeTests.swift +5 -5
  41. package/dist/components/Button.d.ts.map +1 -1
  42. package/dist/components/Button.js +1 -0
  43. package/dist/components/Icon.d.ts +10 -0
  44. package/dist/components/Icon.d.ts.map +1 -0
  45. package/dist/components/Icon.js +7 -0
  46. package/dist/components/index.d.ts.map +1 -1
  47. package/dist/components/index.js +2 -0
  48. package/dist/contract/fromComponentDoc.d.ts +11 -2
  49. package/dist/contract/fromComponentDoc.d.ts.map +1 -1
  50. package/dist/contract/fromComponentDoc.js +16 -0
  51. package/dist/contract/generateSwift.d.ts.map +1 -1
  52. package/dist/contract/generateSwift.js +33 -0
  53. package/dist/contract/generateTypeScript.d.ts.map +1 -1
  54. package/dist/contract/generateTypeScript.js +11 -2
  55. package/dist/contract/types.d.ts +9 -0
  56. package/dist/contract/types.d.ts.map +1 -1
  57. package/dist/types/android.d.ts +14 -4
  58. package/dist/types/core.d.ts +10 -0
  59. package/dist/types/ios.d.ts +14 -4
  60. package/dist/types/macos.d.ts +14 -4
  61. package/dist/types/shared.d.ts +14 -4
  62. package/dist/types/windows.d.ts +14 -4
  63. package/package.json +4 -4
  64. package/src/components/Button.ts +1 -0
  65. package/src/components/Icon.ts +18 -0
  66. package/src/components/index.ts +2 -0
  67. package/src/contract/contracts.test.ts +37 -0
  68. package/src/contract/fromComponentDoc.test.ts +29 -0
  69. package/src/contract/fromComponentDoc.ts +32 -2
  70. package/src/contract/generateSwift.ts +38 -0
  71. package/src/contract/generateTypeScript.ts +11 -2
  72. package/src/contract/types.ts +9 -0
  73. package/src/theme/swiftIOSTheme.test.ts +38 -5
  74. package/apple/Sources/Astryx/Heading/AstryxHeading.swift +0 -46
  75. package/apple/Sources/Astryx/Text/AstryxText.swift +0 -83
@@ -27,9 +27,15 @@ let package = Package(
27
27
  name: "Astryx",
28
28
  dependencies: ["AstryxNative"],
29
29
  path: "Sources/Astryx",
30
+ // Each component's spec lives beside its view (AST-093 FR1).
31
+ exclude: ["Icon/Icon.spec.md"],
30
32
  resources: [.process("Resources")]
31
33
  ),
32
34
  .testTarget(name: "AstryxNativeTests", dependencies: ["AstryxNative"], path: "Tests/AstryxNativeTests"),
33
- .testTarget(name: "AstryxTests", dependencies: ["Astryx"], path: "Tests/AstryxTests"),
35
+ .testTarget(
36
+ name: "AstryxTests", dependencies: ["Astryx"], path: "Tests/AstryxTests",
37
+ // A tiny image that stands in for an app-shipped icon asset.
38
+ resources: [.process("Resources")]
39
+ ),
34
40
  ]
35
41
  )
package/apple/README.md CHANGED
@@ -26,12 +26,13 @@ apple/
26
26
  │ │ │ └── Generated/ # typed tokens and every published theme, generated; do not edit
27
27
  │ │ ├── Resources/ # core's messages per locale, generated; do not edit
28
28
  │ │ ├── Localization/
29
- │ │ ├── Text/ # AstryxText
30
- │ │ ├── Heading/ # AstryxHeading
31
- │ │ ├── Button/ # AstryxButton
32
- │ │ ├── TextInput/ # AstryxTextInput
33
- │ │ ├── Card/ # AstryxCard
34
- │ │ └── Banner/ # AstryxBanner
29
+ │ │ ├── Icon/ # Icon, Icon.spec.md
30
+ │ │ ├── Text/ # Text
31
+ │ │ ├── Heading/ # Heading
32
+ │ │ ├── Button/ # Button
33
+ │ │ ├── TextInput/ # TextInput
34
+ │ │ ├── Card/ # Card
35
+ │ │ └── Banner/ # Banner
35
36
  │ └── AstryxNative/ # contracts, wire, logic host (module AstryxNative)
36
37
  │ └── Generated/ # generated; do not edit
37
38
  ├── Tests/
@@ -44,17 +45,22 @@ apple/
44
45
  ```
45
46
 
46
47
  - **`Sources/Astryx/<Name>/`** holds one folder per component, named as in
47
- `packages/core/src/<Name>/`. A component's parameters use core's prop names
48
+ `packages/core/src/<Name>/`. Each view has core's component name in the
49
+ `Astryx` module (`Button`, `Text`), which re-exports SwiftUI and the
50
+ generated contracts, so an app file writes `import Astryx` alone. In a file
51
+ that also imports SwiftUI, write `Astryx.Button` and `SwiftUI.Button`. A
52
+ component's parameters use core's prop names
48
53
  and values (`variant: .primary`, `size: .lg`, `isLoading`), so code reads
49
- the same across web and native, with six exceptions, the interim shape
50
- AST-093 FR5 exempts until the components move onto the generated contracts:
51
- - `AstryxButton` takes `action` where core has `onClick`.
52
- - `AstryxButton` takes `fillsWidth` where core has `width="100%"`.
53
- - `AstryxButton` takes `icon` as a shared icon name (`"search"`), which the
54
+ the same across web and native. `Text` and `Heading` take the generated
55
+ `TextProps` and `HeadingProps`, as AST-093 FR5 asks; the others keep five
56
+ exceptions, the interim shape FR5 exempts until they move onto the generated
57
+ contracts:
58
+ - `Button` takes `action` where core has `onClick`.
59
+ - `Button` takes `fillsWidth` where core has `width="100%"`.
60
+ - `Button` takes `icon` as a shared icon name (`"search"`), which the
54
61
  generated `ButtonProps` does not carry yet.
55
- - `AstryxTextInput` takes a `Binding` where core has `value` and `onChange`.
56
- - `AstryxHeading`'s `level` is optional where core requires it.
57
- - `AstryxCard` takes `padding` as a whole spacing step, so it cannot spell
62
+ - `TextInput` takes a `Binding` where core has `value` and `onChange`.
63
+ - `Card` takes `padding` as a whole spacing step, so it cannot spell
58
64
  core's `0.5` and `1.5` steps.
59
65
  - **`Templates/`** holds one folder per page template, named after the
60
66
  `astryx template` id. Templates are clone-only, as on the web: you copy a
@@ -74,7 +80,6 @@ app.
74
80
 
75
81
  ```swift
76
82
  import Astryx
77
- import SwiftUI
78
83
 
79
84
  @main
80
85
  struct DemoApp: App {
@@ -95,11 +100,22 @@ size.
95
100
  Components read every color, size, and font from the `astryx` environment
96
101
  value, never from a constant of their own. `astryx.tokens` holds the theme's
97
102
  tokens in the current mode as typed members (`tokens.color.textPrimary`,
98
- `tokens.space.s4`, `tokens.font.bodySize`), so a misspelled token fails to
99
- compile. Text sizes grow with Dynamic Type. The components still read some
103
+ `tokens.space.s4`, `tokens.font.body.size`), so a misspelled token fails to
104
+ compile. Font tokens come grouped: each type style (`tokens.font.body`,
105
+ `tokens.font.heading1`) holds its `size`, `weight`, and `leading`, and
106
+ `tokens.font.family`, `.size`, and `.weight` hold the scales. Text sizes grow with Dynamic Type. The components still read some
100
107
  tokens by core's name (`astryx.color("color-accent")`); moving them onto the
101
108
  typed members comes next.
102
109
 
110
+ `Icon(.search)` draws a shared icon name as the platform's SF Symbol, at
111
+ core's icon size and color, hidden from VoiceOver unless it has a `label`.
112
+ `IconName` holds every built-in name as a static member; a theme maps a name
113
+ to another symbol or to an image the app ships, or registers a
114
+ `<library>:<name>` name, with
115
+ `Theme(…, icons: ["acme:rocket": "airplane", "close": .assetName("brand.close")])`.
116
+ An asset the app does not ship stops a development build with a message that
117
+ names it, and draws nothing in a release build.
118
+
103
119
  Font families come from the theme's `font-family-*` stacks: the first family
104
120
  the device has wins, and a system family such as `-apple-system` or
105
121
  `sans-serif` is the system font. Icons use core's shared icon names
@@ -173,8 +189,9 @@ Both packages also declare macOS 14 so they build and test on a Mac without a
173
189
  simulator; iOS 17 is the target. Open `Templates/Package.swift` in Xcode to
174
190
  run `LoginTemplate` in an iOS simulator through its preview.
175
191
 
176
- To add a component, create `Sources/Astryx/<Name>/Astryx<Name>.swift` with
177
- core's prop names, read its tokens from `astryx`, and add tests under
192
+ To add a component, create `Sources/Astryx/<Name>/<Name>.swift` declaring
193
+ `<Name>` with core's prop names, writing `SwiftUI.<Name>` wherever it means the
194
+ system type, read its tokens from `astryx`, and add tests under
178
195
  `Tests/AstryxTests/`. To add a template, create
179
196
  `Templates/<Name>/<Name>Template.swift` from the output of
180
197
  `astryx template <id>`, with tests under `Templates/Tests/`.
@@ -35,7 +35,7 @@ public enum AstryxBannerStatus: String, CaseIterable, Sendable {
35
35
  /// A message about the page or a section of it. The status sets the color
36
36
  /// and icon only; the title must say what happened, because the icon is
37
37
  /// decorative to VoiceOver.
38
- public struct AstryxBanner: View {
38
+ public struct Banner: View {
39
39
  let status: AstryxBannerStatus
40
40
  let title: String
41
41
  let description: String?
@@ -49,17 +49,17 @@ public struct AstryxBanner: View {
49
49
 
50
50
  public var body: some View {
51
51
  HStack(alignment: .firstTextBaseline, spacing: astryx.space(2)) {
52
- if let symbol = astryx.icon(status.iconName) {
52
+ if let icon = astryx.icon(status.iconName) {
53
53
  // Sized with the title so it grows with Dynamic Type.
54
- Image(systemName: symbol)
54
+ icon.sized(astryx.fontSize("text-body-size", 14))
55
55
  .font(astryx.font(size: "text-body-size", 14, weight: "text-body-weight"))
56
56
  .foregroundStyle(astryx.color(status.iconToken))
57
57
  .accessibilityHidden(true)
58
58
  }
59
59
  VStack(alignment: .leading, spacing: astryx.space(1)) {
60
- AstryxText(title, weight: .semibold)
60
+ Text(title, weight: .semibold)
61
61
  if let description {
62
- AstryxText(description, color: .secondary)
62
+ Text(description, color: .secondary)
63
63
  }
64
64
  }
65
65
  .frame(maxWidth: .infinity, alignment: .leading)
@@ -70,7 +70,7 @@ public struct AstryxBanner: View {
70
70
  .fill(astryx.color(status.groundToken))
71
71
  )
72
72
  .accessibilityElement(children: .ignore)
73
- .accessibilityLabel(Text(accessibilityText))
73
+ .accessibilityLabel(SwiftUI.Text(accessibilityText))
74
74
  }
75
75
 
76
76
  /// What VoiceOver reads: the title, then the description.
@@ -0,0 +1,16 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ extension View {
4
+ /// Core's base props on a view: `id` names it for UI tests, and `isHidden`
5
+ /// removes it from layout and from assistive technology, as core's `hidden`
6
+ /// attribute does.
7
+ @ViewBuilder func astryxBase(_ base: AstryxBaseProps) -> some View {
8
+ if base.isHidden == true {
9
+ EmptyView()
10
+ } else if let id = base.id {
11
+ accessibilityIdentifier(id)
12
+ } else {
13
+ self
14
+ }
15
+ }
16
+ }
@@ -29,7 +29,7 @@ public enum AstryxButtonSize: String, CaseIterable, Sendable {
29
29
 
30
30
  /// A labeled action, painted with the theme's tokens. The tap area reaches
31
31
  /// the platform's 44-by-44-point minimum even when the drawn button is smaller.
32
- public struct AstryxButton: View {
32
+ public struct Button: View {
33
33
  let label: String
34
34
  let variant: AstryxButtonVariant
35
35
  let size: AstryxButtonSize
@@ -66,14 +66,15 @@ public struct AstryxButton: View {
66
66
  }
67
67
 
68
68
  public var body: some View {
69
- Button(action: activate) {
69
+ SwiftUI.Button(action: activate) {
70
70
  // The label keeps its place while loading so the width holds; the
71
71
  // spinner draws over it.
72
72
  HStack(spacing: astryx.space(2)) {
73
- if let icon, let symbol = astryx.icon(icon) {
74
- Image(systemName: symbol)
73
+ if let icon, let image = astryx.icon(icon) {
74
+ // An asset matches the label's text size, as a symbol does.
75
+ image.sized(astryx.fontSize("text-label-size", 14))
75
76
  }
76
- Text(label).lineLimit(1)
77
+ SwiftUI.Text(label).lineLimit(1)
77
78
  }
78
79
  .opacity(isLoading ? 0 : 1)
79
80
  .overlay {
@@ -94,9 +95,9 @@ public struct AstryxButton: View {
94
95
  // As core's Button: a loading button stays undimmed and focusable, keeps
95
96
  // its label, and says Loading; taps do nothing until it finishes.
96
97
  .disabled(isDisabled)
97
- .accessibilityLabel(Text(accessibilityText))
98
+ .accessibilityLabel(SwiftUI.Text(accessibilityText))
98
99
  .accessibilityAddTraits(Self.accessibilityTraits)
99
- .accessibilityValue(Text(loadingValue ?? ""))
100
+ .accessibilityValue(SwiftUI.Text(loadingValue ?? ""))
100
101
  .onChange(of: isLoading) { _, loading in
101
102
  if loading { AccessibilityNotification.Announcement(AstryxStrings.buttonLoading).post() }
102
103
  }
@@ -4,7 +4,7 @@ import SwiftUI
4
4
 
5
5
  /// A card: one independent item on the theme's card ground, with a hairline
6
6
  /// border and the container radius.
7
- public struct AstryxCard<Content: View>: View {
7
+ public struct Card<Content: View>: View {
8
8
  let padding: Int
9
9
  let content: Content
10
10
  @Environment(\.astryx) private var astryx
@@ -0,0 +1,8 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ // `import Astryx` is the only import an app file needs: it brings SwiftUI and
4
+ // the generated contracts with it. In a file that imports only Astryx, a name
5
+ // Astryx shares with SwiftUI (`Button`, `Text`) means Astryx's view, and
6
+ // `SwiftUI.Button` names the system one (Swift component API spec, A1, A1a).
7
+ @_exported import AstryxNative
8
+ @_exported import SwiftUI
@@ -0,0 +1,117 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ // Adapted from github.com/ejc3/astryx-swift (MIT).
3
+
4
+ /// A heading at levels 1 through 6, painted with the theme's heading tokens
5
+ /// and exposed to VoiceOver as a heading. It takes core's Heading props
6
+ /// (`HeadingProps`).
7
+ public struct Heading: View {
8
+ public let props: HeadingProps
9
+ @Environment(\.astryx) private var astryx
10
+ @Environment(\.multilineTextAlignment) private var inheritedAlignment
11
+
12
+ public init(_ props: HeadingProps) {
13
+ self.props = props
14
+ }
15
+
16
+ /// - Parameters:
17
+ /// - children: the words, core's `children`.
18
+ /// - level: core's `level`: the heading's place in the outline, which
19
+ /// also sets its look unless `type` is given.
20
+ /// - type: core's `type`, a display style that replaces the level's look.
21
+ /// - weight: core's `weight`, replacing the look's weight.
22
+ /// - accessibilityLevel: core's `accessibilityLevel`, the level VoiceOver
23
+ /// reads when it differs from `level`.
24
+ /// - color: core's `color`; primary when nil.
25
+ /// - maxLines: core's `maxLines`; text past it is truncated. Unset, a
26
+ /// surrounding `.lineLimit` still applies.
27
+ /// - justify: core's `justify`, the alignment of wrapped lines.
28
+ /// - hasStrikethrough: core's `hasStrikethrough`.
29
+ /// - base: core's base props.
30
+ public init(
31
+ _ children: String,
32
+ level: HeadingLevel,
33
+ type: HeadingType? = nil,
34
+ weight: HeadingWeight? = nil,
35
+ accessibilityLevel: HeadingAccessibilityLevel? = nil,
36
+ color: HeadingColor? = nil,
37
+ maxLines: Double? = nil,
38
+ justify: HeadingJustify? = nil,
39
+ hasStrikethrough: Bool? = nil,
40
+ base: AstryxBaseProps = AstryxBaseProps()
41
+ ) {
42
+ self.init(
43
+ HeadingProps(
44
+ level: level,
45
+ type: type,
46
+ weight: weight,
47
+ children: children,
48
+ accessibilityLevel: accessibilityLevel,
49
+ color: color,
50
+ maxLines: maxLines,
51
+ justify: justify,
52
+ hasStrikethrough: hasStrikethrough,
53
+ base: base
54
+ )
55
+ )
56
+ }
57
+
58
+ public var body: some View {
59
+ styled(SwiftUI.Text(props.children))
60
+ .astryxMaxLines(props.maxLines)
61
+ .multilineTextAlignment(props.justify?.alignment ?? inheritedAlignment)
62
+ .astryxJustified(props.justify?.frameAlignment)
63
+ .accessibilityAddTraits(Self.accessibilityTraits)
64
+ .accessibilityHeading(headingLevel)
65
+ .astryxBase(props.base)
66
+ }
67
+
68
+ func styled(_ text: SwiftUI.Text) -> SwiftUI.Text {
69
+ var style = typeStyle
70
+ if let weight = props.weight {
71
+ style.weight = astryx.tokens.font.weight.value(named: weight.rawValue)
72
+ }
73
+ var text = text.font(astryx.font(style))
74
+ if let color = astryx.tokens.color.text(named: (props.color ?? .primary).rawValue) {
75
+ text = text.foregroundStyle(color)
76
+ }
77
+ if props.hasStrikethrough == true { text = text.strikethrough() }
78
+ return text
79
+ }
80
+
81
+ /// The level's heading style, or the display style `type` names, in the
82
+ /// theme's heading family.
83
+ var typeStyle: TypeStyle {
84
+ let font = astryx.tokens.font
85
+ let style: TypeStyleTokens =
86
+ switch props.type {
87
+ case .display1: font.display1
88
+ case .display2: font.display2
89
+ case .display3: font.display3
90
+ case nil:
91
+ switch props.level {
92
+ case .v1: font.heading1
93
+ case .v2: font.heading2
94
+ case .v3: font.heading3
95
+ case .v4: font.heading4
96
+ case .v5: font.heading5
97
+ case .v6: font.heading6
98
+ }
99
+ }
100
+ return TypeStyle(style, families: font.family.heading)
101
+ }
102
+
103
+ /// The traits assistive technology gets: a header.
104
+ static var accessibilityTraits: AccessibilityTraits { .isHeader }
105
+
106
+ /// The level VoiceOver reads: `accessibilityLevel` when given, else `level`.
107
+ var headingLevel: AccessibilityHeadingLevel {
108
+ switch props.accessibilityLevel?.rawValue ?? props.level.rawValue {
109
+ case 1: .h1
110
+ case 2: .h2
111
+ case 3: .h3
112
+ case 4: .h4
113
+ case 5: .h5
114
+ default: .h6
115
+ }
116
+ }
117
+ }
@@ -0,0 +1,186 @@
1
+ ---
2
+ schema_version: 5
3
+ template_version: 8
4
+ kind: component
5
+ id: component:swift/Icon
6
+ platform_of: component:Icon
7
+ authority: draft
8
+ archive_reason: null
9
+ superseded_by: null
10
+ approved_by: null
11
+ approved_at: null
12
+ owners: [cixzhang]
13
+ review_triggers: [public-api, behavior, theming, accessibility]
14
+ verified_by:
15
+ [
16
+ packages/native/apple/Tests/AstryxTests/IconTests.swift,
17
+ packages/native/src/contract/contracts.test.ts,
18
+ ]
19
+ modules: []
20
+ families: []
21
+ design_specs: []
22
+ architecture:
23
+ [
24
+ architecture:component-size-cascade,
25
+ architecture:icon-resolution-and-component-slots,
26
+ ]
27
+ contributing: []
28
+ system_specs: [spec:AST-082, spec:AST-093]
29
+ ---
30
+
31
+ # Icon (Swift) component contract
32
+
33
+ ## Contract at a glance
34
+
35
+ | Area | Contract |
36
+ | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
37
+ | Public contract | `Icon(_ icon: IconName, color: IconColor? = nil, size: IconSize? = nil, label: String? = nil)` and `Icon(_ props: IconProps)`, a SwiftUI `View` in module `Astryx`. `IconName` is a string-backed name with every built-in name as a static member (`.search`, `.close`). |
38
+ | Behavior | Draws the image the theme or the platform maps the name to (an SF Symbol, or an asset the app ships), in a square box at core's icon size grown with Dynamic Type, in core's icon color token or the surrounding foreground. A name nothing maps, or an asset the app does not ship, draws nothing. |
39
+ | End-user impact | A person sees the platform's own symbol for each shared icon, or the app's own artwork where its theme maps one, sized and colored as on the web, and VoiceOver reads only icons that carry a label. |
40
+ | Builder impact | An app passes a shared name (`.search`) or a name it maps in its theme (`"acme:rocket"`). An SVG component, which the web also accepts, has no Swift form. |
41
+ | Compatibility/readiness | Additive: a new view and a new generated `IconName` type. Draft; built and tested on macOS and the iOS Simulator; device captures are attached to the change. |
42
+ | Review checks | Reject a substitute symbol for an unknown name, an image or attributed-text form of the icon slot, a visual parameter beyond the contract, an unlabeled icon that VoiceOver reads, or an owner's size winning over an explicit `size`. |
43
+ | Governing rules | `spec:AST-082` FR4, FR7, FR17; `spec:AST-093` FR1–FR5; the Swift component API, styles, slots, theme environment, and accessibility records under `packages/native/specs/swift/library/`. |
44
+
45
+ This table is a review projection; the body below is authoritative.
46
+
47
+ ## Intent
48
+
49
+ Icon on Apple platforms draws Astryx's shared icon names as SF Symbols, or as
50
+ images the app ships where its theme maps them, so an
51
+ icon beside system controls looks like the platform's own while its size, color,
52
+ and accessibility follow the core `Icon` contract.
53
+
54
+ ## Compatibility and migration
55
+
56
+ - Released default preserved: `not yet released`
57
+ - Compatibility class: additive
58
+ - Controlled/uncontrolled behavior: not applicable
59
+ - Migration decision: none
60
+
61
+ Consumer migration instructions belong in consumer docs and release notes.
62
+
63
+ ## Ownership boundary
64
+
65
+ **Owns**
66
+
67
+ - Resolving a shared icon name to an image: the theme's mapping (an SF Symbol
68
+ or an app asset), else the platform's default SF Symbol.
69
+ - Drawing the image in a square box at core's icon size, grown with Dynamic
70
+ Type, in core's icon color token. An asset takes the color token when its
71
+ asset catalog renders it as a template image, and keeps its own colors when
72
+ it renders as an original image.
73
+ - The icon's VoiceOver exposure.
74
+
75
+ **Does not own / non-goals**
76
+
77
+ - The meaning of `icon`, `color`, `size`, and `label` and the built-in names —
78
+ owned by `component:Icon` and `spec:AST-082` FR17.
79
+ - Which size an owner gives the icons in its icon slot — owned by each owning
80
+ component and `architecture:component-size-cascade`.
81
+ - The default SF Symbol for each name — owned by `@astryxdesign/native`'s icon
82
+ defaults, which a test keeps equal to the Swift map.
83
+
84
+ ## Public concepts
85
+
86
+ | Concept | Closed values or states | Meaning | Availability by variant/orientation/state | Default | Owner | Stability | Invalid-value behavior |
87
+ | ------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------- | -------------------------- | ---------------------- | ------------ | ----------------------------------------------------------------------- |
88
+ | icon name | the built-in names; a `<library>:<name>` the theme maps | `IconName`, a string on the wire, with each built-in as a static member | always | required | `component:Icon` | experimental | an unmapped name stops a development build and draws nothing in release |
89
+ | symbol source | theme mapping; platform default | `Theme(icons:)` replaces a built-in or registers a library name with an SF Symbol or an app asset | always | platform default | `component:swift/Icon` | experimental | a theme entry wins over the default |
90
+ | size | `xsm`, `sm`, `md`, `lg`; unset | 12, 16, 20, 24 points at the default text size | always | owner's icon size, else md | `component:Icon` | experimental | unset takes the owner's size, then `md` |
91
+ | color | `primary`, `secondary`, `tertiary`, `disabled`, `accent`, `success`, `error`, `warning`, `inherit`; unset | core's icon color tokens; `inherit` keeps the surrounding foreground | always | `inherit` | `component:Icon` | experimental | unset is `inherit` |
92
+
93
+ ## Behavioral and layout contract
94
+
95
+ | ID | Candidate invariant | Basis | Draft review state |
96
+ | --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ------------------ |
97
+ | FR1 | The view MUST draw the image the theme's `icons` maps the name to, an SF Symbol or an asset the app ships, else the platform default for a built-in name. A name neither maps, or an asset the app does not ship, MUST stop a development build with a message naming it and draw nothing in a release build, never a substitute. | `spec:AST-082` FR17; Swift slots S2 | verify |
98
+ | FR2 | The symbol MUST fit a square box whose side is core's size in points (12, 16, 20, 24 for `xsm`, `sm`, `md`, `lg`), multiplied by the Dynamic Type scale the theme environment reads. | core `Icon` sizes; `spec:AST-093` FR3; theme environment E1 | verify |
99
+ | FR3 | The view MUST use its own `size`, else the size its owning component gives icons in its documented icon slot, else `md`. | `architecture:component-size-cascade`; Swift slots S4 | verify |
100
+ | FR4 | The view MUST draw in core's token for its `color` — primary and secondary icon tokens, the secondary icon token for `tertiary`, the disabled icon token, and the accent, success, error, and warning colors — and, for `inherit` or no color, MUST keep the surrounding foreground style. | core `Icon` color map; `spec:AST-093` FR4 | verify |
101
+ | FR5 | Both initializers MUST build the same `IconProps`; the labeled initializer takes the contract's props with the generated types and defaults, and base props (`id`, `isHidden`) come through `IconProps`. With `isHidden` the view draws nothing; `id` becomes the accessibility identifier. | Swift component API A2, A3, A7; `spec:AST-082` FR7 | verify |
102
+
103
+ ### Allowed variation
104
+
105
+ - **AV1 — Glyph shape.** The glyph is the platform's SF Symbol, not the web's
106
+ SVG; a theme may map any name to another SF Symbol or to an image the app
107
+ ships (`spec:AST-082` FR17).
108
+
109
+ ### Representative states
110
+
111
+ | State | Required invariant | Allowed variation |
112
+ | --------------------------- | --------------------------------------------- | ---------------------------- |
113
+ | every size | box side 12, 16, 20, 24 points at Large text | glyph proportions per symbol |
114
+ | every color, light and dark | the color's token in the current mode | none |
115
+ | inside an owner's icon slot | the owner's size unless the icon sets its own | none |
116
+ | largest text size | the box grows with Dynamic Type | none |
117
+ | labeled and unlabeled | VoiceOver reads only the labeled icon | none |
118
+
119
+ ### Transformation and precedence order
120
+
121
+ - **ORD1 — Image.** Theme mapping (SF Symbol or app asset) → platform default
122
+ SF Symbol → nothing.
123
+ - **ORD2 — Size.** Explicit `size` → owner's icon size → `md`, then × Dynamic
124
+ Type scale.
125
+
126
+ ### Performance and resources
127
+
128
+ - **PR1 — No image data of its own.** The view draws a system symbol by name,
129
+ or an asset from the app's main bundle by name; it holds no image data and
130
+ loads nothing from the network.
131
+
132
+ ## Accessibility contract
133
+
134
+ - **AR1 — Decorative unless labeled.** With no `label`, or an empty one, the
135
+ view is hidden from VoiceOver. With a label, it is one image element named by
136
+ the label, keeping SwiftUI's image trait and adding none (`spec:AST-082` FR17;
137
+ Swift accessibility X1, X4).
138
+ - **AR2 — Rendered tree not measured.** Tests assert what the view sets; the
139
+ rendered accessibility tree is not measured until an app-hosted target exists.
140
+
141
+ ## Design relationships
142
+
143
+ | Anatomy or state | Design requirement | Representation authority | Hierarchy role | Component contract |
144
+ | ---------------- | -------------------------------------------------------------- | -------------------------- | -------------- | ------------------ |
145
+ | Glyph | SF Symbols (Apple Human Interface Guidelines), or an app asset | prescribed by the platform | supporting | FR1, FR2 |
146
+
147
+ The component implements design requirements without copying their rationale.
148
+
149
+ Platform control: SwiftUI `Image(systemName:)`, or `Image(_:bundle:)` for an
150
+ app asset. It is the platform's own icon view and satisfies the contract, so no custom drawing is needed (`spec:AST-093`
151
+ FR2). Guideline pages: SF Symbols and Accessibility in the Apple Human
152
+ Interface Guidelines.
153
+
154
+ Theme values read: `color.iconPrimary`, `color.iconSecondary`,
155
+ `color.iconDisabled`, `color.accent`, `color.success`, `color.error`,
156
+ `color.warning`, the theme's `icons` map, and the Dynamic Type scale.
157
+
158
+ ## Family and system relationships
159
+
160
+ - `component:Icon` owns the props, the built-in names, the size scale, and the
161
+ color map; this record states only how Swift draws them.
162
+ - `architecture:component-size-cascade` owns when an owner's icon size applies;
163
+ owners set it through an internal environment value this view reads.
164
+
165
+ ## Verification map
166
+
167
+ | Contract | Verification | Representative states | Mutation or failure expectation | Audit section |
168
+ | -------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | -------------------------- |
169
+ | FR1 | `IconTests.swift` symbol and asset tests (a test bundle stands in for the app's); `contracts.test.ts` IconName test | every built-in; theme replacement; library name; a shipped asset; a missing asset drawing nothing and reported (the release-build path) | a missing default, a symbol absent on the platform, a missing asset that draws or goes unreported, or a substitute fails | `audit:swift/Icon/symbols` |
170
+ | FR2, FR3 | `IconTests.swift` size and Dynamic Type renders | each size; owner size; explicit size; AX5 | a wrong side or an owner size beating an explicit size fails | `audit:swift/Icon/size` |
171
+ | FR4 | `IconTests.swift` color map and two-theme render | every color; neutral and butter | a wrong token or a color of the view's own fails | `audit:swift/Icon/color` |
172
+ | FR5 | `IconTests.swift` initializer and hidden tests | both initializers; `isHidden` | differing props or a drawn hidden icon fails | `audit:swift/Icon/api` |
173
+ | AR1 | `IconTests.swift` accessibility assertions | labeled, unlabeled, empty label | an unlabeled icon with a label, or a labeled one without, fails | `audit:swift/Icon/a11y` |
174
+
175
+ ## Decision log
176
+
177
+ No component-local decision is recorded.
178
+
179
+ ## Open questions
180
+
181
+ None.
182
+
183
+ ## Content boundary
184
+
185
+ This file does not duplicate consumer prop tables/examples, current audit
186
+ results, implementation steps, or family/system rules. It links to their owners.