@astryxdesign/cli 0.6.3-canary.ea2f048 → 0.6.3-canary.ebaebc4

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 (188) hide show
  1. package/README.md +2 -1
  2. package/api/build/build.type.d.mts +2 -2
  3. package/api/build/build.type.mjs +2 -2
  4. package/api/component/component.type.d.mts +6 -6
  5. package/api/component/component.type.mjs +19 -19
  6. package/api/discover/discover.type.d.mts +4 -4
  7. package/api/discover/discover.type.mjs +10 -10
  8. package/api/docs/_adapter.d.mts +37 -24
  9. package/api/docs/_adapter.mjs +169 -83
  10. package/api/docs/compiled-topics.test.mjs +78 -0
  11. package/api/docs/detail/detail.mjs +14 -63
  12. package/api/docs/detail/section/section.d.mts +1 -1
  13. package/api/docs/detail/section/section.mjs +44 -20
  14. package/api/docs/detail/section/section.test.mjs +41 -0
  15. package/api/docs/docs.d.mts +7 -2
  16. package/api/docs/docs.doc.mjs +27 -10
  17. package/api/docs/docs.mjs +16 -9
  18. package/api/docs/docs.test.mjs +6 -0
  19. package/api/docs/docs.type.d.mts +40 -3
  20. package/api/docs/docs.type.mjs +36 -8
  21. package/api/docs/index/index.d.mts +18 -0
  22. package/api/docs/index/index.mjs +32 -0
  23. package/api/docs/index/index.test.mjs +62 -0
  24. package/api/docs/integrationDocs.test.mjs +106 -0
  25. package/api/doctor/doctor.d.mts +48 -0
  26. package/api/doctor/doctor.mjs +232 -0
  27. package/api/doctor/doctor.test.mjs +196 -0
  28. package/api/hook/hook.type.d.mts +3 -3
  29. package/api/hook/hook.type.mjs +11 -11
  30. package/api/hook/list/list.d.mts +1 -1
  31. package/api/integration/add-contribution.mjs +5 -3
  32. package/api/integration/add-contribution.test.mjs +4 -4
  33. package/api/integration/integration-authoring.type.d.mts +1 -1
  34. package/api/integration/pack-check.mjs +49 -7
  35. package/api/integration/pack-check.test.mjs +249 -0
  36. package/api/search/search.d.mts +1 -1
  37. package/api/search/search.mjs +5 -5
  38. package/api/search/search.type.d.mts +2 -2
  39. package/api/search/search.type.mjs +1 -1
  40. package/api/swizzle/swizzle.type.d.mts +2 -2
  41. package/api/swizzle/swizzle.type.mjs +2 -2
  42. package/api/template/template.d.mts +1 -1
  43. package/api/template/template.type.d.mts +6 -6
  44. package/api/template/template.type.mjs +12 -12
  45. package/api/theme/build/build.mjs +20 -6
  46. package/api/theme/build/build.test.mjs +127 -0
  47. package/api/theme/palette/generate/generate.mjs +1 -1
  48. package/api/theme/palette/generate/generator.d.mts +10 -13
  49. package/api/theme/palette/generate/generator.mjs +7 -3
  50. package/api/theme/theme.type.d.mts +170 -11
  51. package/api/theme/theme.type.mjs +94 -27
  52. package/api/upgrade/_adapter.mjs +71 -5
  53. package/api/upgrade/project-context.test.mjs +272 -0
  54. package/api/upgrade/upgrade.doc.mjs +4 -3
  55. package/api/upgrade/upgrade.type.d.mts +5 -5
  56. package/api/upgrade/upgrade.type.mjs +11 -11
  57. package/assets/codemods/integration-discovery.mjs +40 -2
  58. package/assets/codemods/integration-discovery.test.mjs +58 -0
  59. package/assets/codemods/transforms/v0.3.0/__tests__/unwrap-authoring-factories.test.mjs +27 -5
  60. package/assets/codemods/transforms/v0.3.0/unwrap-authoring-factories.mjs +20 -5
  61. package/assets/docs/README.md +9 -0
  62. package/assets/docs/authoring.doc.mjs +14 -0
  63. package/assets/docs/cli-integrations.doc.mjs +86 -15
  64. package/assets/docs/styling-libraries.doc.mjs +1 -1
  65. package/assets/docs/working-with-ai.doc.mjs +1 -1
  66. package/authoring/_shared/contract.ts +22 -0
  67. package/authoring/codemod/codemod.doc.mjs +6 -1
  68. package/authoring/codemod/parse.d.mts +8 -8
  69. package/authoring/codemod/parse.mjs +8 -6
  70. package/authoring/config/parse.d.mts +13 -13
  71. package/authoring/config/parse.mjs +8 -8
  72. package/authoring/config/type.ts +3 -3
  73. package/authoring/debug/parse.d.mts +5 -5
  74. package/authoring/debug/parse.mjs +3 -3
  75. package/authoring/doctypes/_schema.d.mts +788 -23
  76. package/authoring/doctypes/_schema.mjs +492 -39
  77. package/authoring/doctypes/base/graph-fields.doc.d.mts +9 -0
  78. package/authoring/doctypes/base/graph-fields.doc.mjs +62 -0
  79. package/authoring/doctypes/base/type.ts +40 -0
  80. package/authoring/doctypes/command/command.doc.mjs +3 -2
  81. package/authoring/doctypes/command/parse.d.mts +2 -2
  82. package/authoring/doctypes/command/parse.mjs +1 -1
  83. package/authoring/doctypes/command/type.ts +3 -2
  84. package/authoring/doctypes/component/component.doc.mjs +6 -3
  85. package/authoring/doctypes/component/parse.d.mts +2 -2
  86. package/authoring/doctypes/component/parse.mjs +1 -1
  87. package/authoring/doctypes/component/type.ts +4 -3
  88. package/authoring/doctypes/enum/parse.d.mts +2 -2
  89. package/authoring/doctypes/enum/parse.mjs +1 -1
  90. package/authoring/doctypes/enum/type.ts +3 -1
  91. package/authoring/doctypes/function/function.doc.mjs +4 -0
  92. package/authoring/doctypes/function/parse.d.mts +2 -2
  93. package/authoring/doctypes/function/parse.mjs +1 -1
  94. package/authoring/doctypes/function/type.ts +6 -2
  95. package/authoring/doctypes/hook/hook.doc.mjs +4 -0
  96. package/authoring/doctypes/hook/parse.d.mts +2 -2
  97. package/authoring/doctypes/hook/parse.mjs +1 -1
  98. package/authoring/doctypes/hook/type.ts +3 -2
  99. package/authoring/doctypes/legacy.d.mts +8 -6
  100. package/authoring/doctypes/legacy.mjs +5 -4
  101. package/authoring/doctypes/load-contract.test.mjs +207 -0
  102. package/authoring/doctypes/namespace/namespace.doc.d.mts +9 -0
  103. package/authoring/doctypes/namespace/namespace.doc.mjs +132 -0
  104. package/authoring/doctypes/namespace/parse.d.mts +12 -0
  105. package/authoring/doctypes/namespace/parse.mjs +25 -0
  106. package/authoring/doctypes/namespace/parse.test.mjs +165 -0
  107. package/authoring/doctypes/namespace/type.ts +71 -0
  108. package/authoring/doctypes/parse.d.mts +20 -18
  109. package/authoring/doctypes/parse.mjs +16 -10
  110. package/authoring/doctypes/parse.test.mjs +77 -3
  111. package/authoring/doctypes/reference/parse.d.mts +2 -2
  112. package/authoring/doctypes/reference/parse.mjs +8 -5
  113. package/authoring/doctypes/reference/reference.doc.mjs +17 -4
  114. package/authoring/doctypes/reference/type.ts +51 -5
  115. package/authoring/doctypes/schema/parse.d.mts +2 -2
  116. package/authoring/doctypes/schema/parse.mjs +1 -1
  117. package/authoring/doctypes/schema/type.ts +3 -2
  118. package/authoring/doctypes/template/parse.d.mts +92 -1
  119. package/authoring/doctypes/template/parse.mjs +36 -2
  120. package/authoring/doctypes/template/parse.test.mjs +8 -2
  121. package/authoring/doctypes/template/template.doc.mjs +4 -0
  122. package/authoring/doctypes/template/type.ts +5 -2
  123. package/authoring/doctypes/types.ts +10 -9
  124. package/authoring/gap-report/parse.d.mts +10 -10
  125. package/authoring/gap-report/parse.mjs +6 -6
  126. package/authoring/gap-report/type.ts +1 -1
  127. package/authoring/identity/identity.doc.d.mts +9 -0
  128. package/authoring/identity/identity.doc.mjs +61 -0
  129. package/authoring/identity/type.ts +132 -0
  130. package/authoring/index.d.mts +1 -0
  131. package/authoring/index.d.ts +49 -17
  132. package/authoring/index.mjs +1 -0
  133. package/authoring/integration/integration.doc.mjs +13 -6
  134. package/authoring/integration/parse.d.mts +2 -2
  135. package/authoring/integration/parse.mjs +1 -1
  136. package/authoring/integration/parse.test.mjs +10 -1
  137. package/authoring/integration/schema.d.mts +6 -4
  138. package/authoring/integration/schema.mjs +9 -3
  139. package/authoring/integration/type.ts +23 -6
  140. package/authoring/shadcn/receipt.d.mts +6 -6
  141. package/clients/cli/commands/docs.doc.mjs +13 -3
  142. package/clients/cli/commands/docs.mjs +121 -21
  143. package/clients/cli/commands/docs.test.mjs +88 -0
  144. package/clients/cli/commands/integration-authoring.test.mjs +13 -9
  145. package/clients/cli/commands/theme-palette-generate.doc.mjs +8 -4
  146. package/clients/cli/commands/upgrade.doc.mjs +2 -2
  147. package/clients/cli/formatters/index.mjs +162 -1
  148. package/clients/cli/formatters/index.test.mjs +91 -0
  149. package/clients/cli/lib/manifest.mjs +7 -2
  150. package/foundation/config/project.mjs +21 -6
  151. package/foundation/discovery/authoring-self-docs.d.mts +69 -0
  152. package/foundation/discovery/authoring-self-docs.mjs +214 -0
  153. package/foundation/discovery/authoring-self-docs.test.mjs +154 -0
  154. package/foundation/discovery/component-discovery.d.mts +1 -1
  155. package/foundation/discovery/component-discovery.mjs +2 -1
  156. package/foundation/discovery/docs-discovery.d.mts +11 -4
  157. package/foundation/discovery/docs-discovery.mjs +208 -88
  158. package/foundation/discovery/docs-discovery.test.mjs +279 -13
  159. package/foundation/discovery/docs-output-budget.d.mts +28 -0
  160. package/foundation/discovery/docs-output-budget.mjs +50 -0
  161. package/foundation/discovery/docs-section-key.d.mts +98 -0
  162. package/foundation/discovery/docs-section-key.mjs +221 -0
  163. package/foundation/discovery/docs-section-key.test.mjs +224 -0
  164. package/foundation/discovery/template-adapter.mjs +2 -1
  165. package/foundation/discovery/theming-targets.test.mjs +4 -0
  166. package/foundation/doc-compiler/compile.d.mts +162 -0
  167. package/foundation/doc-compiler/compile.mjs +262 -0
  168. package/foundation/doc-compiler/doc-compiler.test.mjs +687 -0
  169. package/foundation/doc-compiler/ir.d.mts +9 -0
  170. package/foundation/doc-compiler/ir.mjs +287 -0
  171. package/foundation/doc-compiler/lenses.d.mts +33 -0
  172. package/foundation/doc-compiler/lenses.mjs +127 -0
  173. package/foundation/identity/provider-identity.d.mts +90 -0
  174. package/foundation/identity/provider-identity.mjs +320 -0
  175. package/foundation/identity/provider-identity.test.mjs +254 -0
  176. package/foundation/identity/providers.d.mts +7 -0
  177. package/foundation/identity/providers.mjs +16 -0
  178. package/foundation/integrations/autolink.mjs +12 -5
  179. package/foundation/integrations/integration-warnings.mjs +6 -0
  180. package/foundation/integrations/integrations.d.mts +46 -2
  181. package/foundation/integrations/integrations.mjs +167 -8
  182. package/foundation/integrations/integrations.test.mjs +384 -1
  183. package/foundation/integrations/provider-conflicts.test.mjs +125 -0
  184. package/foundation/integrations/validate-contributions.d.mts +2 -0
  185. package/foundation/integrations/validate-contributions.mjs +10 -0
  186. package/foundation/response/json-contract.test.mjs +46 -17
  187. package/foundation/response/response-types.doc.mjs +6 -1
  188. package/package.json +9 -11
@@ -0,0 +1,221 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Stable section keys, section lookup, and the topic index.
5
+ *
6
+ * @input Reference-doc sections, each with an optional authored `id`.
7
+ * @output The key a section is addressed by (its `id`, else a kebab-case key
8
+ * derived from its authored title), lookup by key or title, and the compact
9
+ * index a topic-only docs read returns.
10
+ * @position Shared by docs discovery (which rejects colliding keys before a
11
+ * reader sees them), the docs leaves (index, section, detail), and Doctor
12
+ * (output budgets). Imports nothing from discovery, so both can use it.
13
+ */
14
+
15
+ /** A stable key: lowercase letters and digits, joined by single hyphens. */
16
+ export const SECTION_KEY_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
17
+
18
+ /** The longest summary an index entry carries, in characters. */
19
+ export const SECTION_SUMMARY_MAX = 240;
20
+
21
+ /**
22
+ * A section's authored title. A `--zh`/`--dense` overlay replaces the visible
23
+ * title, but extensions are written against the authored one and keys derive
24
+ * from it, so both stay the same in every language.
25
+ */
26
+ const SOURCE_TITLE = Symbol('astryx.docs.sourceTitle');
27
+
28
+ /**
29
+ * Record the authored title of a section whose visible title a translation
30
+ * overlay replaces.
31
+ * @template {object} T
32
+ * @param {T} section
33
+ * @param {string} title
34
+ * @returns {T}
35
+ */
36
+ export function withSourceTitle(section, title) {
37
+ Object.defineProperty(section, SOURCE_TITLE, {value: title, configurable: true});
38
+ return section;
39
+ }
40
+
41
+ /**
42
+ * @param {any} section
43
+ * @returns {string}
44
+ */
45
+ export function sourceTitle(section) {
46
+ return section?.[SOURCE_TITLE] ?? section?.title;
47
+ }
48
+
49
+ /**
50
+ * The key a title derives: accents folded, `&` spelled out, and every other
51
+ * run of non-alphanumerics collapsed to one hyphen. Empty when the title has
52
+ * no Latin letters or digits to derive from.
53
+ * @param {unknown} title
54
+ * @returns {string}
55
+ */
56
+ export function sectionTitleKey(title) {
57
+ if (typeof title !== 'string') return '';
58
+ return title
59
+ .normalize('NFKD')
60
+ .replace(/[\u0300-\u036f]/g, '')
61
+ .toLowerCase()
62
+ .replace(/&/g, ' and ')
63
+ .replace(/[^a-z0-9]+/g, '-')
64
+ .replace(/^-+|-+$/g, '');
65
+ }
66
+
67
+ /**
68
+ * The key a section is addressed by: its authored `id`, else the key its
69
+ * authored title derives.
70
+ * @param {any} section
71
+ * @returns {string}
72
+ */
73
+ export function sectionKey(section) {
74
+ return typeof section?.id === 'string'
75
+ ? section.id
76
+ : sectionTitleKey(sourceTitle(section));
77
+ }
78
+
79
+ /**
80
+ * Problems with the keys of a topic's sections: an authored id that is not a
81
+ * stable key, a title no key derives from, and two sections sharing a key.
82
+ * Keys are never suffixed to make them unique, because readers link to them.
83
+ * @param {any[]} sections
84
+ * @returns {string[]}
85
+ */
86
+ export function sectionKeyProblems(sections) {
87
+ /** @type {string[]} */
88
+ const problems = [];
89
+ /** @type {Map<string, number>} */
90
+ const seen = new Map();
91
+ sections.forEach((section, s) => {
92
+ const at = `sections[${s}]`;
93
+ // A missing title is reported where titles are checked; no key to judge.
94
+ if (
95
+ section?.id == null &&
96
+ (typeof section?.title !== 'string' || section.title === '')
97
+ ) {
98
+ return;
99
+ }
100
+ if (
101
+ section?.id != null &&
102
+ (typeof section.id !== 'string' || !SECTION_KEY_RE.test(section.id))
103
+ ) {
104
+ problems.push(
105
+ `${at}.id: ${JSON.stringify(section.id)} is not a stable key. Use lowercase letters and digits joined by single hyphens.`,
106
+ );
107
+ return;
108
+ }
109
+ const key = sectionKey(section);
110
+ if (key === '') {
111
+ problems.push(
112
+ `${at}: no key derives from the title ${JSON.stringify(section?.title)}. Give the section an id.`,
113
+ );
114
+ return;
115
+ }
116
+ const first = seen.get(key);
117
+ if (first != null) {
118
+ problems.push(
119
+ `${at}: the key "${key}" is already used by sections[${first}]. Give one of them a distinct id or title.`,
120
+ );
121
+ return;
122
+ }
123
+ seen.set(key, s);
124
+ });
125
+ return problems;
126
+ }
127
+
128
+ /**
129
+ * Stamp every section with the key it is addressed by. Runs only after
130
+ * extensions merge: a derived key must never take part in merge matching.
131
+ * @template {{sections: any[]}} T
132
+ * @param {T} doc
133
+ * @returns {T}
134
+ */
135
+ export function withSectionKeys(doc) {
136
+ return {
137
+ ...doc,
138
+ sections: doc.sections.map(section =>
139
+ section.id != null
140
+ ? section
141
+ : withSourceTitle(
142
+ {...section, id: sectionKey(section)},
143
+ sourceTitle(section),
144
+ ),
145
+ ),
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Find the section a reader asked for: by key, then by exact title (or the
151
+ * key the query derives), then by a title that contains the query. More than
152
+ * one match is refused rather than guessed.
153
+ * @param {any[]} sections
154
+ * @param {string} query
155
+ * @returns {{section: any | null, candidates: any[]}} `candidates` lists the
156
+ * matches when the query is ambiguous, and is empty when nothing matches
157
+ */
158
+ export function findDocSection(sections, query) {
159
+ const wanted = query.trim();
160
+ const byKey = sections.find(section => sectionKey(section) === wanted);
161
+ if (byKey) return {section: byKey, candidates: []};
162
+
163
+ const lower = wanted.toLowerCase();
164
+ const derived = sectionTitleKey(wanted);
165
+ const exact = sections.filter(
166
+ section =>
167
+ section.title.toLowerCase() === lower ||
168
+ (derived !== '' && sectionTitleKey(sourceTitle(section)) === derived),
169
+ );
170
+ if (exact.length === 1) return {section: exact[0], candidates: []};
171
+ if (exact.length > 1) return {section: null, candidates: exact};
172
+
173
+ const partial = sections.filter(section =>
174
+ section.title.toLowerCase().includes(lower),
175
+ );
176
+ if (partial.length === 1) return {section: partial[0], candidates: []};
177
+ return {section: null, candidates: partial};
178
+ }
179
+
180
+ /**
181
+ * One line that says what a section holds: its first prose or list text,
182
+ * whitespace collapsed, cut at a word boundary.
183
+ * @param {any} section
184
+ * @param {number} [max]
185
+ * @returns {string}
186
+ */
187
+ export function sectionSummary(section, max = SECTION_SUMMARY_MAX) {
188
+ const first = (section?.content ?? []).find(
189
+ (/** @type {any} */ block) =>
190
+ (block?.type === 'prose' && typeof block.text === 'string') ||
191
+ (block?.type === 'list' && Array.isArray(block.items)),
192
+ );
193
+ const raw =
194
+ first == null ? '' : first.type === 'prose' ? first.text : first.items[0];
195
+ const text = String(raw ?? '')
196
+ .replace(/\s+/g, ' ')
197
+ .trim();
198
+ if (text.length <= max) return text;
199
+ const cut = text.slice(0, max - 1);
200
+ const space = cut.lastIndexOf(' ');
201
+ return `${(space > max / 2 ? cut.slice(0, space) : cut).trimEnd()}…`;
202
+ }
203
+
204
+ /**
205
+ * The index a topic-only read returns: what the topic is, and one entry per
206
+ * section with the key to read it by.
207
+ * @param {{name: string, title: string, description: string, sections: any[]}} doc
208
+ * @returns {import('../../api/docs/docs.type.mjs').DocsIndex}
209
+ */
210
+ export function buildDocsIndexData(doc) {
211
+ return {
212
+ name: doc.name,
213
+ title: doc.title,
214
+ description: doc.description,
215
+ sections: doc.sections.map(section => ({
216
+ id: sectionKey(section),
217
+ title: section.title,
218
+ summary: sectionSummary(section),
219
+ })),
220
+ };
221
+ }
@@ -0,0 +1,224 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {describe, expect, it} from 'vitest';
4
+ import {
5
+ SECTION_KEY_RE,
6
+ SECTION_SUMMARY_MAX,
7
+ buildDocsIndexData,
8
+ findDocSection,
9
+ sectionKey,
10
+ sectionKeyProblems,
11
+ sectionSummary,
12
+ sectionTitleKey,
13
+ sourceTitle,
14
+ withSectionKeys,
15
+ withSourceTitle,
16
+ } from './docs-section-key.mjs';
17
+
18
+ /** @param {string} title @param {object} [fields] */
19
+ const section = (title, fields = {}) => ({title, content: [], ...fields});
20
+
21
+ describe('sectionTitleKey', () => {
22
+ it.each([
23
+ ['Quick Start', 'quick-start'],
24
+ ['Light/Dark Mode', 'light-dark-mode'],
25
+ ["Do & Don't", 'do-and-don-t'],
26
+ [' Café Crème ', 'cafe-creme'],
27
+ ['useTheme()', 'usetheme'],
28
+ ['--- Tokens ---', 'tokens'],
29
+ ['亮/暗模式', ''],
30
+ ['', ''],
31
+ ])('%j derives %j', (title, key) => {
32
+ expect(sectionTitleKey(title)).toBe(key);
33
+ });
34
+
35
+ it('derives only stable keys', () => {
36
+ for (const title of ['A B', 'x__y', 'Élan 2.0', 'a-b-', '-a']) {
37
+ expect(sectionTitleKey(title)).toMatch(SECTION_KEY_RE);
38
+ }
39
+ });
40
+
41
+ it('derives nothing from a non-string', () => {
42
+ expect(sectionTitleKey(undefined)).toBe('');
43
+ expect(sectionTitleKey(42)).toBe('');
44
+ });
45
+ });
46
+
47
+ describe('sectionKey', () => {
48
+ it('prefers the authored id', () => {
49
+ expect(sectionKey(section('Quick Start', {id: 'start'}))).toBe('start');
50
+ });
51
+
52
+ it('derives from the authored title, not a translated one', () => {
53
+ const translated = withSourceTitle(section('快速开始'), 'Quick Start');
54
+ expect(sectionKey(translated)).toBe('quick-start');
55
+ });
56
+ });
57
+
58
+ describe('sectionKeyProblems', () => {
59
+ it('accepts distinct keys', () => {
60
+ expect(
61
+ sectionKeyProblems([section('Install'), section('Usage', {id: 'use'})]),
62
+ ).toEqual([]);
63
+ });
64
+
65
+ it('rejects two sections that share a key', () => {
66
+ const problems = sectionKeyProblems([
67
+ section('Quick Start'),
68
+ section('Quick-start'),
69
+ ]);
70
+ expect(problems).toHaveLength(1);
71
+ expect(problems[0]).toContain('"quick-start" is already used by sections[0]');
72
+ });
73
+
74
+ it('rejects an authored id that collides with a derived key', () => {
75
+ expect(
76
+ sectionKeyProblems([section('Install'), section('Setup', {id: 'install'})]),
77
+ ).toHaveLength(1);
78
+ });
79
+
80
+ it('rejects an unsafe id and never suffixes one', () => {
81
+ for (const id of ['Quick Start', 'a_b', 'a--b', '-a', '', 7]) {
82
+ expect(sectionKeyProblems([section('Install', {id})])[0]).toContain(
83
+ 'is not a stable key',
84
+ );
85
+ }
86
+ });
87
+
88
+ it('rejects a title no key derives from', () => {
89
+ expect(sectionKeyProblems([section('亮/暗模式')])[0]).toContain(
90
+ 'Give the section an id',
91
+ );
92
+ expect(sectionKeyProblems([section('亮/暗模式', {id: 'light-dark'})])).toEqual(
93
+ [],
94
+ );
95
+ });
96
+
97
+ it('leaves a missing title to the title check', () => {
98
+ expect(sectionKeyProblems([{content: []}, section('')])).toEqual([]);
99
+ });
100
+ });
101
+
102
+ describe('withSectionKeys', () => {
103
+ it('stamps derived keys and keeps authored ones', () => {
104
+ const doc = withSectionKeys({
105
+ sections: [section('Quick Start'), section('Usage', {id: 'use'})],
106
+ });
107
+ expect(doc.sections.map(s => s.id)).toEqual(['quick-start', 'use']);
108
+ });
109
+
110
+ it('keeps the authored title of a translated section', () => {
111
+ const doc = withSectionKeys({
112
+ sections: [withSourceTitle(section('快速开始'), 'Quick Start')],
113
+ });
114
+ expect(doc.sections[0].id).toBe('quick-start');
115
+ expect(sourceTitle(doc.sections[0])).toBe('Quick Start');
116
+ });
117
+ });
118
+
119
+ describe('findDocSection', () => {
120
+ const sections = withSectionKeys({
121
+ sections: [
122
+ section('Quick Start'),
123
+ section('Theme Props'),
124
+ section('Theme Tokens', {id: 'tokens'}),
125
+ section('Overview', {id: 'overview-a'}),
126
+ section('Overview', {id: 'overview-b'}),
127
+ ],
128
+ }).sections;
129
+
130
+ it('finds a section by key first', () => {
131
+ expect(findDocSection(sections, 'tokens').section.title).toBe(
132
+ 'Theme Tokens',
133
+ );
134
+ });
135
+
136
+ it('finds a section by exact title, case-insensitively', () => {
137
+ expect(findDocSection(sections, 'quick start').section.id).toBe(
138
+ 'quick-start',
139
+ );
140
+ });
141
+
142
+ it('finds a section by the key its query derives', () => {
143
+ expect(findDocSection(sections, 'Quick-Start!').section.id).toBe(
144
+ 'quick-start',
145
+ );
146
+ });
147
+
148
+ it('finds a section by a unique part of its title', () => {
149
+ expect(findDocSection(sections, 'props').section.id).toBe('theme-props');
150
+ });
151
+
152
+ it('refuses an ambiguous exact title and lists the candidates', () => {
153
+ const {section: match, candidates} = findDocSection(sections, 'Overview');
154
+ expect(match).toBeNull();
155
+ expect(candidates.map(s => s.id)).toEqual(['overview-a', 'overview-b']);
156
+ });
157
+
158
+ it('refuses an ambiguous partial title', () => {
159
+ const {section: match, candidates} = findDocSection(sections, 'theme');
160
+ expect(match).toBeNull();
161
+ expect(candidates.map(s => s.id)).toEqual(['theme-props', 'tokens']);
162
+ });
163
+
164
+ it('returns nothing for no match', () => {
165
+ expect(findDocSection(sections, 'zzz')).toEqual({
166
+ section: null,
167
+ candidates: [],
168
+ });
169
+ });
170
+ });
171
+
172
+ describe('sectionSummary', () => {
173
+ it('uses the first prose or list text, whitespace collapsed', () => {
174
+ expect(
175
+ sectionSummary({
176
+ content: [
177
+ {type: 'code', lang: 'ts', code: 'x'},
178
+ {type: 'prose', text: 'Line one.\n\n Line two.'},
179
+ ],
180
+ }),
181
+ ).toBe('Line one. Line two.');
182
+ expect(
183
+ sectionSummary({content: [{type: 'list', items: ['First', 'Second']}]}),
184
+ ).toBe('First');
185
+ });
186
+
187
+ it('is empty when the section has no text', () => {
188
+ expect(sectionSummary({content: [{type: 'table', headers: [], rows: []}]})).toBe(
189
+ '',
190
+ );
191
+ });
192
+
193
+ it('cuts a long summary at a word boundary', () => {
194
+ const summary = sectionSummary({
195
+ content: [{type: 'prose', text: 'word '.repeat(200)}],
196
+ });
197
+ expect(summary.length).toBeLessThanOrEqual(SECTION_SUMMARY_MAX);
198
+ expect(summary.endsWith('word…')).toBe(true);
199
+ });
200
+ });
201
+
202
+ describe('buildDocsIndexData', () => {
203
+ it('lists each section by key, title, and summary', () => {
204
+ expect(
205
+ buildDocsIndexData({
206
+ name: 'theme',
207
+ title: 'Theme',
208
+ description: 'Theming.',
209
+ sections: [
210
+ section('Quick Start', {content: [{type: 'prose', text: 'Start.'}]}),
211
+ section('Usage', {id: 'use'}),
212
+ ],
213
+ }),
214
+ ).toEqual({
215
+ name: 'theme',
216
+ title: 'Theme',
217
+ description: 'Theming.',
218
+ sections: [
219
+ {id: 'quick-start', title: 'Quick Start', summary: 'Start.'},
220
+ {id: 'use', title: 'Usage', summary: ''},
221
+ ],
222
+ });
223
+ });
224
+ });
@@ -27,10 +27,11 @@ import {createJiti} from 'jiti';
27
27
  import {loadModuleWithParser} from '../fs/module-loader.mjs';
28
28
  import {parseTemplate} from '../../authoring/doctypes/template/parse.mjs';
29
29
  import {CLI_ROOT, discoverExternalPackages} from '../fs/paths.mjs';
30
+ import {CORE_PROVIDER_ID} from '../identity/providers.mjs';
30
31
  import {Project} from '../config/project.mjs';
31
32
 
32
33
  /** Identity used for core (built-in) templates in package-scoped listings. */
33
- const CORE_PACKAGE = '@astryxdesign/core';
34
+ const CORE_PACKAGE = CORE_PROVIDER_ID;
34
35
 
35
36
  /**
36
37
  * Identity for a template in package-scoped views. Core (built-in) templates
@@ -265,14 +265,18 @@ describe('collectThemingTargets', () => {
265
265
  expect(doc.subComponentOf).toBe('Dialog');
266
266
  expect(doc.theming.targets).toEqual([
267
267
  {className: 'astryx-dialog-header'},
268
+ {className: 'astryx-dialog-header-start-content'},
268
269
  {className: 'astryx-dialog-header-title-block'},
270
+ {className: 'astryx-dialog-header-end-content'},
269
271
  {className: 'astryx-dialog-header-close-icon'},
270
272
  ]);
271
273
  });
272
274
 
273
275
  it.each([
274
276
  'dialog-header',
277
+ 'dialog-header-start-content',
275
278
  'dialog-header-title-block',
279
+ 'dialog-header-end-content',
276
280
  'dialog-header-close-icon',
277
281
  ])('enumerates %s once under its canonical Dialog owner', async key => {
278
282
  const matches = (await enumerated).filter(target => target.key === key);
@@ -0,0 +1,162 @@
1
+ // @generated by scripts/sync-api-types.mjs from the JSDoc in foundation/**/*.mjs.
2
+ // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
+
4
+ /**
5
+ * One authored file, as discovery loaded it.
6
+ * @typedef {object} AuthoredFile
7
+ * @property {string} file the file's name, for messages only
8
+ * @property {any} [doc] the parsed doc, when it loaded
9
+ * @property {unknown} [error] why it did not load or parse
10
+ * @property {any} [overlay] the language overlay's export, when one applies
11
+ * @property {unknown} [overlayError] why the overlay did not load
12
+ */
13
+ /**
14
+ * @typedef {object} ReferenceTopicInput
15
+ * @property {string} id the topic's name in the catalog
16
+ * @property {string} provider the package that owns the topic
17
+ * @property {string | null} replaces the topic it took the place of
18
+ * @property {string | null} lang the overlay language, or null for authored text
19
+ * @property {AuthoredFile} base
20
+ * @property {Array<AuthoredFile & {provider: string}>} extensions in merge order
21
+ */
22
+ /**
23
+ * A token reference after linking: the target section's content, or why it
24
+ * has none.
25
+ * @typedef {{status: 'resolved', topic: string, section: string, previewType?: string, content: any[]}
26
+ * | {status: 'unknown-topic'}
27
+ * | {status: 'unknown-section'}} TokenRefResolution
28
+ */
29
+ /**
30
+ * @typedef {object} CompiledReferenceNode
31
+ * @property {number} schemaVersion
32
+ * @property {'reference'} kind
33
+ * @property {'lowered' | 'linked'} stage `linked` once every token reference
34
+ * carries its resolution; a lowered node carries none
35
+ * @property {string} id the topic's name in the catalog
36
+ * @property {string | null} lang
37
+ * @property {{provider: string, replaces: string | null, extensions: string[]}} provenance
38
+ * @property {Record<string, string>} sourceTitles section key -> authored title
39
+ * @property {any} doc the topic: authored fields in authored order, every
40
+ * section keyed; a linked node's token references carry `resolved`
41
+ */
42
+ /**
43
+ * Lower one topic: overlay each file, merge the extensions in order, and stamp
44
+ * every section with its key.
45
+ * @param {ReferenceTopicInput} input
46
+ * @returns {CompiledReferenceNode}
47
+ */
48
+ export function lowerReferenceTopic(input: ReferenceTopicInput): CompiledReferenceNode;
49
+ /**
50
+ * Link every section of a lowered node.
51
+ * @param {CompiledReferenceNode} node
52
+ * @param {(topic: string) => Promise<CompiledReferenceNode | null>} lowerTarget
53
+ * the lowered node a reference names, or null when no topic has that name
54
+ * @returns {Promise<CompiledReferenceNode>}
55
+ */
56
+ export function linkReferenceTopic(node: CompiledReferenceNode, lowerTarget: (topic: string) => Promise<CompiledReferenceNode | null>): Promise<CompiledReferenceNode>;
57
+ /**
58
+ * Resolve the token references in one section. A section with none comes back
59
+ * as it went in.
60
+ * @template {{content: any[]}} S
61
+ * @param {S} section
62
+ * @param {(topic: string) => Promise<CompiledReferenceNode | null>} lowerTarget
63
+ * @returns {Promise<S>}
64
+ */
65
+ export function linkReferenceSection<S extends {
66
+ content: any[];
67
+ }>(section: S, lowerTarget: (topic: string) => Promise<CompiledReferenceNode | null>): Promise<S>;
68
+ /** Bumped whenever the shape of a compiled node changes. */
69
+ export const COMPILED_DOC_SCHEMA_VERSION: 1;
70
+ /**
71
+ * One authored file, as discovery loaded it.
72
+ */
73
+ export type AuthoredFile = {
74
+ /**
75
+ * the file's name, for messages only
76
+ */
77
+ file: string;
78
+ /**
79
+ * the parsed doc, when it loaded
80
+ */
81
+ doc?: any;
82
+ /**
83
+ * why it did not load or parse
84
+ */
85
+ error?: unknown;
86
+ /**
87
+ * the language overlay's export, when one applies
88
+ */
89
+ overlay?: any;
90
+ /**
91
+ * why the overlay did not load
92
+ */
93
+ overlayError?: unknown;
94
+ };
95
+ export type ReferenceTopicInput = {
96
+ /**
97
+ * the topic's name in the catalog
98
+ */
99
+ id: string;
100
+ /**
101
+ * the package that owns the topic
102
+ */
103
+ provider: string;
104
+ /**
105
+ * the topic it took the place of
106
+ */
107
+ replaces: string | null;
108
+ /**
109
+ * the overlay language, or null for authored text
110
+ */
111
+ lang: string | null;
112
+ base: AuthoredFile;
113
+ /**
114
+ * in merge order
115
+ */
116
+ extensions: Array<AuthoredFile & {
117
+ provider: string;
118
+ }>;
119
+ };
120
+ /**
121
+ * A token reference after linking: the target section's content, or why it
122
+ * has none.
123
+ */
124
+ export type TokenRefResolution = {
125
+ status: "resolved";
126
+ topic: string;
127
+ section: string;
128
+ previewType?: string;
129
+ content: any[];
130
+ } | {
131
+ status: "unknown-topic";
132
+ } | {
133
+ status: "unknown-section";
134
+ };
135
+ export type CompiledReferenceNode = {
136
+ schemaVersion: number;
137
+ kind: "reference";
138
+ /**
139
+ * `linked` once every token reference
140
+ * carries its resolution; a lowered node carries none
141
+ */
142
+ stage: "lowered" | "linked";
143
+ /**
144
+ * the topic's name in the catalog
145
+ */
146
+ id: string;
147
+ lang: string | null;
148
+ provenance: {
149
+ provider: string;
150
+ replaces: string | null;
151
+ extensions: string[];
152
+ };
153
+ /**
154
+ * section key -> authored title
155
+ */
156
+ sourceTitles: Record<string, string>;
157
+ /**
158
+ * the topic: authored fields in authored order, every
159
+ * section keyed; a linked node's token references carry `resolved`
160
+ */
161
+ doc: any;
162
+ };