@astryxdesign/native 0.7.5-canary.f2dafed → 0.7.5-canary.f943c2b
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/README.md +128 -12
- package/apple/Package.swift +23 -4
- package/apple/README.md +199 -0
- package/apple/Sources/Astryx/Banner/Banner.swift +80 -0
- package/apple/Sources/Astryx/Button/Button.swift +167 -0
- package/apple/Sources/Astryx/Card/Card.swift +33 -0
- package/apple/Sources/Astryx/Exports.swift +8 -0
- package/apple/Sources/Astryx/Heading/Heading.swift +46 -0
- package/apple/Sources/Astryx/Icon/Icon.spec.md +182 -0
- package/apple/Sources/Astryx/Icon/Icon.swift +154 -0
- package/apple/Sources/Astryx/Localization/AstryxStrings.swift +22 -0
- package/apple/Sources/Astryx/Resources/af.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/ar.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/ca.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/cs.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/da.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/de.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/el.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/en.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/es.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/fi.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/fr.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/he.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/hu.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/it.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/ja.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/ko.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/nb.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/nl.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/pl.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/pt-BR.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/pt-PT.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/ro.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/ru.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/sr.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/sv.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/tr.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/uk.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/vi.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/zh-Hans.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Resources/zh-Hant.lproj/Localizable.strings +4 -0
- package/apple/Sources/Astryx/Text/Text.swift +83 -0
- package/apple/Sources/Astryx/TextInput/TextInput.swift +81 -0
- package/apple/Sources/Astryx/Theme/AstryxIcons.swift +37 -0
- package/apple/Sources/Astryx/Theme/Generated/ButterTheme.swift +493 -0
- package/apple/Sources/Astryx/Theme/Generated/ChocolateTheme.swift +493 -0
- package/apple/Sources/Astryx/Theme/Generated/DefaultTheme.swift +493 -0
- package/apple/Sources/Astryx/Theme/Generated/GothicTheme.swift +493 -0
- package/apple/Sources/Astryx/Theme/Generated/MatchaTheme.swift +493 -0
- package/apple/Sources/Astryx/Theme/Generated/NeutralTheme.swift +493 -0
- package/apple/Sources/Astryx/Theme/Generated/StoneTheme.swift +493 -0
- package/apple/Sources/Astryx/Theme/Generated/Tokens.swift +1260 -0
- package/apple/Sources/Astryx/Theme/Generated/Y2kTheme.swift +493 -0
- package/apple/Sources/Astryx/Theme/Theme.swift +270 -0
- package/apple/Sources/AstryxNative/Generated/AstryxBase.swift +1 -1
- package/apple/Sources/AstryxNative/Generated/AstryxNode.swift +72 -0
- package/apple/Sources/AstryxNative/Generated/AstryxWire.swift +67 -0
- package/apple/Sources/AstryxNative/Generated/Badge.swift +89 -0
- package/apple/Sources/AstryxNative/Generated/Button.swift +166 -0
- package/apple/Sources/AstryxNative/Generated/Card.swift +117 -0
- package/apple/Sources/AstryxNative/Generated/Divider.swift +97 -0
- package/apple/Sources/AstryxNative/Generated/HStack.swift +308 -0
- package/apple/Sources/AstryxNative/Generated/Heading.swift +169 -0
- package/apple/Sources/AstryxNative/Generated/Icon.swift +106 -0
- package/apple/Sources/AstryxNative/Generated/IconName.swift +47 -0
- package/apple/Sources/AstryxNative/Generated/ProgressBar.swift +144 -0
- package/apple/Sources/AstryxNative/Generated/Selector.swift +133 -9
- package/apple/Sources/AstryxNative/Generated/Switch.swift +202 -0
- package/apple/Sources/AstryxNative/Generated/Text.swift +172 -0
- package/apple/Sources/AstryxNative/Generated/TextInput.swift +243 -0
- package/apple/Sources/AstryxNative/Generated/VStack.swift +308 -0
- package/apple/Sources/AstryxNative/Logic/AstryxLogicHost.swift +102 -0
- package/apple/Tests/AstryxNativeTests/ControlPropsTests.swift +29 -0
- package/apple/Tests/AstryxNativeTests/LogicHostTests.swift +85 -0
- package/apple/Tests/AstryxNativeTests/WireTests.swift +53 -0
- package/apple/Tests/AstryxTests/BannerTests.swift +47 -0
- package/apple/Tests/AstryxTests/ButtonTests.swift +72 -0
- package/apple/Tests/AstryxTests/CardTests.swift +19 -0
- package/apple/Tests/AstryxTests/EnvironmentTests.swift +87 -0
- package/apple/Tests/AstryxTests/HeadingTests.swift +40 -0
- package/apple/Tests/AstryxTests/IconTests.swift +142 -0
- package/apple/Tests/AstryxTests/NamesTests.swift +28 -0
- package/apple/Tests/AstryxTests/Pixels.swift +68 -0
- package/apple/Tests/AstryxTests/StringsTests.swift +23 -0
- package/apple/Tests/AstryxTests/TextInputTests.swift +29 -0
- package/apple/Tests/AstryxTests/TextTests.swift +25 -0
- package/apple/Tests/AstryxTests/ThemeTests.swift +45 -0
- package/apple/Tests/AstryxTests/TwoThemeTests.swift +65 -0
- package/dist/components/Badge.d.ts +10 -0
- package/dist/components/Badge.d.ts.map +1 -0
- package/dist/components/Badge.js +7 -0
- package/dist/components/Button.d.ts +10 -0
- package/dist/components/Button.d.ts.map +1 -0
- package/dist/components/Button.js +16 -0
- package/dist/components/Card.d.ts +10 -0
- package/dist/components/Card.d.ts.map +1 -0
- package/dist/components/Card.js +7 -0
- package/dist/components/Divider.d.ts +10 -0
- package/dist/components/Divider.d.ts.map +1 -0
- package/dist/components/Divider.js +6 -0
- package/dist/components/HStack.d.ts +10 -0
- package/dist/components/HStack.d.ts.map +1 -0
- package/dist/components/HStack.js +9 -0
- package/dist/components/Heading.d.ts +10 -0
- package/dist/components/Heading.d.ts.map +1 -0
- package/dist/components/Heading.js +14 -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/ProgressBar.d.ts +10 -0
- package/dist/components/ProgressBar.d.ts.map +1 -0
- package/dist/components/ProgressBar.js +5 -0
- package/dist/components/Switch.d.ts +10 -0
- package/dist/components/Switch.d.ts.map +1 -0
- package/dist/components/Switch.js +6 -0
- package/dist/components/Text.d.ts +10 -0
- package/dist/components/Text.d.ts.map +1 -0
- package/dist/components/Text.js +13 -0
- package/dist/components/TextInput.d.ts +10 -0
- package/dist/components/TextInput.d.ts.map +1 -0
- package/dist/components/TextInput.js +9 -0
- package/dist/components/VStack.d.ts +10 -0
- package/dist/components/VStack.d.ts.map +1 -0
- package/dist/components/VStack.js +9 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +27 -1
- package/dist/components/stackShared.d.ts +9 -0
- package/dist/components/stackShared.d.ts.map +1 -0
- package/dist/components/stackShared.js +4 -0
- package/dist/contract/fromComponentDoc.d.ts +53 -5
- package/dist/contract/fromComponentDoc.d.ts.map +1 -1
- package/dist/contract/fromComponentDoc.js +63 -4
- package/dist/contract/generateSwift.d.ts.map +1 -1
- package/dist/contract/generateSwift.js +226 -10
- package/dist/contract/generateTypeScript.d.ts.map +1 -1
- package/dist/contract/generateTypeScript.js +78 -9
- package/dist/contract/index.d.ts +7 -3
- package/dist/contract/index.d.ts.map +1 -1
- package/dist/contract/index.js +2 -1
- package/dist/contract/parseDocType.d.ts +5 -2
- package/dist/contract/parseDocType.d.ts.map +1 -1
- package/dist/contract/parseDocType.js +34 -5
- package/dist/contract/rules.d.ts +5 -2
- package/dist/contract/rules.d.ts.map +1 -1
- package/dist/contract/rules.js +8 -4
- package/dist/contract/types.d.ts +20 -0
- package/dist/contract/types.d.ts.map +1 -1
- package/dist/logic/connectBridge.d.ts +29 -0
- package/dist/logic/connectBridge.d.ts.map +1 -0
- package/dist/logic/connectBridge.js +32 -0
- package/dist/logic/createLogicCore.d.ts +44 -0
- package/dist/logic/createLogicCore.d.ts.map +1 -0
- package/dist/logic/createLogicCore.js +40 -0
- package/dist/logic/index.d.ts +12 -0
- package/dist/logic/index.d.ts.map +1 -0
- package/dist/logic/index.js +10 -0
- package/dist/types/android.d.ts +146 -2
- package/dist/types/core.d.ts +160 -0
- package/dist/types/ios.d.ts +146 -2
- package/dist/types/macos.d.ts +146 -2
- package/dist/types/shared.d.ts +146 -2
- package/dist/types/windows.d.ts +146 -2
- package/package.json +12 -4
- package/src/components/Badge.ts +18 -0
- package/src/components/Button.ts +27 -0
- package/src/components/Card.ts +18 -0
- package/src/components/Divider.ts +17 -0
- package/src/components/HStack.ts +20 -0
- package/src/components/Heading.ts +25 -0
- package/src/components/Icon.ts +18 -0
- package/src/components/ProgressBar.ts +16 -0
- package/src/components/Switch.ts +17 -0
- package/src/components/Text.ts +24 -0
- package/src/components/TextInput.ts +20 -0
- package/src/components/VStack.ts +20 -0
- package/src/components/index.ts +27 -1
- package/src/components/stackShared.ts +14 -0
- package/src/contract/contracts.test.ts +95 -0
- package/src/contract/fromComponentDoc.test.ts +126 -0
- package/src/contract/fromComponentDoc.ts +136 -9
- package/src/contract/generateSwift.ts +261 -10
- package/src/contract/generateTypeScript.ts +92 -10
- package/src/contract/index.ts +13 -4
- package/src/contract/parseDocType.test.ts +45 -0
- package/src/contract/parseDocType.ts +41 -6
- package/src/contract/rules.ts +11 -4
- package/src/contract/types.ts +15 -0
- package/src/icons/swiftIcons.test.ts +22 -0
- package/src/logic/connectBridge.ts +72 -0
- package/src/logic/createLogicCore.ts +97 -0
- package/src/logic/index.ts +19 -0
- package/src/logic/logic.test.ts +160 -0
- package/src/theme/swiftIOSStrings.test.ts +49 -0
- package/src/theme/swiftIOSTheme.test.ts +223 -0
package/README.md
CHANGED
|
@@ -12,16 +12,23 @@ rests on evidence.
|
|
|
12
12
|
|
|
13
13
|
## What is here today
|
|
14
14
|
|
|
15
|
-
| Part | Path | What it does
|
|
16
|
-
| -------------- | --------------------------------- |
|
|
17
|
-
| Contracts | `src/contract/` | Reads a core component's documentation and splits its props into a portable **core contract** and a **web-only** part, with a reason for each web-only prop. Checks prop names against Swift and Kotlin keywords and platform toolkit names.
|
|
18
|
-
| Platform types | `@astryxdesign/native/components` | TypeScript props per platform, chosen by type-resolution condition: `ios`, `android`, `macos`, `windows`, or `shared` for the core contract only.
|
|
19
|
-
| Swift binding | `apple/` | A Swift package with props generated from the same contracts
|
|
20
|
-
| Native theme | `defineNativeTheme` | Durable theme intent (`material: 'opaque' \| 'translucent'`) plus platform entries scoped by OS version, such as Liquid Glass from iOS 26. Never sets tokens.
|
|
21
|
-
| Icons | `NATIVE_ICON_DEFAULTS` | Astryx's built-in icon names mapped to SF Symbols on Apple platforms and Material Symbols on Android.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
15
|
+
| Part | Path | What it does |
|
|
16
|
+
| -------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
17
|
+
| Contracts | `src/contract/` | Reads a core component's documentation and splits its props into a portable **core contract** and a **web-only** part, with a reason for each web-only prop. Checks prop names against Swift and Kotlin keywords and platform toolkit names. |
|
|
18
|
+
| Platform types | `@astryxdesign/native/components` | TypeScript props per platform, chosen by type-resolution condition: `ios`, `android`, `macos`, `windows`, or `shared` for the core contract only. |
|
|
19
|
+
| Swift binding | `apple/` | A Swift package with two products: `AstryxNative`, props generated from the same contracts (iOS and macOS props sit behind `#if os(...)`) plus the logic host, and `Astryx`, SwiftUI views drawn with core's tokens. Clone-only page templates sit in `apple/Templates/`. See [its README](apple/README.md). |
|
|
20
|
+
| Native theme | `defineNativeTheme` | Durable theme intent (`material: 'opaque' \| 'translucent'`) plus platform entries scoped by OS version, such as Liquid Glass from iOS 26. Never sets tokens. |
|
|
21
|
+
| Icons | `NATIVE_ICON_DEFAULTS` | Astryx's built-in icon names mapped to SF Symbols on Apple platforms and Material Symbols on Android. |
|
|
22
|
+
| Wire | `AstryxNode`, `AstryxAction` | Each component as JSON a TypeScript core sends a native host (arm C), and the action a host sends back when a view calls a callback. Swift decodes `<Name>Node` and turns it into `<Name>Props` with `viewProps(send:)`. |
|
|
23
|
+
| Logic core | `@astryxdesign/native/logic` | `createLogicCore` holds a screen's state, builds its node tree, and reduces actions; `connectBridge` carries it to a native host as JSON. ECMAScript built-ins only, so it runs in Hermes. Shaped after EJ Campbell's astryx-swift spike. |
|
|
24
|
+
| Logic host | `AstryxLogicHost` (Swift) | Runs a logic core bundle in JavaScriptCore, decodes each tree into `AstryxNode`, and sends views' actions back. A Hermes host speaks the same bridge. |
|
|
25
|
+
|
|
26
|
+
Contracts cover Badge, Button, Card, Divider, Heading, HStack, ProgressBar,
|
|
27
|
+
Selector, Switch, Text, TextInput, and VStack. The `Astryx` Swift product draws
|
|
28
|
+
Text, Heading, Button, TextInput, Card, and Banner, and `apple/Templates/` the
|
|
29
|
+
`login` template, in SwiftUI.
|
|
30
|
+
React Native and Android bindings are not built yet; see
|
|
31
|
+
[the experiment matrix](#experiment-matrix).
|
|
25
32
|
|
|
26
33
|
```ts
|
|
27
34
|
// tsconfig.json for an iOS app: "customConditions": ["ios"]
|
|
@@ -34,22 +41,131 @@ const props: SelectorProps = {
|
|
|
34
41
|
};
|
|
35
42
|
```
|
|
36
43
|
|
|
44
|
+
How core documentation becomes a contract:
|
|
45
|
+
|
|
46
|
+
- A callback whose arguments are values followed by the DOM event that caused
|
|
47
|
+
them, such as `(checked: boolean, e: ChangeEvent) => void`, keeps the values
|
|
48
|
+
in the core; the event stays on the web.
|
|
49
|
+
- A callback that carries only an event stays on the web, unless the component
|
|
50
|
+
names it in `callbacks`, as Button does for `onClick`. Its core form takes no
|
|
51
|
+
arguments. A callback with an event before a value stays on the web either
|
|
52
|
+
way.
|
|
53
|
+
- Free-form content becomes a typed slot only where a component declares one
|
|
54
|
+
in `slots`; `text` is a string, as for a badge's label or a heading's words,
|
|
55
|
+
and `content` is child Astryx components, as for a stack's or a card's
|
|
56
|
+
children. A SwiftUI view takes content as a view builder, so `<Name>Props`
|
|
57
|
+
leaves it out; `<Name>Node` carries it as child nodes.
|
|
58
|
+
- A closed set of numbers, such as a heading level, is a Swift enum with raw
|
|
59
|
+
values.
|
|
60
|
+
- A core prop may share a platform toolkit's name only when the component
|
|
61
|
+
declares in `sameMeaning` that it means the same thing there.
|
|
62
|
+
|
|
63
|
+
### The wire (arm C)
|
|
64
|
+
|
|
65
|
+
A TypeScript core sends a tree of nodes and receives actions. A node lists the
|
|
66
|
+
callbacks the core listens to in `events`; a view gets only those callbacks.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// tsconfig: "customConditions": ["ios"]
|
|
70
|
+
import type {AstryxAction, AstryxNode} from '@astryxdesign/native/components';
|
|
71
|
+
|
|
72
|
+
const screen: AstryxNode = {
|
|
73
|
+
component: 'VStack',
|
|
74
|
+
key: 'root',
|
|
75
|
+
props: {
|
|
76
|
+
gap: 2,
|
|
77
|
+
children: [
|
|
78
|
+
{
|
|
79
|
+
component: 'Switch',
|
|
80
|
+
key: 'wifi',
|
|
81
|
+
props: {label: 'Wi-Fi', value: true},
|
|
82
|
+
events: ['onChange'],
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
// What the host sends back when the switch is turned off:
|
|
88
|
+
const action: AstryxAction = {
|
|
89
|
+
key: 'wifi',
|
|
90
|
+
component: 'Switch',
|
|
91
|
+
event: 'onChange',
|
|
92
|
+
args: [false],
|
|
93
|
+
};
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```swift
|
|
97
|
+
let host = AstryxLogicHost(bundle: bundleSource) // calls the bundle's global astryxStart(host)
|
|
98
|
+
if case .switch(let wifi) = host.root {
|
|
99
|
+
let props = wifi.viewProps(send: host.send)
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
// tsconfig: "customConditions": ["ios"]
|
|
105
|
+
import type {AstryxAction, AstryxNode} from '@astryxdesign/native/components';
|
|
106
|
+
import {connectBridge, createLogicCore} from '@astryxdesign/native/logic';
|
|
107
|
+
import type {Bridge, BridgeHost} from '@astryxdesign/native/logic';
|
|
108
|
+
|
|
109
|
+
const core = createLogicCore<{wifi: boolean}, AstryxNode, AstryxAction>({
|
|
110
|
+
initial: {wifi: true},
|
|
111
|
+
view: state => ({
|
|
112
|
+
component: 'Switch',
|
|
113
|
+
key: 'wifi',
|
|
114
|
+
props: {label: 'Wi-Fi', value: state.wifi},
|
|
115
|
+
events: ['onChange'],
|
|
116
|
+
}),
|
|
117
|
+
reduce: (state, action) =>
|
|
118
|
+
action.component === 'Switch' && action.event === 'onChange'
|
|
119
|
+
? {wifi: action.args[0]}
|
|
120
|
+
: state,
|
|
121
|
+
});
|
|
122
|
+
// The bundle's entry: the native host calls it with its two calls in and
|
|
123
|
+
// gets back the bridge it reads snapshots from and dispatches actions to.
|
|
124
|
+
(globalThis as {astryxStart?: (host: BridgeHost) => Bridge}).astryxStart =
|
|
125
|
+
host => connectBridge(core, host);
|
|
126
|
+
```
|
|
127
|
+
|
|
37
128
|
## Working on it
|
|
38
129
|
|
|
39
130
|
```sh
|
|
131
|
+
pnpm -F @astryxdesign/native typecheck # source and tests
|
|
40
132
|
pnpm -F @astryxdesign/native build # dist/, including platform types
|
|
133
|
+
pnpm -F @astryxdesign/native typecheck:types # every platform's types, after build
|
|
41
134
|
pnpm -F @astryxdesign/native generate:apple # regenerate apple/Sources/.../Generated
|
|
135
|
+
pnpm -F @astryxdesign/native generate:swift-ios # regenerate the Astryx product's typed tokens, every published theme, and strings
|
|
42
136
|
pnpm vitest run --project node packages/native
|
|
43
137
|
```
|
|
44
138
|
|
|
45
139
|
Generated Swift is committed so a Swift package can pin a commit. A test fails
|
|
46
140
|
when it drifts from the contracts.
|
|
47
141
|
|
|
142
|
+
**Cleanup needed.** Committing the generated Swift
|
|
143
|
+
(`apple/Sources/AstryxNative/Generated`) is temporary. It is there only because
|
|
144
|
+
Swift Package Manager installs a package from a git commit and cannot run the
|
|
145
|
+
Node generator. The intended replacement: CI generates the Swift files and
|
|
146
|
+
publishes them to a Swift-only branch or tag that apps pin, and the generated
|
|
147
|
+
files are ignored on `main`.
|
|
148
|
+
|
|
48
149
|
Boundaries, checked by `pnpm check:package-boundaries`:
|
|
49
150
|
|
|
50
151
|
- `@astryxdesign/core` never imports this package.
|
|
51
|
-
-
|
|
52
|
-
|
|
152
|
+
- No file in this package, in any folder, imports core, apart from the
|
|
153
|
+
build-time reads AST-082 FR16 lists until core's portable entry ships: the
|
|
154
|
+
theme generator in `scripts/` may import `@astryxdesign/core/theme` and
|
|
155
|
+
`@astryxdesign/core/theme/tokens`; `scripts/swift-ios-strings.mjs` may
|
|
156
|
+
read core's locale catalogs (`@astryxdesign/core/locales/*.json`); and
|
|
157
|
+
`scripts/core-docs.mjs` reads core's component documentation and built-in
|
|
158
|
+
icon names as data.
|
|
159
|
+
- The check reads source text in every folder except `node_modules`, `dist`,
|
|
160
|
+
`.build`, and `.swiftpm`. It flags any other module specifier that names
|
|
161
|
+
core (`import`, `require`, `require.resolve`, `import.meta.resolve`, or a
|
|
162
|
+
relative path into `packages/core`). It also flags a path to core's package,
|
|
163
|
+
written as adjacent string segments (`'@astryxdesign', 'core'`,
|
|
164
|
+
`'node_modules', '@astryxdesign/core'`, `'packages', 'core'`) or inside one
|
|
165
|
+
string (`node_modules/@astryxdesign/core`, `packages/core`). In
|
|
166
|
+
`scripts/core-docs.mjs` it allows exactly one such path, the `CORE_ROOT`
|
|
167
|
+
definition, and only the listed reads under `CORE_ROOT`. It does not follow
|
|
168
|
+
paths computed in other ways.
|
|
53
169
|
|
|
54
170
|
## Experiment matrix
|
|
55
171
|
|
package/apple/Package.swift
CHANGED
|
@@ -1,18 +1,37 @@
|
|
|
1
1
|
// swift-tools-version: 6.0
|
|
2
2
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
3
|
|
|
4
|
-
// Experimental Astryx for Apple platforms (AST-082).
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// Experimental Astryx for Apple platforms (AST-082). Two products:
|
|
5
|
+
// - `Astryx`: SwiftUI views painted with the theme's tokens, the theme
|
|
6
|
+
// environment, and core's localized messages (Sources/Astryx).
|
|
7
|
+
// - `AstryxNative`: the contract types generated by `pnpm generate:apple`
|
|
8
|
+
// (Sources/AstryxNative/Generated; do not edit them by hand) and the logic
|
|
9
|
+
// host.
|
|
10
|
+
// Page templates are a separate package in Templates/, with no products,
|
|
11
|
+
// because they are clone-only source and are never published.
|
|
7
12
|
|
|
8
13
|
import PackageDescription
|
|
9
14
|
|
|
10
15
|
let package = Package(
|
|
11
16
|
name: "AstryxNative",
|
|
17
|
+
defaultLocalization: "en",
|
|
12
18
|
platforms: [.iOS(.v17), .macOS(.v14)],
|
|
13
|
-
products: [
|
|
19
|
+
products: [
|
|
20
|
+
.library(name: "Astryx", targets: ["Astryx"]),
|
|
21
|
+
.library(name: "AstryxNative", targets: ["AstryxNative"]),
|
|
22
|
+
],
|
|
14
23
|
targets: [
|
|
15
24
|
.target(name: "AstryxNative", path: "Sources/AstryxNative"),
|
|
25
|
+
// Resources holds core's catalog messages per locale, generated.
|
|
26
|
+
.target(
|
|
27
|
+
name: "Astryx",
|
|
28
|
+
dependencies: ["AstryxNative"],
|
|
29
|
+
path: "Sources/Astryx",
|
|
30
|
+
// Each component's spec lives beside its view (AST-093 FR1).
|
|
31
|
+
exclude: ["Icon/Icon.spec.md"],
|
|
32
|
+
resources: [.process("Resources")]
|
|
33
|
+
),
|
|
16
34
|
.testTarget(name: "AstryxNativeTests", dependencies: ["AstryxNative"], path: "Tests/AstryxNativeTests"),
|
|
35
|
+
.testTarget(name: "AstryxTests", dependencies: ["Astryx"], path: "Tests/AstryxTests"),
|
|
17
36
|
]
|
|
18
37
|
)
|
package/apple/README.md
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Astryx for Apple platforms
|
|
2
|
+
|
|
3
|
+
> **Experimental. Canary only.** Part of `@astryxdesign/native`. APIs change
|
|
4
|
+
> without notice.
|
|
5
|
+
|
|
6
|
+
One Swift package with two products:
|
|
7
|
+
|
|
8
|
+
- **`Astryx`**: SwiftUI views painted with the theme's tokens, the theme
|
|
9
|
+
environment, and core's localized messages. It is arm A of the AST-082
|
|
10
|
+
experiment (pure native: platform views painted with Astryx's tokens).
|
|
11
|
+
- **`AstryxNative`**: contract types generated from core's component
|
|
12
|
+
documentation, the wire a TypeScript logic core sends, and the logic host
|
|
13
|
+
that runs one (arm C).
|
|
14
|
+
|
|
15
|
+
Page templates built from the views are a separate package with no products,
|
|
16
|
+
because templates are clone-only and never published.
|
|
17
|
+
|
|
18
|
+
## Layout
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
apple/
|
|
22
|
+
├── Package.swift # products Astryx and AstryxNative
|
|
23
|
+
├── Sources/
|
|
24
|
+
│ ├── Astryx/ # SwiftUI views (module Astryx)
|
|
25
|
+
│ │ ├── Theme/ # tokens, the theme environment, Dynamic Type
|
|
26
|
+
│ │ │ └── Generated/ # typed tokens and every published theme, generated; do not edit
|
|
27
|
+
│ │ ├── Resources/ # core's messages per locale, generated; do not edit
|
|
28
|
+
│ │ ├── Localization/
|
|
29
|
+
│ │ ├── Icon/ # Icon, Icon.spec.md
|
|
30
|
+
│ │ ├── Text/ # Text
|
|
31
|
+
│ │ ├── Heading/ # Heading
|
|
32
|
+
│ │ ├── Button/ # Button
|
|
33
|
+
│ │ ├── TextInput/ # TextInput
|
|
34
|
+
│ │ ├── Card/ # Card
|
|
35
|
+
│ │ └── Banner/ # Banner
|
|
36
|
+
│ └── AstryxNative/ # contracts, wire, logic host (module AstryxNative)
|
|
37
|
+
│ └── Generated/ # generated; do not edit
|
|
38
|
+
├── Tests/
|
|
39
|
+
│ ├── AstryxTests/
|
|
40
|
+
│ └── AstryxNativeTests/
|
|
41
|
+
└── Templates/ # page templates to copy; its own package, never published
|
|
42
|
+
├── Package.swift # no products; compiles and tests the templates
|
|
43
|
+
├── Login/ # LoginTemplate, from `astryx template login`
|
|
44
|
+
└── Tests/
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- **`Sources/Astryx/<Name>/`** holds one folder per component, named as in
|
|
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
|
|
53
|
+
and values (`variant: .primary`, `size: .lg`, `isLoading`), so code reads
|
|
54
|
+
the same across web and native, with six exceptions, the interim shape
|
|
55
|
+
AST-093 FR5 exempts until the components move onto the generated contracts:
|
|
56
|
+
- `Button` takes `action` where core has `onClick`.
|
|
57
|
+
- `Button` takes `fillsWidth` where core has `width="100%"`.
|
|
58
|
+
- `Button` takes `icon` as a shared icon name (`"search"`), which the
|
|
59
|
+
generated `ButtonProps` does not carry yet.
|
|
60
|
+
- `TextInput` takes a `Binding` where core has `value` and `onChange`.
|
|
61
|
+
- `Heading`'s `level` is optional where core requires it.
|
|
62
|
+
- `Card` takes `padding` as a whole spacing step, so it cannot spell
|
|
63
|
+
core's `0.5` and `1.5` steps.
|
|
64
|
+
- **`Templates/`** holds one folder per page template, named after the
|
|
65
|
+
`astryx template` id. Templates are clone-only, as on the web: you copy a
|
|
66
|
+
template's folder into your app and own it from then on. They are a
|
|
67
|
+
separate package with no products, so nothing imports them, and
|
|
68
|
+
`@astryxdesign/native` leaves them out of its npm package. A template is
|
|
69
|
+
built only from `Astryx` views and SwiftUI layout, and matches the web
|
|
70
|
+
template's content and states. Rules a template needs, such as validation,
|
|
71
|
+
live in a plain value type beside the view so tests can check them without a
|
|
72
|
+
screen.
|
|
73
|
+
|
|
74
|
+
## Use it
|
|
75
|
+
|
|
76
|
+
Depend on the `Astryx` product. To start from a template, copy its folder,
|
|
77
|
+
such as `Templates/Login/`, into your app's sources; it then belongs to your
|
|
78
|
+
app.
|
|
79
|
+
|
|
80
|
+
```swift
|
|
81
|
+
import Astryx
|
|
82
|
+
|
|
83
|
+
@main
|
|
84
|
+
struct DemoApp: App {
|
|
85
|
+
var body: some Scene {
|
|
86
|
+
WindowGroup {
|
|
87
|
+
LoginTemplate().astryxTheme() // copied from Templates/Login/
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`astryxTheme(_:)` paints a subtree with a theme; without it, components use
|
|
94
|
+
core's default theme. Either way they follow Dark Mode and the person's text
|
|
95
|
+
size.
|
|
96
|
+
|
|
97
|
+
## Theme tokens
|
|
98
|
+
|
|
99
|
+
Components read every color, size, and font from the `astryx` environment
|
|
100
|
+
value, never from a constant of their own. `astryx.tokens` holds the theme's
|
|
101
|
+
tokens in the current mode as typed members (`tokens.color.textPrimary`,
|
|
102
|
+
`tokens.space.s4`, `tokens.font.bodySize`), so a misspelled token fails to
|
|
103
|
+
compile. Text sizes grow with Dynamic Type. The components still read some
|
|
104
|
+
tokens by core's name (`astryx.color("color-accent")`); moving them onto the
|
|
105
|
+
typed members comes next.
|
|
106
|
+
|
|
107
|
+
`Icon(.search)` draws a shared icon name as the platform's SF Symbol, at
|
|
108
|
+
core's icon size and color, hidden from VoiceOver unless it has a `label`.
|
|
109
|
+
`IconName` holds every built-in name as a static member; a theme maps a name
|
|
110
|
+
to another symbol, or registers a `<library>:<name>` name, with
|
|
111
|
+
`Theme(…, icons: ["acme:rocket": "airplane"])`.
|
|
112
|
+
|
|
113
|
+
Font families come from the theme's `font-family-*` stacks: the first family
|
|
114
|
+
the device has wins, and a system family such as `-apple-system` or
|
|
115
|
+
`sans-serif` is the system font. Icons use core's shared icon names
|
|
116
|
+
(`success`, `warning`, …), drawn as an SF Symbol by default; a theme replaces
|
|
117
|
+
one with `Theme(…, icons: ["success": "checkmark.seal.fill"])`.
|
|
118
|
+
|
|
119
|
+
`Sources/Astryx/Theme/Generated/` is written by the Swift theme generator
|
|
120
|
+
(`../specs/swift/library/theme-generator.md`) from core's own resolver, light
|
|
121
|
+
and dark:
|
|
122
|
+
|
|
123
|
+
- `Tokens.swift`: the typed token surface, from core's token names.
|
|
124
|
+
- One `<Name>Theme.swift` per theme, declaring `Theme.<name>`: core's
|
|
125
|
+
defaults (`.default`) and every published theme package (`.butter`,
|
|
126
|
+
`.chocolate`, `.gothic`, `.matcha`, `.neutral`, `.stone`, `.y2k`).
|
|
127
|
+
|
|
128
|
+
```swift
|
|
129
|
+
LoginTemplate().astryxTheme(.neutral)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Regenerate after a theme or core token change:
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
pnpm -F @astryxdesign/native generate:swift-ios
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
A product generates its own theme the same way, from its theme's module and
|
|
139
|
+
export:
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
node packages/native/scripts/swift-ios-theme.mjs --theme ./src/acmeTheme.ts#acmeTheme --out ./Sources/Theme
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
A theme's local tokens are not generated yet; the generator names each one it
|
|
146
|
+
leaves out.
|
|
147
|
+
|
|
148
|
+
**Fonts.** A theme's font families are carried as data. Astryx never bundles
|
|
149
|
+
a font: a component walks the theme's family list in order and uses the first
|
|
150
|
+
family the app has registered; a system family in the list (such as
|
|
151
|
+
`-apple-system` or `sans-serif`) is the system font, and a list with no
|
|
152
|
+
registered family falls back to the system font. To draw a theme's brand face, bundle and register it
|
|
153
|
+
in the app. Faces the published themes name (body, heading, code):
|
|
154
|
+
|
|
155
|
+
| Theme | Faces to bundle |
|
|
156
|
+
| ------------ | ------------------------------------------ |
|
|
157
|
+
| `.default` | none (system fonts) |
|
|
158
|
+
| `.butter` | Outfit, JetBrains Mono |
|
|
159
|
+
| `.chocolate` | Albert Sans, Fraunces, JetBrains Mono |
|
|
160
|
+
| `.gothic` | Fustat, JetBrains Mono |
|
|
161
|
+
| `.matcha` | DM Sans, Playwrite US Trad, JetBrains Mono |
|
|
162
|
+
| `.neutral` | Figtree |
|
|
163
|
+
| `.stone` | Figtree, Montserrat, JetBrains Mono |
|
|
164
|
+
| `.y2k` | Poppins, JetBrains Mono |
|
|
165
|
+
|
|
166
|
+
The same command writes
|
|
167
|
+
`Sources/Astryx/Resources/<locale>.lproj/Localizable.strings` from core's
|
|
168
|
+
locale catalogs, so words the components speak to assistive technology, such
|
|
169
|
+
as a loading button's "Loading", follow the person's language. Tests in
|
|
170
|
+
`@astryxdesign/native` fail when either committed output drifts from core.
|
|
171
|
+
|
|
172
|
+
## Working on it
|
|
173
|
+
|
|
174
|
+
On a Mac with Xcode or the Swift command-line tools:
|
|
175
|
+
|
|
176
|
+
```sh
|
|
177
|
+
cd packages/native/apple
|
|
178
|
+
swift test # both products
|
|
179
|
+
cd Templates && swift test # templates, against the Astryx product
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Both packages also declare macOS 14 so they build and test on a Mac without a
|
|
183
|
+
simulator; iOS 17 is the target. Open `Templates/Package.swift` in Xcode to
|
|
184
|
+
run `LoginTemplate` in an iOS simulator through its preview.
|
|
185
|
+
|
|
186
|
+
To add a component, create `Sources/Astryx/<Name>/<Name>.swift` declaring
|
|
187
|
+
`<Name>` with core's prop names, writing `SwiftUI.<Name>` wherever it means the
|
|
188
|
+
system type, read its tokens from `astryx`, and add tests under
|
|
189
|
+
`Tests/AstryxTests/`. To add a template, create
|
|
190
|
+
`Templates/<Name>/<Name>Template.swift` from the output of
|
|
191
|
+
`astryx template <id>`, with tests under `Templates/Tests/`.
|
|
192
|
+
|
|
193
|
+
## Not here yet
|
|
194
|
+
|
|
195
|
+
- The views do not use the generated contract types in `AstryxNative` yet.
|
|
196
|
+
Moving them onto those types is how a core prop change reaches Swift.
|
|
197
|
+
- Pixel and behavior parity with the web components has not been measured.
|
|
198
|
+
- Increase Contrast, theme-local tokens, and platform theme entries
|
|
199
|
+
(`defineNativeTheme`, reserved as `Theme.native`) are not wired in.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import SwiftUI
|
|
4
|
+
|
|
5
|
+
/// Core's `Banner` statuses.
|
|
6
|
+
public enum AstryxBannerStatus: String, CaseIterable, Sendable {
|
|
7
|
+
case info
|
|
8
|
+
case warning
|
|
9
|
+
case error
|
|
10
|
+
case success
|
|
11
|
+
|
|
12
|
+
/// The shared icon name for this status (AST-082 FR17), drawn as the
|
|
13
|
+
/// theme's or the platform's symbol for it.
|
|
14
|
+
var iconName: String { rawValue }
|
|
15
|
+
|
|
16
|
+
var groundToken: String {
|
|
17
|
+
switch self {
|
|
18
|
+
case .info: "color-accent-muted"
|
|
19
|
+
case .warning: "color-warning-muted"
|
|
20
|
+
case .error: "color-error-muted"
|
|
21
|
+
case .success: "color-success-muted"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
var iconToken: String {
|
|
26
|
+
switch self {
|
|
27
|
+
case .info: "color-accent"
|
|
28
|
+
case .warning: "color-warning"
|
|
29
|
+
case .error: "color-error"
|
|
30
|
+
case .success: "color-success"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// A message about the page or a section of it. The status sets the color
|
|
36
|
+
/// and icon only; the title must say what happened, because the icon is
|
|
37
|
+
/// decorative to VoiceOver.
|
|
38
|
+
public struct Banner: View {
|
|
39
|
+
let status: AstryxBannerStatus
|
|
40
|
+
let title: String
|
|
41
|
+
let description: String?
|
|
42
|
+
@Environment(\.astryx) private var astryx
|
|
43
|
+
|
|
44
|
+
public init(status: AstryxBannerStatus, title: String, description: String? = nil) {
|
|
45
|
+
self.status = status
|
|
46
|
+
self.title = title
|
|
47
|
+
self.description = description
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public var body: some View {
|
|
51
|
+
HStack(alignment: .firstTextBaseline, spacing: astryx.space(2)) {
|
|
52
|
+
if let symbol = astryx.icon(status.iconName) {
|
|
53
|
+
// Sized with the title so it grows with Dynamic Type.
|
|
54
|
+
Image(systemName: symbol)
|
|
55
|
+
.font(astryx.font(size: "text-body-size", 14, weight: "text-body-weight"))
|
|
56
|
+
.foregroundStyle(astryx.color(status.iconToken))
|
|
57
|
+
.accessibilityHidden(true)
|
|
58
|
+
}
|
|
59
|
+
VStack(alignment: .leading, spacing: astryx.space(1)) {
|
|
60
|
+
Text(title, weight: .semibold)
|
|
61
|
+
if let description {
|
|
62
|
+
Text(description, color: .secondary)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
66
|
+
}
|
|
67
|
+
.padding(astryx.space(3))
|
|
68
|
+
.background(
|
|
69
|
+
RoundedRectangle(cornerRadius: astryx.size("radius-container", 12), style: .continuous)
|
|
70
|
+
.fill(astryx.color(status.groundToken))
|
|
71
|
+
)
|
|
72
|
+
.accessibilityElement(children: .ignore)
|
|
73
|
+
.accessibilityLabel(SwiftUI.Text(accessibilityText))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// What VoiceOver reads: the title, then the description.
|
|
77
|
+
var accessibilityText: String {
|
|
78
|
+
description.map { "\(title), \($0)" } ?? title
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
// Adapted from github.com/ejc3/astryx-swift (MIT).
|
|
3
|
+
|
|
4
|
+
import SwiftUI
|
|
5
|
+
|
|
6
|
+
/// Core's `Button` variants.
|
|
7
|
+
public enum AstryxButtonVariant: String, CaseIterable, Sendable {
|
|
8
|
+
case primary
|
|
9
|
+
case secondary
|
|
10
|
+
case ghost
|
|
11
|
+
case destructive
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/// Core's `Button` sizes: `size-element-sm`, `-md`, and `-lg` high.
|
|
15
|
+
public enum AstryxButtonSize: String, CaseIterable, Sendable {
|
|
16
|
+
case sm
|
|
17
|
+
case md
|
|
18
|
+
case lg
|
|
19
|
+
|
|
20
|
+
var heightToken: String { "size-element-\(rawValue)" }
|
|
21
|
+
var fallbackHeight: CGFloat {
|
|
22
|
+
switch self {
|
|
23
|
+
case .sm: 28
|
|
24
|
+
case .md: 32
|
|
25
|
+
case .lg: 36
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// A labeled action, painted with the theme's tokens. The tap area reaches
|
|
31
|
+
/// the platform's 44-by-44-point minimum even when the drawn button is smaller.
|
|
32
|
+
public struct Button: View {
|
|
33
|
+
let label: String
|
|
34
|
+
let variant: AstryxButtonVariant
|
|
35
|
+
let size: AstryxButtonSize
|
|
36
|
+
let icon: String?
|
|
37
|
+
let isLoading: Bool
|
|
38
|
+
let isDisabled: Bool
|
|
39
|
+
let fillsWidth: Bool
|
|
40
|
+
let action: () -> Void
|
|
41
|
+
@Environment(\.astryx) private var astryx
|
|
42
|
+
|
|
43
|
+
/// - Parameters:
|
|
44
|
+
/// - icon: a shared icon name, such as `"search"`, shown before the
|
|
45
|
+
/// label; the theme's symbol for it, else the platform default.
|
|
46
|
+
/// - fillsWidth: the button takes the whole width it is offered, as
|
|
47
|
+
/// core's `width="100%"` does.
|
|
48
|
+
public init(
|
|
49
|
+
label: String,
|
|
50
|
+
variant: AstryxButtonVariant = .secondary,
|
|
51
|
+
size: AstryxButtonSize = .md,
|
|
52
|
+
icon: String? = nil,
|
|
53
|
+
isLoading: Bool = false,
|
|
54
|
+
isDisabled: Bool = false,
|
|
55
|
+
fillsWidth: Bool = false,
|
|
56
|
+
action: @escaping () -> Void
|
|
57
|
+
) {
|
|
58
|
+
self.label = label
|
|
59
|
+
self.variant = variant
|
|
60
|
+
self.size = size
|
|
61
|
+
self.icon = icon
|
|
62
|
+
self.isLoading = isLoading
|
|
63
|
+
self.isDisabled = isDisabled
|
|
64
|
+
self.fillsWidth = fillsWidth
|
|
65
|
+
self.action = action
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public var body: some View {
|
|
69
|
+
SwiftUI.Button(action: activate) {
|
|
70
|
+
// The label keeps its place while loading so the width holds; the
|
|
71
|
+
// spinner draws over it.
|
|
72
|
+
HStack(spacing: astryx.space(2)) {
|
|
73
|
+
if let icon, let symbol = astryx.icon(icon) {
|
|
74
|
+
Image(systemName: symbol)
|
|
75
|
+
}
|
|
76
|
+
SwiftUI.Text(label).lineLimit(1)
|
|
77
|
+
}
|
|
78
|
+
.opacity(isLoading ? 0 : 1)
|
|
79
|
+
.overlay {
|
|
80
|
+
if isLoading {
|
|
81
|
+
ProgressView().controlSize(.small).tint(foreground)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
.buttonStyle(
|
|
86
|
+
AstryxButtonStyle(
|
|
87
|
+
astryx: astryx,
|
|
88
|
+
size: size,
|
|
89
|
+
fillsWidth: fillsWidth,
|
|
90
|
+
foreground: foreground,
|
|
91
|
+
background: background
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
// As core's Button: a loading button stays undimmed and focusable, keeps
|
|
95
|
+
// its label, and says Loading; taps do nothing until it finishes.
|
|
96
|
+
.disabled(isDisabled)
|
|
97
|
+
.accessibilityLabel(SwiftUI.Text(accessibilityText))
|
|
98
|
+
.accessibilityAddTraits(Self.accessibilityTraits)
|
|
99
|
+
.accessibilityValue(SwiftUI.Text(loadingValue ?? ""))
|
|
100
|
+
.onChange(of: isLoading) { _, loading in
|
|
101
|
+
if loading { AccessibilityNotification.Announcement(AstryxStrings.buttonLoading).post() }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// The button's accessible name: its label, also while loading.
|
|
106
|
+
var accessibilityText: String { label }
|
|
107
|
+
|
|
108
|
+
/// The traits assistive technology gets: a button.
|
|
109
|
+
static var accessibilityTraits: AccessibilityTraits { .isButton }
|
|
110
|
+
|
|
111
|
+
/// What assistive technology reads after the label while loading: core's
|
|
112
|
+
/// catalog message, in the person's language.
|
|
113
|
+
var loadingValue: String? { isLoading ? AstryxStrings.buttonLoading : nil }
|
|
114
|
+
|
|
115
|
+
/// Runs the action unless the button is loading.
|
|
116
|
+
func activate() {
|
|
117
|
+
guard !isLoading else { return }
|
|
118
|
+
action()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private var foreground: Color {
|
|
122
|
+
switch variant {
|
|
123
|
+
case .primary: astryx.color("color-on-accent")
|
|
124
|
+
case .destructive: astryx.color("color-on-error")
|
|
125
|
+
case .secondary, .ghost: astryx.color("color-text-primary")
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private var background: Color {
|
|
130
|
+
switch variant {
|
|
131
|
+
case .primary: astryx.color("color-accent")
|
|
132
|
+
case .destructive: astryx.color("color-error")
|
|
133
|
+
case .secondary: astryx.color("color-neutral")
|
|
134
|
+
case .ghost: .clear
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
struct AstryxButtonStyle: ButtonStyle {
|
|
140
|
+
let astryx: AstryxStyle
|
|
141
|
+
let size: AstryxButtonSize
|
|
142
|
+
let fillsWidth: Bool
|
|
143
|
+
let foreground: Color
|
|
144
|
+
let background: Color
|
|
145
|
+
@Environment(\.isEnabled) private var isEnabled
|
|
146
|
+
|
|
147
|
+
func makeBody(configuration: Configuration) -> some View {
|
|
148
|
+
let shape = RoundedRectangle(
|
|
149
|
+
cornerRadius: astryx.size("radius-element", 8),
|
|
150
|
+
style: .continuous
|
|
151
|
+
)
|
|
152
|
+
// Grows with text size, but not so far that one line of a large label
|
|
153
|
+
// becomes a tall slab.
|
|
154
|
+
let height = astryx.size(size.heightToken, size.fallbackHeight) * min(astryx.textScale, 1.4)
|
|
155
|
+
configuration.label
|
|
156
|
+
.font(astryx.font(size: "text-label-size", 14, weight: "text-label-weight"))
|
|
157
|
+
.foregroundStyle(foreground)
|
|
158
|
+
.padding(.horizontal, astryx.space(3))
|
|
159
|
+
.frame(maxWidth: fillsWidth ? .infinity : nil, minHeight: height)
|
|
160
|
+
.background(shape.fill(background))
|
|
161
|
+
.opacity(isEnabled ? (configuration.isPressed ? 0.75 : 1) : 0.5)
|
|
162
|
+
// Drawn at the token size; laid out at least 44 by 44 so the whole
|
|
163
|
+
// platform-minimum tap area counts.
|
|
164
|
+
.frame(minWidth: 44, minHeight: 44)
|
|
165
|
+
.contentShape(Rectangle())
|
|
166
|
+
}
|
|
167
|
+
}
|