@astryxdesign/native 0.7.5-canary.f2dafed → 0.7.5-canary.f57df18

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 (197) hide show
  1. package/README.md +128 -12
  2. package/apple/Package.swift +27 -4
  3. package/apple/README.md +205 -0
  4. package/apple/Sources/Astryx/Banner/Banner.swift +80 -0
  5. package/apple/Sources/Astryx/Base/BaseProps.swift +16 -0
  6. package/apple/Sources/Astryx/Button/Button.swift +168 -0
  7. package/apple/Sources/Astryx/Card/Card.swift +33 -0
  8. package/apple/Sources/Astryx/Exports.swift +8 -0
  9. package/apple/Sources/Astryx/Heading/Heading.swift +117 -0
  10. package/apple/Sources/Astryx/Icon/Icon.spec.md +186 -0
  11. package/apple/Sources/Astryx/Icon/Icon.swift +155 -0
  12. package/apple/Sources/Astryx/Localization/AstryxStrings.swift +22 -0
  13. package/apple/Sources/Astryx/Resources/af.lproj/Localizable.strings +4 -0
  14. package/apple/Sources/Astryx/Resources/ar.lproj/Localizable.strings +4 -0
  15. package/apple/Sources/Astryx/Resources/ca.lproj/Localizable.strings +4 -0
  16. package/apple/Sources/Astryx/Resources/cs.lproj/Localizable.strings +4 -0
  17. package/apple/Sources/Astryx/Resources/da.lproj/Localizable.strings +4 -0
  18. package/apple/Sources/Astryx/Resources/de.lproj/Localizable.strings +4 -0
  19. package/apple/Sources/Astryx/Resources/el.lproj/Localizable.strings +4 -0
  20. package/apple/Sources/Astryx/Resources/en.lproj/Localizable.strings +4 -0
  21. package/apple/Sources/Astryx/Resources/es.lproj/Localizable.strings +4 -0
  22. package/apple/Sources/Astryx/Resources/fi.lproj/Localizable.strings +4 -0
  23. package/apple/Sources/Astryx/Resources/fr.lproj/Localizable.strings +4 -0
  24. package/apple/Sources/Astryx/Resources/he.lproj/Localizable.strings +4 -0
  25. package/apple/Sources/Astryx/Resources/hu.lproj/Localizable.strings +4 -0
  26. package/apple/Sources/Astryx/Resources/it.lproj/Localizable.strings +4 -0
  27. package/apple/Sources/Astryx/Resources/ja.lproj/Localizable.strings +4 -0
  28. package/apple/Sources/Astryx/Resources/ko.lproj/Localizable.strings +4 -0
  29. package/apple/Sources/Astryx/Resources/nb.lproj/Localizable.strings +4 -0
  30. package/apple/Sources/Astryx/Resources/nl.lproj/Localizable.strings +4 -0
  31. package/apple/Sources/Astryx/Resources/pl.lproj/Localizable.strings +4 -0
  32. package/apple/Sources/Astryx/Resources/pt-BR.lproj/Localizable.strings +4 -0
  33. package/apple/Sources/Astryx/Resources/pt-PT.lproj/Localizable.strings +4 -0
  34. package/apple/Sources/Astryx/Resources/ro.lproj/Localizable.strings +4 -0
  35. package/apple/Sources/Astryx/Resources/ru.lproj/Localizable.strings +4 -0
  36. package/apple/Sources/Astryx/Resources/sr.lproj/Localizable.strings +4 -0
  37. package/apple/Sources/Astryx/Resources/sv.lproj/Localizable.strings +4 -0
  38. package/apple/Sources/Astryx/Resources/tr.lproj/Localizable.strings +4 -0
  39. package/apple/Sources/Astryx/Resources/uk.lproj/Localizable.strings +4 -0
  40. package/apple/Sources/Astryx/Resources/vi.lproj/Localizable.strings +4 -0
  41. package/apple/Sources/Astryx/Resources/zh-Hans.lproj/Localizable.strings +4 -0
  42. package/apple/Sources/Astryx/Resources/zh-Hant.lproj/Localizable.strings +4 -0
  43. package/apple/Sources/Astryx/Text/Text.swift +102 -0
  44. package/apple/Sources/Astryx/Text/TypeStyle.swift +143 -0
  45. package/apple/Sources/Astryx/TextInput/TextInput.swift +81 -0
  46. package/apple/Sources/Astryx/Theme/AstryxIcons.swift +108 -0
  47. package/apple/Sources/Astryx/Theme/Generated/ButterTheme.swift +429 -0
  48. package/apple/Sources/Astryx/Theme/Generated/ChocolateTheme.swift +429 -0
  49. package/apple/Sources/Astryx/Theme/Generated/DefaultTheme.swift +429 -0
  50. package/apple/Sources/Astryx/Theme/Generated/GothicTheme.swift +429 -0
  51. package/apple/Sources/Astryx/Theme/Generated/MatchaTheme.swift +429 -0
  52. package/apple/Sources/Astryx/Theme/Generated/NeutralTheme.swift +429 -0
  53. package/apple/Sources/Astryx/Theme/Generated/StoneTheme.swift +429 -0
  54. package/apple/Sources/Astryx/Theme/Generated/Tokens.swift +1262 -0
  55. package/apple/Sources/Astryx/Theme/Generated/Y2kTheme.swift +429 -0
  56. package/apple/Sources/Astryx/Theme/Theme.swift +273 -0
  57. package/apple/Sources/AstryxNative/Generated/AstryxBase.swift +1 -1
  58. package/apple/Sources/AstryxNative/Generated/AstryxNode.swift +72 -0
  59. package/apple/Sources/AstryxNative/Generated/AstryxWire.swift +67 -0
  60. package/apple/Sources/AstryxNative/Generated/Badge.swift +89 -0
  61. package/apple/Sources/AstryxNative/Generated/Button.swift +166 -0
  62. package/apple/Sources/AstryxNative/Generated/Card.swift +117 -0
  63. package/apple/Sources/AstryxNative/Generated/Divider.swift +97 -0
  64. package/apple/Sources/AstryxNative/Generated/HStack.swift +308 -0
  65. package/apple/Sources/AstryxNative/Generated/Heading.swift +169 -0
  66. package/apple/Sources/AstryxNative/Generated/Icon.swift +106 -0
  67. package/apple/Sources/AstryxNative/Generated/IconName.swift +47 -0
  68. package/apple/Sources/AstryxNative/Generated/ProgressBar.swift +144 -0
  69. package/apple/Sources/AstryxNative/Generated/Selector.swift +133 -9
  70. package/apple/Sources/AstryxNative/Generated/Switch.swift +202 -0
  71. package/apple/Sources/AstryxNative/Generated/Text.swift +172 -0
  72. package/apple/Sources/AstryxNative/Generated/TextInput.swift +243 -0
  73. package/apple/Sources/AstryxNative/Generated/VStack.swift +308 -0
  74. package/apple/Sources/AstryxNative/Logic/AstryxLogicHost.swift +102 -0
  75. package/apple/Tests/AstryxNativeTests/ControlPropsTests.swift +29 -0
  76. package/apple/Tests/AstryxNativeTests/LogicHostTests.swift +85 -0
  77. package/apple/Tests/AstryxNativeTests/WireTests.swift +53 -0
  78. package/apple/Tests/AstryxTests/BannerTests.swift +47 -0
  79. package/apple/Tests/AstryxTests/ButtonTests.swift +72 -0
  80. package/apple/Tests/AstryxTests/CardTests.swift +19 -0
  81. package/apple/Tests/AstryxTests/EnvironmentTests.swift +87 -0
  82. package/apple/Tests/AstryxTests/HeadingTests.swift +68 -0
  83. package/apple/Tests/AstryxTests/IconTests.swift +188 -0
  84. package/apple/Tests/AstryxTests/NamesTests.swift +28 -0
  85. package/apple/Tests/AstryxTests/Pixels.swift +68 -0
  86. package/apple/Tests/AstryxTests/Resources/astryx-test-icon.png +0 -0
  87. package/apple/Tests/AstryxTests/StringsTests.swift +23 -0
  88. package/apple/Tests/AstryxTests/TextInputTests.swift +29 -0
  89. package/apple/Tests/AstryxTests/TextTests.swift +109 -0
  90. package/apple/Tests/AstryxTests/ThemeTests.swift +45 -0
  91. package/apple/Tests/AstryxTests/TwoThemeTests.swift +65 -0
  92. package/dist/components/Badge.d.ts +10 -0
  93. package/dist/components/Badge.d.ts.map +1 -0
  94. package/dist/components/Badge.js +7 -0
  95. package/dist/components/Button.d.ts +10 -0
  96. package/dist/components/Button.d.ts.map +1 -0
  97. package/dist/components/Button.js +16 -0
  98. package/dist/components/Card.d.ts +10 -0
  99. package/dist/components/Card.d.ts.map +1 -0
  100. package/dist/components/Card.js +7 -0
  101. package/dist/components/Divider.d.ts +10 -0
  102. package/dist/components/Divider.d.ts.map +1 -0
  103. package/dist/components/Divider.js +6 -0
  104. package/dist/components/HStack.d.ts +10 -0
  105. package/dist/components/HStack.d.ts.map +1 -0
  106. package/dist/components/HStack.js +9 -0
  107. package/dist/components/Heading.d.ts +10 -0
  108. package/dist/components/Heading.d.ts.map +1 -0
  109. package/dist/components/Heading.js +14 -0
  110. package/dist/components/Icon.d.ts +10 -0
  111. package/dist/components/Icon.d.ts.map +1 -0
  112. package/dist/components/Icon.js +7 -0
  113. package/dist/components/ProgressBar.d.ts +10 -0
  114. package/dist/components/ProgressBar.d.ts.map +1 -0
  115. package/dist/components/ProgressBar.js +5 -0
  116. package/dist/components/Switch.d.ts +10 -0
  117. package/dist/components/Switch.d.ts.map +1 -0
  118. package/dist/components/Switch.js +6 -0
  119. package/dist/components/Text.d.ts +10 -0
  120. package/dist/components/Text.d.ts.map +1 -0
  121. package/dist/components/Text.js +13 -0
  122. package/dist/components/TextInput.d.ts +10 -0
  123. package/dist/components/TextInput.d.ts.map +1 -0
  124. package/dist/components/TextInput.js +9 -0
  125. package/dist/components/VStack.d.ts +10 -0
  126. package/dist/components/VStack.d.ts.map +1 -0
  127. package/dist/components/VStack.js +9 -0
  128. package/dist/components/index.d.ts.map +1 -1
  129. package/dist/components/index.js +27 -1
  130. package/dist/components/stackShared.d.ts +9 -0
  131. package/dist/components/stackShared.d.ts.map +1 -0
  132. package/dist/components/stackShared.js +4 -0
  133. package/dist/contract/fromComponentDoc.d.ts +53 -5
  134. package/dist/contract/fromComponentDoc.d.ts.map +1 -1
  135. package/dist/contract/fromComponentDoc.js +63 -4
  136. package/dist/contract/generateSwift.d.ts.map +1 -1
  137. package/dist/contract/generateSwift.js +226 -10
  138. package/dist/contract/generateTypeScript.d.ts.map +1 -1
  139. package/dist/contract/generateTypeScript.js +78 -9
  140. package/dist/contract/index.d.ts +7 -3
  141. package/dist/contract/index.d.ts.map +1 -1
  142. package/dist/contract/index.js +2 -1
  143. package/dist/contract/parseDocType.d.ts +5 -2
  144. package/dist/contract/parseDocType.d.ts.map +1 -1
  145. package/dist/contract/parseDocType.js +34 -5
  146. package/dist/contract/rules.d.ts +5 -2
  147. package/dist/contract/rules.d.ts.map +1 -1
  148. package/dist/contract/rules.js +8 -4
  149. package/dist/contract/types.d.ts +20 -0
  150. package/dist/contract/types.d.ts.map +1 -1
  151. package/dist/logic/connectBridge.d.ts +29 -0
  152. package/dist/logic/connectBridge.d.ts.map +1 -0
  153. package/dist/logic/connectBridge.js +32 -0
  154. package/dist/logic/createLogicCore.d.ts +44 -0
  155. package/dist/logic/createLogicCore.d.ts.map +1 -0
  156. package/dist/logic/createLogicCore.js +40 -0
  157. package/dist/logic/index.d.ts +12 -0
  158. package/dist/logic/index.d.ts.map +1 -0
  159. package/dist/logic/index.js +10 -0
  160. package/dist/types/android.d.ts +146 -2
  161. package/dist/types/core.d.ts +160 -0
  162. package/dist/types/ios.d.ts +146 -2
  163. package/dist/types/macos.d.ts +146 -2
  164. package/dist/types/shared.d.ts +146 -2
  165. package/dist/types/windows.d.ts +146 -2
  166. package/package.json +12 -4
  167. package/src/components/Badge.ts +18 -0
  168. package/src/components/Button.ts +27 -0
  169. package/src/components/Card.ts +18 -0
  170. package/src/components/Divider.ts +17 -0
  171. package/src/components/HStack.ts +20 -0
  172. package/src/components/Heading.ts +25 -0
  173. package/src/components/Icon.ts +18 -0
  174. package/src/components/ProgressBar.ts +16 -0
  175. package/src/components/Switch.ts +17 -0
  176. package/src/components/Text.ts +24 -0
  177. package/src/components/TextInput.ts +20 -0
  178. package/src/components/VStack.ts +20 -0
  179. package/src/components/index.ts +27 -1
  180. package/src/components/stackShared.ts +14 -0
  181. package/src/contract/contracts.test.ts +95 -0
  182. package/src/contract/fromComponentDoc.test.ts +126 -0
  183. package/src/contract/fromComponentDoc.ts +136 -9
  184. package/src/contract/generateSwift.ts +261 -10
  185. package/src/contract/generateTypeScript.ts +92 -10
  186. package/src/contract/index.ts +13 -4
  187. package/src/contract/parseDocType.test.ts +45 -0
  188. package/src/contract/parseDocType.ts +41 -6
  189. package/src/contract/rules.ts +11 -4
  190. package/src/contract/types.ts +15 -0
  191. package/src/icons/swiftIcons.test.ts +22 -0
  192. package/src/logic/connectBridge.ts +72 -0
  193. package/src/logic/createLogicCore.ts +97 -0
  194. package/src/logic/index.ts +19 -0
  195. package/src/logic/logic.test.ts +160 -0
  196. package/src/theme/swiftIOSStrings.test.ts +49 -0
  197. package/src/theme/swiftIOSTheme.test.ts +256 -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. iOS and macOS props sit behind `#if os(...)`. |
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
- Only `Selector` has native additions so far. React Native and Android bindings
24
- are not built yet; see [the experiment matrix](#experiment-matrix).
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
- - This package's source never imports core. It reads core's documentation
52
- files as data at build time.
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
 
@@ -1,18 +1,41 @@
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). The contract types under
5
- // Sources/AstryxNative/Generated are generated by `pnpm generate:apple`; do not
6
- // edit them by hand.
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: [.library(name: "AstryxNative", targets: ["AstryxNative"])],
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(
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
+ ),
17
40
  ]
18
41
  )
@@ -0,0 +1,205 @@
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. `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
61
+ generated `ButtonProps` does not carry yet.
62
+ - `TextInput` takes a `Binding` where core has `value` and `onChange`.
63
+ - `Card` takes `padding` as a whole spacing step, so it cannot spell
64
+ core's `0.5` and `1.5` steps.
65
+ - **`Templates/`** holds one folder per page template, named after the
66
+ `astryx template` id. Templates are clone-only, as on the web: you copy a
67
+ template's folder into your app and own it from then on. They are a
68
+ separate package with no products, so nothing imports them, and
69
+ `@astryxdesign/native` leaves them out of its npm package. A template is
70
+ built only from `Astryx` views and SwiftUI layout, and matches the web
71
+ template's content and states. Rules a template needs, such as validation,
72
+ live in a plain value type beside the view so tests can check them without a
73
+ screen.
74
+
75
+ ## Use it
76
+
77
+ Depend on the `Astryx` product. To start from a template, copy its folder,
78
+ such as `Templates/Login/`, into your app's sources; it then belongs to your
79
+ app.
80
+
81
+ ```swift
82
+ import Astryx
83
+
84
+ @main
85
+ struct DemoApp: App {
86
+ var body: some Scene {
87
+ WindowGroup {
88
+ LoginTemplate().astryxTheme() // copied from Templates/Login/
89
+ }
90
+ }
91
+ }
92
+ ```
93
+
94
+ `astryxTheme(_:)` paints a subtree with a theme; without it, components use
95
+ core's default theme. Either way they follow Dark Mode and the person's text
96
+ size.
97
+
98
+ ## Theme tokens
99
+
100
+ Components read every color, size, and font from the `astryx` environment
101
+ value, never from a constant of their own. `astryx.tokens` holds the theme's
102
+ tokens in the current mode as typed members (`tokens.color.textPrimary`,
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
107
+ tokens by core's name (`astryx.color("color-accent")`); moving them onto the
108
+ typed members comes next.
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
+
119
+ Font families come from the theme's `font-family-*` stacks: the first family
120
+ the device has wins, and a system family such as `-apple-system` or
121
+ `sans-serif` is the system font. Icons use core's shared icon names
122
+ (`success`, `warning`, …), drawn as an SF Symbol by default; a theme replaces
123
+ one with `Theme(…, icons: ["success": "checkmark.seal.fill"])`.
124
+
125
+ `Sources/Astryx/Theme/Generated/` is written by the Swift theme generator
126
+ (`../specs/swift/library/theme-generator.md`) from core's own resolver, light
127
+ and dark:
128
+
129
+ - `Tokens.swift`: the typed token surface, from core's token names.
130
+ - One `<Name>Theme.swift` per theme, declaring `Theme.<name>`: core's
131
+ defaults (`.default`) and every published theme package (`.butter`,
132
+ `.chocolate`, `.gothic`, `.matcha`, `.neutral`, `.stone`, `.y2k`).
133
+
134
+ ```swift
135
+ LoginTemplate().astryxTheme(.neutral)
136
+ ```
137
+
138
+ Regenerate after a theme or core token change:
139
+
140
+ ```sh
141
+ pnpm -F @astryxdesign/native generate:swift-ios
142
+ ```
143
+
144
+ A product generates its own theme the same way, from its theme's module and
145
+ export:
146
+
147
+ ```sh
148
+ node packages/native/scripts/swift-ios-theme.mjs --theme ./src/acmeTheme.ts#acmeTheme --out ./Sources/Theme
149
+ ```
150
+
151
+ A theme's local tokens are not generated yet; the generator names each one it
152
+ leaves out.
153
+
154
+ **Fonts.** A theme's font families are carried as data. Astryx never bundles
155
+ a font: a component walks the theme's family list in order and uses the first
156
+ family the app has registered; a system family in the list (such as
157
+ `-apple-system` or `sans-serif`) is the system font, and a list with no
158
+ registered family falls back to the system font. To draw a theme's brand face, bundle and register it
159
+ in the app. Faces the published themes name (body, heading, code):
160
+
161
+ | Theme | Faces to bundle |
162
+ | ------------ | ------------------------------------------ |
163
+ | `.default` | none (system fonts) |
164
+ | `.butter` | Outfit, JetBrains Mono |
165
+ | `.chocolate` | Albert Sans, Fraunces, JetBrains Mono |
166
+ | `.gothic` | Fustat, JetBrains Mono |
167
+ | `.matcha` | DM Sans, Playwrite US Trad, JetBrains Mono |
168
+ | `.neutral` | Figtree |
169
+ | `.stone` | Figtree, Montserrat, JetBrains Mono |
170
+ | `.y2k` | Poppins, JetBrains Mono |
171
+
172
+ The same command writes
173
+ `Sources/Astryx/Resources/<locale>.lproj/Localizable.strings` from core's
174
+ locale catalogs, so words the components speak to assistive technology, such
175
+ as a loading button's "Loading", follow the person's language. Tests in
176
+ `@astryxdesign/native` fail when either committed output drifts from core.
177
+
178
+ ## Working on it
179
+
180
+ On a Mac with Xcode or the Swift command-line tools:
181
+
182
+ ```sh
183
+ cd packages/native/apple
184
+ swift test # both products
185
+ cd Templates && swift test # templates, against the Astryx product
186
+ ```
187
+
188
+ Both packages also declare macOS 14 so they build and test on a Mac without a
189
+ simulator; iOS 17 is the target. Open `Templates/Package.swift` in Xcode to
190
+ run `LoginTemplate` in an iOS simulator through its preview.
191
+
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
195
+ `Tests/AstryxTests/`. To add a template, create
196
+ `Templates/<Name>/<Name>Template.swift` from the output of
197
+ `astryx template <id>`, with tests under `Templates/Tests/`.
198
+
199
+ ## Not here yet
200
+
201
+ - The views do not use the generated contract types in `AstryxNative` yet.
202
+ Moving them onto those types is how a core prop change reaches Swift.
203
+ - Pixel and behavior parity with the web components has not been measured.
204
+ - Increase Contrast, theme-local tokens, and platform theme entries
205
+ (`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 icon = astryx.icon(status.iconName) {
53
+ // Sized with the title so it grows with Dynamic Type.
54
+ icon.sized(astryx.fontSize("text-body-size", 14))
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,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
+ }