@astryxdesign/native 0.7.4-canary.720bb10 → 0.7.5-canary.04434e7

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 (119) hide show
  1. package/README.md +116 -4
  2. package/apple/Sources/AstryxNative/Generated/AstryxBase.swift +1 -1
  3. package/apple/Sources/AstryxNative/Generated/AstryxNode.swift +69 -0
  4. package/apple/Sources/AstryxNative/Generated/AstryxWire.swift +67 -0
  5. package/apple/Sources/AstryxNative/Generated/Badge.swift +89 -0
  6. package/apple/Sources/AstryxNative/Generated/Button.swift +154 -0
  7. package/apple/Sources/AstryxNative/Generated/Card.swift +117 -0
  8. package/apple/Sources/AstryxNative/Generated/Divider.swift +97 -0
  9. package/apple/Sources/AstryxNative/Generated/HStack.swift +308 -0
  10. package/apple/Sources/AstryxNative/Generated/Heading.swift +169 -0
  11. package/apple/Sources/AstryxNative/Generated/ProgressBar.swift +144 -0
  12. package/apple/Sources/AstryxNative/Generated/Selector.swift +133 -9
  13. package/apple/Sources/AstryxNative/Generated/Switch.swift +202 -0
  14. package/apple/Sources/AstryxNative/Generated/Text.swift +172 -0
  15. package/apple/Sources/AstryxNative/Generated/TextInput.swift +243 -0
  16. package/apple/Sources/AstryxNative/Generated/VStack.swift +308 -0
  17. package/apple/Sources/AstryxNative/Logic/AstryxLogicHost.swift +102 -0
  18. package/apple/Tests/AstryxNativeTests/ControlPropsTests.swift +29 -0
  19. package/apple/Tests/AstryxNativeTests/LogicHostTests.swift +85 -0
  20. package/apple/Tests/AstryxNativeTests/WireTests.swift +53 -0
  21. package/dist/components/Badge.d.ts +10 -0
  22. package/dist/components/Badge.d.ts.map +1 -0
  23. package/dist/components/Badge.js +7 -0
  24. package/dist/components/Button.d.ts +10 -0
  25. package/dist/components/Button.d.ts.map +1 -0
  26. package/dist/components/Button.js +15 -0
  27. package/dist/components/Card.d.ts +10 -0
  28. package/dist/components/Card.d.ts.map +1 -0
  29. package/dist/components/Card.js +7 -0
  30. package/dist/components/Divider.d.ts +10 -0
  31. package/dist/components/Divider.d.ts.map +1 -0
  32. package/dist/components/Divider.js +6 -0
  33. package/dist/components/HStack.d.ts +10 -0
  34. package/dist/components/HStack.d.ts.map +1 -0
  35. package/dist/components/HStack.js +9 -0
  36. package/dist/components/Heading.d.ts +10 -0
  37. package/dist/components/Heading.d.ts.map +1 -0
  38. package/dist/components/Heading.js +14 -0
  39. package/dist/components/ProgressBar.d.ts +10 -0
  40. package/dist/components/ProgressBar.d.ts.map +1 -0
  41. package/dist/components/ProgressBar.js +5 -0
  42. package/dist/components/Switch.d.ts +10 -0
  43. package/dist/components/Switch.d.ts.map +1 -0
  44. package/dist/components/Switch.js +6 -0
  45. package/dist/components/Text.d.ts +10 -0
  46. package/dist/components/Text.d.ts.map +1 -0
  47. package/dist/components/Text.js +13 -0
  48. package/dist/components/TextInput.d.ts +10 -0
  49. package/dist/components/TextInput.d.ts.map +1 -0
  50. package/dist/components/TextInput.js +9 -0
  51. package/dist/components/VStack.d.ts +10 -0
  52. package/dist/components/VStack.d.ts.map +1 -0
  53. package/dist/components/VStack.js +9 -0
  54. package/dist/components/index.d.ts.map +1 -1
  55. package/dist/components/index.js +25 -1
  56. package/dist/components/stackShared.d.ts +9 -0
  57. package/dist/components/stackShared.d.ts.map +1 -0
  58. package/dist/components/stackShared.js +4 -0
  59. package/dist/contract/fromComponentDoc.d.ts +44 -5
  60. package/dist/contract/fromComponentDoc.d.ts.map +1 -1
  61. package/dist/contract/fromComponentDoc.js +47 -4
  62. package/dist/contract/generateSwift.d.ts.map +1 -1
  63. package/dist/contract/generateSwift.js +193 -10
  64. package/dist/contract/generateTypeScript.d.ts.map +1 -1
  65. package/dist/contract/generateTypeScript.js +69 -9
  66. package/dist/contract/index.d.ts +7 -3
  67. package/dist/contract/index.d.ts.map +1 -1
  68. package/dist/contract/index.js +2 -1
  69. package/dist/contract/parseDocType.d.ts +5 -2
  70. package/dist/contract/parseDocType.d.ts.map +1 -1
  71. package/dist/contract/parseDocType.js +34 -5
  72. package/dist/contract/rules.d.ts +5 -2
  73. package/dist/contract/rules.d.ts.map +1 -1
  74. package/dist/contract/rules.js +8 -4
  75. package/dist/contract/types.d.ts +11 -0
  76. package/dist/contract/types.d.ts.map +1 -1
  77. package/dist/logic/connectBridge.d.ts +29 -0
  78. package/dist/logic/connectBridge.d.ts.map +1 -0
  79. package/dist/logic/connectBridge.js +32 -0
  80. package/dist/logic/createLogicCore.d.ts +44 -0
  81. package/dist/logic/createLogicCore.d.ts.map +1 -0
  82. package/dist/logic/createLogicCore.js +40 -0
  83. package/dist/logic/index.d.ts +12 -0
  84. package/dist/logic/index.d.ts.map +1 -0
  85. package/dist/logic/index.js +10 -0
  86. package/dist/types/android.d.ts +136 -2
  87. package/dist/types/core.d.ts +150 -0
  88. package/dist/types/ios.d.ts +136 -2
  89. package/dist/types/macos.d.ts +136 -2
  90. package/dist/types/shared.d.ts +136 -2
  91. package/dist/types/windows.d.ts +136 -2
  92. package/package.json +10 -4
  93. package/src/components/Badge.ts +18 -0
  94. package/src/components/Button.ts +26 -0
  95. package/src/components/Card.ts +18 -0
  96. package/src/components/Divider.ts +17 -0
  97. package/src/components/HStack.ts +20 -0
  98. package/src/components/Heading.ts +25 -0
  99. package/src/components/ProgressBar.ts +16 -0
  100. package/src/components/Switch.ts +17 -0
  101. package/src/components/Text.ts +24 -0
  102. package/src/components/TextInput.ts +20 -0
  103. package/src/components/VStack.ts +20 -0
  104. package/src/components/index.ts +25 -1
  105. package/src/components/stackShared.ts +14 -0
  106. package/src/contract/contracts.test.ts +58 -0
  107. package/src/contract/fromComponentDoc.test.ts +97 -0
  108. package/src/contract/fromComponentDoc.ts +106 -9
  109. package/src/contract/generateSwift.ts +223 -10
  110. package/src/contract/generateTypeScript.ts +83 -10
  111. package/src/contract/index.ts +13 -4
  112. package/src/contract/parseDocType.test.ts +45 -0
  113. package/src/contract/parseDocType.ts +41 -6
  114. package/src/contract/rules.ts +11 -4
  115. package/src/contract/types.ts +6 -0
  116. package/src/logic/connectBridge.ts +72 -0
  117. package/src/logic/createLogicCore.ts +97 -0
  118. package/src/logic/index.ts +19 -0
  119. package/src/logic/logic.test.ts +160 -0
@@ -0,0 +1,18 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file components/Badge.ts
5
+ * @input None
6
+ * @output What @astryxdesign/native adds to the core Badge contract
7
+ * @position Native additions read by fromComponentDoc together with core's
8
+ * Badge documentation
9
+ */
10
+
11
+ import type {NativeAdditions} from '../contract/fromComponentDoc.js';
12
+
13
+ export const badge: NativeAdditions = {
14
+ name: 'Badge',
15
+ baseTarget: 'root',
16
+ // `icon` stays web-only until the core has an icon slot.
17
+ slots: {label: 'text'},
18
+ };
@@ -0,0 +1,26 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file components/Button.ts
5
+ * @input None
6
+ * @output What @astryxdesign/native adds to the core Button contract
7
+ * @position Native additions read by fromComponentDoc together with core's
8
+ * Button documentation
9
+ */
10
+
11
+ import type {NativeAdditions} from '../contract/fromComponentDoc.js';
12
+
13
+ export const button: NativeAdditions = {
14
+ name: 'Button',
15
+ baseTarget: 'root',
16
+ callbacks: ['onClick'],
17
+ sameMeaning: {onClick: 'a press, as in Compose'},
18
+ webOnly: {
19
+ type: 'HTML form submission type',
20
+ name: 'HTML form field name',
21
+ form: 'HTML form owner',
22
+ target: 'HTML link target',
23
+ rel: 'HTML link relationship',
24
+ isIconOnly: 'needs an icon, which has no core slot yet',
25
+ },
26
+ };
@@ -0,0 +1,18 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file components/Card.ts
5
+ * @input None
6
+ * @output What @astryxdesign/native adds to the core Card contract
7
+ * @position Native additions read by fromComponentDoc together with core's
8
+ * Card documentation
9
+ */
10
+
11
+ import type {NativeAdditions} from '../contract/fromComponentDoc.js';
12
+
13
+ export const card: NativeAdditions = {
14
+ name: 'Card',
15
+ baseTarget: 'container',
16
+ slots: {children: 'content'},
17
+ sameMeaning: {children: "the component's content, as on the web"},
18
+ };
@@ -0,0 +1,17 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file components/Divider.ts
5
+ * @input None
6
+ * @output What @astryxdesign/native adds to the core Divider contract
7
+ * @position Native additions read by fromComponentDoc together with core's
8
+ * Divider documentation
9
+ */
10
+
11
+ import type {NativeAdditions} from '../contract/fromComponentDoc.js';
12
+
13
+ export const divider: NativeAdditions = {
14
+ name: 'Divider',
15
+ baseTarget: 'root',
16
+ slots: {label: 'text'},
17
+ };
@@ -0,0 +1,20 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file components/HStack.ts
5
+ * @input None
6
+ * @output What @astryxdesign/native adds to the core HStack contract
7
+ * @position Native additions read by fromComponentDoc together with core's
8
+ * Stack documentation, which documents HStack
9
+ */
10
+
11
+ import type {NativeAdditions} from '../contract/fromComponentDoc.js';
12
+ import {STACK_SLOTS} from './stackShared.js';
13
+
14
+ export const hStack: NativeAdditions = {
15
+ name: 'HStack',
16
+ baseTarget: 'container',
17
+ doc: {file: 'Stack', component: 'HStack'},
18
+ slots: STACK_SLOTS,
19
+ sameMeaning: {children: "the component's content, as on the web"},
20
+ };
@@ -0,0 +1,25 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file components/Heading.ts
5
+ * @input None
6
+ * @output What @astryxdesign/native adds to the core Heading contract
7
+ * @position Native additions read by fromComponentDoc together with core's
8
+ * Heading documentation
9
+ */
10
+
11
+ import type {NativeAdditions} from '../contract/fromComponentDoc.js';
12
+
13
+ export const heading: NativeAdditions = {
14
+ name: 'Heading',
15
+ baseTarget: 'root',
16
+ doc: {file: 'Text/Heading'},
17
+ slots: {children: 'text'},
18
+ sameMeaning: {children: "the component's content, as on the web"},
19
+ webOnly: {
20
+ display: 'CSS display; native layout places text by its container',
21
+ wordBreak: 'CSS line breaking; native text breaks by platform rules',
22
+ textWrap: 'CSS text wrapping; native text wraps by platform rules',
23
+ hasCapsize: 'web text-box trimming of CSS line height',
24
+ },
25
+ };
@@ -0,0 +1,16 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file components/ProgressBar.ts
5
+ * @input None
6
+ * @output What @astryxdesign/native adds to the core ProgressBar contract
7
+ * @position Native additions read by fromComponentDoc together with core's
8
+ * ProgressBar documentation
9
+ */
10
+
11
+ import type {NativeAdditions} from '../contract/fromComponentDoc.js';
12
+
13
+ export const progressBar: NativeAdditions = {
14
+ name: 'ProgressBar',
15
+ baseTarget: 'root',
16
+ };
@@ -0,0 +1,17 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file components/Switch.ts
5
+ * @input None
6
+ * @output What @astryxdesign/native adds to the core Switch contract
7
+ * @position Native additions read by fromComponentDoc together with core's
8
+ * Switch documentation
9
+ */
10
+
11
+ import type {NativeAdditions} from '../contract/fromComponentDoc.js';
12
+
13
+ export const switchControl: NativeAdditions = {
14
+ name: 'Switch',
15
+ // The web Switch forwards base props to its root wrapper.
16
+ baseTarget: 'root',
17
+ };
@@ -0,0 +1,24 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file components/Text.ts
5
+ * @input None
6
+ * @output What @astryxdesign/native adds to the core Text contract
7
+ * @position Native additions read by fromComponentDoc together with core's
8
+ * Text documentation
9
+ */
10
+
11
+ import type {NativeAdditions} from '../contract/fromComponentDoc.js';
12
+
13
+ export const text: NativeAdditions = {
14
+ name: 'Text',
15
+ baseTarget: 'root',
16
+ slots: {children: 'text'},
17
+ sameMeaning: {children: "the component's content, as on the web"},
18
+ webOnly: {
19
+ display: 'CSS display; native layout places text by its container',
20
+ wordBreak: 'CSS line breaking; native text breaks by platform rules',
21
+ textWrap: 'CSS text wrapping; native text wraps by platform rules',
22
+ hasCapsize: 'web text-box trimming of CSS line height',
23
+ },
24
+ };
@@ -0,0 +1,20 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file components/TextInput.ts
5
+ * @input None
6
+ * @output What @astryxdesign/native adds to the core TextInput contract
7
+ * @position Native additions read by fromComponentDoc together with core's
8
+ * TextInput documentation
9
+ */
10
+
11
+ import type {NativeAdditions} from '../contract/fromComponentDoc.js';
12
+
13
+ export const textInput: NativeAdditions = {
14
+ name: 'TextInput',
15
+ // family:input-fields routes base props to the primary semantic control.
16
+ baseTarget: 'control',
17
+ webOnly: {
18
+ autoComplete: 'HTML autocomplete attribute',
19
+ },
20
+ };
@@ -0,0 +1,20 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file components/VStack.ts
5
+ * @input None
6
+ * @output What @astryxdesign/native adds to the core VStack contract
7
+ * @position Native additions read by fromComponentDoc together with core's
8
+ * Stack documentation, which documents VStack
9
+ */
10
+
11
+ import type {NativeAdditions} from '../contract/fromComponentDoc.js';
12
+ import {STACK_SLOTS} from './stackShared.js';
13
+
14
+ export const vStack: NativeAdditions = {
15
+ name: 'VStack',
16
+ baseTarget: 'container',
17
+ doc: {file: 'Stack', component: 'VStack'},
18
+ slots: STACK_SLOTS,
19
+ sameMeaning: {children: "the component's content, as on the web"},
20
+ };
@@ -9,6 +9,30 @@
9
9
  */
10
10
 
11
11
  import type {NativeAdditions} from '../contract/fromComponentDoc.js';
12
+ import {badge} from './Badge.js';
13
+ import {button} from './Button.js';
14
+ import {card} from './Card.js';
15
+ import {divider} from './Divider.js';
16
+ import {heading} from './Heading.js';
17
+ import {hStack} from './HStack.js';
18
+ import {progressBar} from './ProgressBar.js';
12
19
  import {selector} from './Selector.js';
20
+ import {switchControl} from './Switch.js';
21
+ import {text} from './Text.js';
22
+ import {textInput} from './TextInput.js';
23
+ import {vStack} from './VStack.js';
13
24
 
14
- export const NATIVE_COMPONENTS: readonly NativeAdditions[] = [selector];
25
+ export const NATIVE_COMPONENTS: readonly NativeAdditions[] = [
26
+ badge,
27
+ button,
28
+ card,
29
+ divider,
30
+ heading,
31
+ hStack,
32
+ progressBar,
33
+ selector,
34
+ switchControl,
35
+ text,
36
+ textInput,
37
+ vStack,
38
+ ];
@@ -0,0 +1,14 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file components/stackShared.ts
5
+ * @input None
6
+ * @output The slot declarations HStack and VStack share
7
+ * @position Read by components/HStack.ts and components/VStack.ts
8
+ */
9
+
10
+ import type {SlotKind} from '../contract/fromComponentDoc.js';
11
+
12
+ export const STACK_SLOTS: Readonly<Record<string, SlotKind>> = {
13
+ children: 'content',
14
+ };
@@ -51,6 +51,40 @@ describe('native contracts', () => {
51
51
  expect(selector!.core).not.toHaveProperty('controlXstyle');
52
52
  });
53
53
 
54
+ it('keeps value callbacks, typed text slots, and number sets in the core', async () => {
55
+ const {contracts} = await buildContracts(loadCoreDoc);
56
+ const byName = new Map(
57
+ contracts.map(contract => [contract.name, contract]),
58
+ );
59
+ expect(byName.get('Switch')!.core.onChange?.type).toEqual({
60
+ kind: 'fn',
61
+ args: [{kind: 'boolean'}],
62
+ });
63
+ expect(byName.get('TextInput')!.core.onChange?.type).toEqual({
64
+ kind: 'fn',
65
+ args: [{kind: 'string'}],
66
+ });
67
+ expect(byName.get('Button')!.core.onClick?.type).toEqual({
68
+ kind: 'fn',
69
+ args: [],
70
+ });
71
+ expect(byName.get('Heading')!.core.children?.type).toEqual({
72
+ kind: 'string',
73
+ });
74
+ expect(byName.get('Heading')!.core.level?.type).toEqual({
75
+ kind: 'numberEnum',
76
+ values: [1, 2, 3, 4, 5, 6],
77
+ });
78
+ expect(byName.get('Text')!.core).not.toHaveProperty('id');
79
+ expect(byName.get('Badge')!.core.label).toMatchObject({
80
+ type: {kind: 'string'},
81
+ optional: false,
82
+ });
83
+ expect(byName.get('Badge')!.webOnly.map(prop => prop.name)).toContain(
84
+ 'icon',
85
+ );
86
+ });
87
+
54
88
  it('reports naming and portability errors', () => {
55
89
  const bad: ComponentContract = {
56
90
  name: 'Bad',
@@ -146,5 +180,29 @@ describe('native contracts', () => {
146
180
  expect(errorsFor(null, usage('pullDown')).join('\n')).toContain(
147
181
  'menuStyle',
148
182
  );
183
+
184
+ const wire = (node: string, action: string) =>
185
+ "import type {AstryxAction, AstryxNode} from '@astryxdesign/native/components';\n" +
186
+ `export const node: AstryxNode = ${node};\n` +
187
+ `export const action: AstryxAction = ${action};\n`;
188
+ const tree =
189
+ "{component: 'VStack', key: 'root', props: {gap: 2, children: [" +
190
+ "{component: 'Switch', key: 'wifi', props: {label: 'Wi-Fi', value: true}, events: ['onChange']}," +
191
+ "{component: 'Selector', key: 'ship', props: {label: 'Shipping', options: [], menuStyle: 'pullDown'}}]}}";
192
+ const toggled =
193
+ "{key: 'wifi', component: 'Switch', event: 'onChange', args: [false]}";
194
+ expect(errorsFor('ios', wire(tree, toggled))).toEqual([]);
195
+ expect(errorsFor('android', wire(tree, toggled)).join('\n')).toContain(
196
+ 'pullDown',
197
+ );
198
+ expect(
199
+ errorsFor(
200
+ 'ios',
201
+ wire(
202
+ tree,
203
+ "{key: 'wifi', component: 'Switch', event: 'onChange', args: ['off']}",
204
+ ),
205
+ ).length,
206
+ ).toBeGreaterThan(0);
149
207
  });
150
208
  });
@@ -0,0 +1,97 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {describe, expect, it} from 'vitest';
4
+ import {fromComponentDoc, type ComponentDocLike} from './fromComponentDoc.js';
5
+ import {generateSwift} from './generateSwift.js';
6
+
7
+ const doc: ComponentDocLike = {
8
+ name: 'Toggle',
9
+ props: [
10
+ {name: 'label', type: 'ReactNode', required: true},
11
+ {name: 'icon', type: 'ReactNode'},
12
+ {name: 'level', type: '1 | 2 | 3'},
13
+ {
14
+ name: 'onChange',
15
+ type: '(on: boolean, e: ChangeEvent<HTMLInputElement>) => void',
16
+ },
17
+ {name: 'onClick', type: '(e: MouseEvent) => void'},
18
+ {name: 'onFocus', type: '(e: FocusEvent) => void'},
19
+ {name: 'onPick', type: '(e: MouseEvent, value: string) => void'},
20
+ ],
21
+ };
22
+
23
+ describe('fromComponentDoc', () => {
24
+ const contract = fromComponentDoc(doc, {
25
+ name: 'Toggle',
26
+ baseTarget: 'control',
27
+ slots: {label: 'text'},
28
+ callbacks: ['onClick', 'onPick'],
29
+ sameMeaning: {onClick: 'a press in Compose too'},
30
+ });
31
+
32
+ it('gives declared content a typed text slot and leaves the rest on the web', () => {
33
+ expect(contract.core.label).toEqual({
34
+ type: {kind: 'string'},
35
+ optional: false,
36
+ });
37
+ expect(contract.webOnly.map(prop => prop.name)).toContain('icon');
38
+ });
39
+
40
+ it('keeps a value callback and a declared press, and leaves other event callbacks on the web', () => {
41
+ expect(contract.core.onChange?.type).toEqual({
42
+ kind: 'fn',
43
+ args: [{kind: 'boolean'}],
44
+ });
45
+ expect(contract.core.onClick?.type).toEqual({kind: 'fn', args: []});
46
+ expect(contract.webOnly).toContainEqual({
47
+ name: 'onFocus',
48
+ reason: 'callback argument: DOM event',
49
+ });
50
+ // `callbacks` never drops values: an event before a value stays on the web.
51
+ expect(contract.core).not.toHaveProperty('onPick');
52
+ expect(contract.webOnly.map(prop => prop.name)).toContain('onPick');
53
+ });
54
+
55
+ it('refuses a toolkit name unless the component declares the same meaning', () => {
56
+ const plain = fromComponentDoc(doc, {
57
+ name: 'Toggle',
58
+ baseTarget: 'control',
59
+ callbacks: ['onClick'],
60
+ });
61
+ expect(plain.core).not.toHaveProperty('onClick');
62
+ expect(
63
+ plain.webOnly.find(prop => prop.name === 'onClick')?.reason,
64
+ ).toContain('compose');
65
+ });
66
+
67
+ it('writes a closed set of numbers as a Swift enum with raw values', () => {
68
+ const swift = generateSwift([contract])['Toggle.swift'];
69
+ expect(swift).toContain(
70
+ 'public enum ToggleLevel: Int, CaseIterable, Codable, Sendable {',
71
+ );
72
+ expect(swift).toContain(' case v1 = 1');
73
+ });
74
+
75
+ it('reads one component of a file that documents several', () => {
76
+ const stack: ComponentDocLike = {
77
+ name: 'Stack',
78
+ components: [
79
+ {name: 'Stack', props: [{name: 'gap', type: '0 | 0.5 | 1'}]},
80
+ {name: 'VStack', props: [{name: 'wrap', type: 'boolean'}]},
81
+ ],
82
+ };
83
+ const vstack = fromComponentDoc(stack, {
84
+ name: 'VStack',
85
+ baseTarget: 'container',
86
+ doc: {file: 'Stack', component: 'VStack'},
87
+ });
88
+ expect(Object.keys(vstack.core)).toEqual(['wrap']);
89
+ expect(() =>
90
+ fromComponentDoc(stack, {
91
+ name: 'Missing',
92
+ baseTarget: 'root',
93
+ doc: {file: 'Stack', component: 'Missing'},
94
+ }),
95
+ ).toThrow('does not document "Missing"');
96
+ });
97
+ });
@@ -17,25 +17,63 @@ import type {
17
17
  ComponentContract,
18
18
  PropMap,
19
19
  PropSpec,
20
+ PropType,
20
21
  WebOnlyProp,
21
22
  } from './types.js';
22
23
  import type {NativePlatform} from '../platforms.js';
23
24
 
24
- /** The subset of a ComponentDoc this reader needs. */
25
+ interface DocProp {
26
+ readonly name: string;
27
+ readonly type?: unknown;
28
+ readonly required?: boolean;
29
+ readonly description?: unknown;
30
+ }
31
+
32
+ /**
33
+ * The subset of a ComponentDoc this reader needs. A file that documents
34
+ * several components, such as Stack with HStack and VStack, lists them in
35
+ * `components`.
36
+ */
25
37
  export interface ComponentDocLike {
26
38
  readonly name: string;
27
- readonly props?: ReadonlyArray<{
39
+ readonly props?: ReadonlyArray<DocProp>;
40
+ readonly components?: ReadonlyArray<{
28
41
  readonly name: string;
29
- readonly type?: unknown;
30
- readonly required?: boolean;
31
- readonly description?: unknown;
42
+ readonly props?: ReadonlyArray<DocProp>;
32
43
  }>;
33
44
  }
34
45
 
46
+ /**
47
+ * Typed slots (AST-082 FR4) for documented free-form content. `text` is a
48
+ * string in the core contract, as a badge's label or a heading's words;
49
+ * `content` is child Astryx components, as a stack's or a card's children.
50
+ */
51
+ export type SlotKind = 'text' | 'content';
52
+
35
53
  /** What the native package adds to one core component. */
36
54
  export interface NativeAdditions {
37
55
  readonly name: string;
38
56
  readonly baseTarget: BaseTarget;
57
+ /**
58
+ * Where core documents the component when it is not `<Name>/<Name>.doc.mjs`:
59
+ * `file` relative to core's `src` without `.doc.mjs`, and `component` when
60
+ * the file documents several components.
61
+ */
62
+ readonly doc?: {readonly file: string; readonly component?: string};
63
+ /** Documented free-form content props, given a typed slot in the core. */
64
+ readonly slots?: Readonly<Record<string, SlotKind>>;
65
+ /**
66
+ * Callbacks whose only documented argument is a DOM event but which name a
67
+ * moment every platform has, such as a button's press. Their core form
68
+ * takes no arguments.
69
+ */
70
+ readonly callbacks?: readonly string[];
71
+ /**
72
+ * Core props whose name a platform toolkit also uses, with the same meaning
73
+ * (AST-082 FR8 forbids only a different meaning), and why: a button's
74
+ * `onClick` is a press in Compose too.
75
+ */
76
+ readonly sameMeaning?: Readonly<Record<string, string>>;
39
77
  /**
40
78
  * Portable definitions for named types the documentation references. Fields
41
79
  * the web type has that are not portable are left out on purpose.
@@ -51,6 +89,29 @@ export interface NativeAdditions {
51
89
  readonly webOnly?: Readonly<Record<string, string>>;
52
90
  }
53
91
 
92
+ /** The documented props of the component the additions name. */
93
+ function documentedProps(
94
+ doc: ComponentDocLike,
95
+ additions: NativeAdditions,
96
+ ): ReadonlyArray<DocProp> {
97
+ const wanted = additions.doc?.component;
98
+ if (wanted == null) {
99
+ return doc.props ?? [];
100
+ }
101
+ const entry = doc.components?.find(component => component.name === wanted);
102
+ if (!entry) {
103
+ throw new Error(
104
+ `${additions.name}: core documentation "${doc.name}" does not document "${wanted}"`,
105
+ );
106
+ }
107
+ return entry.props ?? [];
108
+ }
109
+
110
+ const SLOT_TYPES: Readonly<Record<SlotKind, PropType>> = {
111
+ text: {kind: 'string'},
112
+ content: {kind: 'content'},
113
+ };
114
+
54
115
  export function fromComponentDoc(
55
116
  doc: ComponentDocLike,
56
117
  additions: NativeAdditions,
@@ -58,8 +119,15 @@ export function fromComponentDoc(
58
119
  const core: Record<string, PropSpec> = {};
59
120
  const webOnly: WebOnlyProp[] = [];
60
121
  const types = additions.types ?? {};
122
+ const describe = (prop: DocProp) =>
123
+ typeof prop.description === 'string' ? {doc: prop.description} : {};
61
124
 
62
- for (const prop of doc.props ?? []) {
125
+ for (const prop of documentedProps(doc, additions)) {
126
+ // `id` and `isHidden` are shared base props on every platform (FR7), so a
127
+ // documented prop of either name is the base prop, not a core prop.
128
+ if (prop.name === 'id' || prop.name === 'isHidden') {
129
+ continue;
130
+ }
63
131
  const declaredWebOnly = additions.webOnly?.[prop.name];
64
132
  if (declaredWebOnly) {
65
133
  webOnly.push({name: prop.name, reason: declaredWebOnly});
@@ -76,13 +144,39 @@ export function fromComponentDoc(
76
144
  webOnly.push({name: prop.name, reason: 'HTML attribute'});
77
145
  continue;
78
146
  }
79
- const parsed = parseDocType(String(prop.type ?? ''));
147
+ const slot = additions.slots?.[prop.name];
148
+ const slotNameProblem =
149
+ slot &&
150
+ coreNameProblem(prop.name, prop.name in (additions.sameMeaning ?? {}));
151
+ if (slotNameProblem) {
152
+ webOnly.push({name: prop.name, reason: slotNameProblem});
153
+ continue;
154
+ }
155
+ if (slot) {
156
+ core[prop.name] = {
157
+ type: SLOT_TYPES[slot],
158
+ optional: prop.required !== true,
159
+ ...describe(prop),
160
+ };
161
+ continue;
162
+ }
163
+ let parsed = parseDocType(String(prop.type ?? ''));
164
+ if (
165
+ !parsed.ok &&
166
+ parsed.eventOnly === true &&
167
+ additions.callbacks?.includes(prop.name)
168
+ ) {
169
+ parsed = {ok: true, type: {kind: 'fn', args: []}};
170
+ }
80
171
  if (!parsed.ok) {
81
172
  webOnly.push({name: prop.name, reason: parsed.reason});
82
173
  continue;
83
174
  }
84
175
  // A portable type can still carry a name another platform owns.
85
- const nameProblem = coreNameProblem(prop.name);
176
+ const nameProblem = coreNameProblem(
177
+ prop.name,
178
+ prop.name in (additions.sameMeaning ?? {}),
179
+ );
86
180
  if (nameProblem) {
87
181
  webOnly.push({name: prop.name, reason: nameProblem});
88
182
  continue;
@@ -116,7 +210,7 @@ export function fromComponentDoc(
116
210
  core[prop.name] = {
117
211
  type,
118
212
  optional: prop.required !== true,
119
- ...(typeof prop.description === 'string' ? {doc: prop.description} : {}),
213
+ ...describe(prop),
120
214
  };
121
215
  }
122
216
 
@@ -127,5 +221,8 @@ export function fromComponentDoc(
127
221
  core,
128
222
  platforms: additions.platforms ?? {},
129
223
  webOnly,
224
+ sameMeaning: Object.keys(additions.sameMeaning ?? {}).filter(
225
+ name => name in core,
226
+ ),
130
227
  };
131
228
  }