@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.
- package/apple/Package.swift +7 -1
- package/apple/README.md +37 -20
- package/apple/Sources/Astryx/Banner/{AstryxBanner.swift → Banner.swift} +6 -6
- package/apple/Sources/Astryx/Base/BaseProps.swift +16 -0
- package/apple/Sources/Astryx/Button/{AstryxButton.swift → Button.swift} +8 -7
- package/apple/Sources/Astryx/Card/{AstryxCard.swift → Card.swift} +1 -1
- package/apple/Sources/Astryx/Exports.swift +8 -0
- package/apple/Sources/Astryx/Heading/Heading.swift +117 -0
- package/apple/Sources/Astryx/Icon/Icon.spec.md +186 -0
- package/apple/Sources/Astryx/Icon/Icon.swift +155 -0
- package/apple/Sources/Astryx/Text/Text.swift +102 -0
- package/apple/Sources/Astryx/Text/TypeStyle.swift +143 -0
- package/apple/Sources/Astryx/TextInput/{AstryxTextInput.swift → TextInput.swift} +4 -4
- package/apple/Sources/Astryx/Theme/AstryxIcons.swift +72 -1
- package/apple/Sources/Astryx/Theme/Generated/ButterTheme.swift +34 -98
- package/apple/Sources/Astryx/Theme/Generated/ChocolateTheme.swift +34 -98
- package/apple/Sources/Astryx/Theme/Generated/DefaultTheme.swift +34 -98
- package/apple/Sources/Astryx/Theme/Generated/GothicTheme.swift +34 -98
- package/apple/Sources/Astryx/Theme/Generated/MatchaTheme.swift +34 -98
- package/apple/Sources/Astryx/Theme/Generated/NeutralTheme.swift +34 -98
- package/apple/Sources/Astryx/Theme/Generated/StoneTheme.swift +34 -98
- package/apple/Sources/Astryx/Theme/Generated/Tokens.swift +246 -244
- package/apple/Sources/Astryx/Theme/Generated/Y2kTheme.swift +34 -98
- package/apple/Sources/Astryx/Theme/Theme.swift +14 -11
- package/apple/Sources/AstryxNative/Generated/AstryxNode.swift +3 -0
- package/apple/Sources/AstryxNative/Generated/Button.swift +14 -2
- package/apple/Sources/AstryxNative/Generated/Icon.swift +106 -0
- package/apple/Sources/AstryxNative/Generated/IconName.swift +47 -0
- package/apple/Tests/AstryxTests/BannerTests.swift +4 -4
- package/apple/Tests/AstryxTests/ButtonTests.swift +13 -13
- package/apple/Tests/AstryxTests/CardTests.swift +1 -1
- package/apple/Tests/AstryxTests/EnvironmentTests.swift +5 -5
- package/apple/Tests/AstryxTests/HeadingTests.swift +41 -13
- package/apple/Tests/AstryxTests/IconTests.swift +188 -0
- package/apple/Tests/AstryxTests/NamesTests.swift +28 -0
- package/apple/Tests/AstryxTests/Resources/astryx-test-icon.png +0 -0
- package/apple/Tests/AstryxTests/TextInputTests.swift +3 -3
- package/apple/Tests/AstryxTests/TextTests.swift +92 -8
- package/apple/Tests/AstryxTests/ThemeTests.swift +2 -2
- package/apple/Tests/AstryxTests/TwoThemeTests.swift +5 -5
- package/dist/components/Button.d.ts.map +1 -1
- package/dist/components/Button.js +1 -0
- package/dist/components/Icon.d.ts +10 -0
- package/dist/components/Icon.d.ts.map +1 -0
- package/dist/components/Icon.js +7 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -0
- package/dist/contract/fromComponentDoc.d.ts +11 -2
- package/dist/contract/fromComponentDoc.d.ts.map +1 -1
- package/dist/contract/fromComponentDoc.js +16 -0
- package/dist/contract/generateSwift.d.ts.map +1 -1
- package/dist/contract/generateSwift.js +33 -0
- package/dist/contract/generateTypeScript.d.ts.map +1 -1
- package/dist/contract/generateTypeScript.js +11 -2
- package/dist/contract/types.d.ts +9 -0
- package/dist/contract/types.d.ts.map +1 -1
- package/dist/types/android.d.ts +14 -4
- package/dist/types/core.d.ts +10 -0
- package/dist/types/ios.d.ts +14 -4
- package/dist/types/macos.d.ts +14 -4
- package/dist/types/shared.d.ts +14 -4
- package/dist/types/windows.d.ts +14 -4
- package/package.json +4 -4
- package/src/components/Button.ts +1 -0
- package/src/components/Icon.ts +18 -0
- package/src/components/index.ts +2 -0
- package/src/contract/contracts.test.ts +37 -0
- package/src/contract/fromComponentDoc.test.ts +29 -0
- package/src/contract/fromComponentDoc.ts +32 -2
- package/src/contract/generateSwift.ts +38 -0
- package/src/contract/generateTypeScript.ts +11 -2
- package/src/contract/types.ts +9 -0
- package/src/theme/swiftIOSTheme.test.ts +38 -5
- package/apple/Sources/Astryx/Heading/AstryxHeading.swift +0 -46
- package/apple/Sources/Astryx/Text/AstryxText.swift +0 -83
package/apple/Package.swift
CHANGED
|
@@ -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(
|
|
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
|
-
│ │ ├──
|
|
30
|
-
│ │ ├──
|
|
31
|
-
│ │ ├──
|
|
32
|
-
│ │ ├──
|
|
33
|
-
│ │ ├──
|
|
34
|
-
│ │
|
|
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>/`.
|
|
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
|
|
50
|
-
AST-093 FR5
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
- `
|
|
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
|
-
- `
|
|
56
|
-
- `
|
|
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.
|
|
99
|
-
compile.
|
|
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
|
|
177
|
-
core's prop names,
|
|
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
|
|
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
|
|
52
|
+
if let icon = astryx.icon(status.iconName) {
|
|
53
53
|
// Sized with the title so it grows with Dynamic Type.
|
|
54
|
-
|
|
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
|
-
|
|
60
|
+
Text(title, weight: .semibold)
|
|
61
61
|
if let description {
|
|
62
|
-
|
|
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
|
|
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
|
|
74
|
-
|
|
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
|
|
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.
|