@astryxdesign/cli 0.7.12 → 0.7.13-canary.a5ad1f2

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 (82) hide show
  1. package/api/doctor/_adapter.d.mts +17 -0
  2. package/api/doctor/_adapter.mjs +40 -0
  3. package/api/doctor/doctor.d.mts +34 -4
  4. package/api/doctor/doctor.mjs +108 -21
  5. package/api/icon/_adapter.d.mts +6 -3
  6. package/api/icon/_adapter.mjs +11 -3
  7. package/api/icon/icon.type.d.mts +22 -1
  8. package/api/icon/icon.type.mjs +12 -4
  9. package/api/icon/iconUse.doc.mjs +27 -7
  10. package/api/icon/list/list.mjs +29 -3
  11. package/api/icon/use/use.d.mts +7 -3
  12. package/api/icon/use/use.mjs +68 -18
  13. package/api/integration/validate-integration.d.mts +4 -0
  14. package/api/integration/validate-integration.mjs +32 -2
  15. package/api/integration/validate-integration.type.d.mts +15 -0
  16. package/api/integration/validate-integration.type.mjs +6 -1
  17. package/api/template/copy/copy.mjs +4 -2
  18. package/api/template/template.type.d.mts +42 -4
  19. package/api/template/template.type.mjs +9 -6
  20. package/assets/discover/official-packages.json +4 -0
  21. package/assets/docs/tree/icon-sets.doc.mjs +91 -7
  22. package/assets/icons/definitions/alignBottom.doc.mjs +9 -0
  23. package/assets/icons/definitions/alignJustify.doc.mjs +9 -0
  24. package/assets/icons/definitions/alignMiddle.doc.mjs +9 -0
  25. package/assets/icons/definitions/alignTop.doc.mjs +9 -0
  26. package/assets/icons/definitions/angle.doc.mjs +9 -0
  27. package/assets/icons/definitions/cloud.doc.mjs +9 -0
  28. package/assets/icons/definitions/collection.doc.mjs +9 -0
  29. package/assets/icons/definitions/contrast.doc.mjs +9 -0
  30. package/assets/icons/definitions/cornerRadius.doc.mjs +9 -0
  31. package/assets/icons/definitions/cut.doc.mjs +9 -0
  32. package/assets/icons/definitions/fingerprint.doc.mjs +9 -0
  33. package/assets/icons/definitions/fire.doc.mjs +9 -0
  34. package/assets/icons/definitions/frame.doc.mjs +9 -0
  35. package/assets/icons/definitions/gift.doc.mjs +9 -0
  36. package/assets/icons/definitions/group.doc.mjs +9 -0
  37. package/assets/icons/definitions/integration.doc.mjs +9 -0
  38. package/assets/icons/definitions/lowercase.doc.mjs +9 -0
  39. package/assets/icons/definitions/paddingHorizontal.doc.mjs +9 -0
  40. package/assets/icons/definitions/paddingPerSide.doc.mjs +9 -0
  41. package/assets/icons/definitions/paddingVertical.doc.mjs +9 -0
  42. package/assets/icons/definitions/panelBottom.doc.mjs +9 -0
  43. package/assets/icons/definitions/photoEffect.doc.mjs +9 -0
  44. package/assets/icons/definitions/radiusPerCorner.doc.mjs +9 -0
  45. package/assets/icons/definitions/rotateCounterclockwise.doc.mjs +9 -0
  46. package/assets/icons/definitions/tablet.doc.mjs +9 -0
  47. package/assets/icons/definitions/text.doc.mjs +9 -0
  48. package/assets/icons/definitions/textRegular.doc.mjs +9 -0
  49. package/assets/icons/definitions/transfer.doc.mjs +9 -0
  50. package/assets/icons/definitions/uppercase.doc.mjs +9 -0
  51. package/assets/icons/definitions/userRemove.doc.mjs +9 -0
  52. package/assets/templates/blocks/components/BaseTypeahead/BaseTypeaheadMenu.doc.mjs +23 -0
  53. package/assets/templates/blocks/components/BaseTypeahead/BaseTypeaheadMenu.tsx +199 -0
  54. package/assets/templates/blocks/components/ButtonGroup/ButtonGroupShowcase.doc.mjs +1 -0
  55. package/assets/templates/blocks/components/ChatSystemMessage/ChatSystemMessageStatusUpdates.doc.mjs +7 -2
  56. package/assets/templates/pages/canvas-editor/template.doc.mjs +20 -0
  57. package/assets/templates/pages/editor/template.doc.mjs +1 -0
  58. package/assets/templates/pages/form-wizard-vertical/template.doc.mjs +1 -0
  59. package/assets/templates/pages/settings-dialog/template.doc.mjs +4 -0
  60. package/assets/templates/pages/shell-top-nav/template.doc.mjs +3 -0
  61. package/assets/templates/pages/table-collapsible/template.doc.mjs +1 -0
  62. package/assets/templates/pages/table-comparison/template.doc.mjs +1 -0
  63. package/authoring/config/config.doc.mjs +4 -4
  64. package/authoring/config/parse.d.mts +1 -1
  65. package/authoring/config/parse.mjs +3 -1
  66. package/authoring/config/type.ts +7 -5
  67. package/authoring/doctypes/template/template.doc.mjs +1 -1
  68. package/authoring/doctypes/template/type.ts +5 -3
  69. package/clients/cli/commands/experimental/icon.mjs +38 -9
  70. package/clients/cli/commands/experimental-icon-use.doc.mjs +9 -4
  71. package/clients/cli/commands/template.mjs +12 -3
  72. package/foundation/config/config-writer.d.mts +14 -0
  73. package/foundation/config/config-writer.mjs +73 -17
  74. package/foundation/discovery/icon-set-discovery.d.mts +77 -7
  75. package/foundation/discovery/icon-set-discovery.mjs +128 -42
  76. package/foundation/discovery/template-icon-notes.d.mts +46 -0
  77. package/foundation/discovery/template-icon-notes.mjs +94 -16
  78. package/foundation/discovery/template-icons.d.mts +247 -0
  79. package/foundation/discovery/template-icons.mjs +591 -37
  80. package/foundation/integrations/cli-requirement.d.mts +13 -4
  81. package/foundation/integrations/cli-requirement.mjs +18 -6
  82. package/package.json +6 -6
@@ -20,6 +20,21 @@ export function readTextFileIfExists(file: string): string | null;
20
20
  export function listProjectSourceFiles(root: string, generatedModule: string, options?: {
21
21
  stylesheets?: boolean;
22
22
  }): string[];
23
+ /**
24
+ * Each file in the project's own source that still uses the placeholder
25
+ * component the CLI writes for a missing icon: the file, the component's
26
+ * name, the meanings its uses name (null when none names one), and how many
27
+ * uses it has. A file reads only as a syntax tree, so a commented-out or
28
+ * quoted copy never counts. Read without executing app code.
29
+ * @param {string} projectDir
30
+ * @returns {Array<{file: string, local: string, meanings: string[] | null, uses: number}>}
31
+ */
32
+ export function findIconPlaceholders(projectDir: string): Array<{
33
+ file: string;
34
+ local: string;
35
+ meanings: string[] | null;
36
+ uses: number;
37
+ }>;
23
38
  /** @param {string} projectDir */
24
39
  export function discoverDoctorThemes(projectDir: string): Promise<import("../../foundation/discovery/theme-discovery.mjs").DiscoveredTheme[]>;
25
40
  /**
@@ -231,6 +246,8 @@ export function readBuiltPath(target: string, { fileLimit }?: {
231
246
  * @returns {string[]}
232
247
  */
233
248
  export function builtIntegrationPackages(target: string, cwd: string): string[];
249
+ export { placeholderFixes } from "../../foundation/discovery/template-icons.mjs";
250
+ export { unlistedIconSets } from "../../foundation/discovery/icon-set-discovery.mjs";
234
251
  export { resolveRecordedTheme } from "../theme/theme.mjs";
235
252
  export { collectUnloadedFonts } from "../theme/build/font-warning.mjs";
236
253
  export const SOURCE_EXTENSIONS: Set<string>;
@@ -39,6 +39,9 @@ import {
39
39
  } from '../../foundation/agent-docs/computer-agent-text.mjs';
40
40
  import {computerAgentTextSettings} from '../../foundation/env/user-settings.mjs';
41
41
  import * as fs from 'node:fs';
42
+ import {placeholderUses} from '../../foundation/discovery/template-icons.mjs';
43
+ export {placeholderFixes} from '../../foundation/discovery/template-icons.mjs';
44
+ export {unlistedIconSets} from '../../foundation/discovery/icon-set-discovery.mjs';
42
45
  import {elementPropOrigins} from '../../foundation/discovery/module-exports.mjs';
43
46
  import {cssImports} from '../../foundation/render/embedded-imports.mjs';
44
47
  import * as path from 'node:path';
@@ -150,6 +153,43 @@ export function listProjectSourceFiles(root, generatedModule, options = {}) {
150
153
  return files;
151
154
  }
152
155
 
156
+ /**
157
+ * Each file in the project's own source that still uses the placeholder
158
+ * component the CLI writes for a missing icon: the file, the component's
159
+ * name, the meanings its uses name (null when none names one), and how many
160
+ * uses it has. A file reads only as a syntax tree, so a commented-out or
161
+ * quoted copy never counts. Read without executing app code.
162
+ * @param {string} projectDir
163
+ * @returns {Array<{file: string, local: string, meanings: string[] | null, uses: number}>}
164
+ */
165
+ export function findIconPlaceholders(projectDir) {
166
+ /** @type {Array<{file: string, local: string, meanings: string[] | null, uses: number}>} */
167
+ const found = [];
168
+ for (const file of listProjectSourceFiles(projectDir, '')) {
169
+ let source;
170
+ try {
171
+ source = readTextFile(file);
172
+ } catch {
173
+ continue;
174
+ }
175
+ // Only a file with the placeholder's dash pattern is parsed.
176
+ if (!source.includes('3 2.33')) continue;
177
+ const uses = placeholderUses(source);
178
+ if (!uses || uses.length === 0) continue;
179
+ const shown = path.relative(projectDir, file).split(path.sep).join('/');
180
+ const meanings = [
181
+ ...new Set(uses.flatMap(each => each.meanings ?? [])),
182
+ ].sort();
183
+ found.push({
184
+ file: shown,
185
+ local: uses[0].local,
186
+ meanings: meanings.length > 0 ? meanings : null,
187
+ uses: uses.length,
188
+ });
189
+ }
190
+ return found.sort((a, b) => a.file.localeCompare(b.file));
191
+ }
192
+
153
193
  /** @param {string} projectDir */
154
194
  export async function discoverDoctorThemes(projectDir) {
155
195
  const project = await Project.load(projectDir, {core: 'project'});
@@ -87,16 +87,25 @@ export function checkPeerDeps(ctx: DoctorContext): DoctorCheck;
87
87
  */
88
88
  export function checkIntegrationIssues(ctx: DoctorContext): DoctorCheck;
89
89
  /**
90
- * The project's icon sets (AST-084 FR17). Fails when `experimental.iconSet` names a set
91
- * that is not installed, or one that is installed but withdrawn (with why);
92
- * warns when two or more sets are installed and none is the project's, and
93
- * for each other installed set that was withdrawn, with why.
90
+ * The project's icon sets (AST-084 FR17). Fails when `experimental.iconSet`
91
+ * names a set, main or backup, that is not installed, that is installed but
92
+ * withdrawn (with why), or that two packages ship under one name; and when
93
+ * more than one set is installed and the list leaves one out. Warns for each
94
+ * other installed set that was withdrawn, with why.
94
95
  * Icon sets are experimental (spec:AST-017 FR27): only a project that sets
95
96
  * `experimental.iconSet` gets the check, so every other report is unchanged.
96
97
  * @param {DoctorContext} ctx
97
98
  * @returns {DoctorCheck | null}
98
99
  */
99
100
  export function checkIconSets(ctx: DoctorContext): DoctorCheck | null;
101
+ /**
102
+ * The placeholder component for a missing icon, still in the project's own
103
+ * source (AST-084): each file and the meanings it stands for, with the commands that give each a
104
+ * real icon. Only a project that sets `experimental.iconSet` gets the check.
105
+ * @param {DoctorContext} ctx
106
+ * @returns {DoctorCheck | null}
107
+ */
108
+ export function checkIconPlaceholders(ctx: DoctorContext): DoctorCheck | null;
100
109
  /**
101
110
  * Check 10 — report the detected package manager, and say so when the project's
102
111
  * own declaration disagrees with what is on disk.
@@ -448,8 +457,29 @@ export type DoctorContext = {
448
457
  status: "selected" | "none" | "ambiguous" | "missing" | "withdrawn";
449
458
  reason: string;
450
459
  configured?: string;
460
+ backups?: Array<{
461
+ selector: string;
462
+ status: "selected" | "ambiguous" | "missing" | "withdrawn";
463
+ reason: string;
464
+ }>;
465
+ order?: Array<{
466
+ name: string;
467
+ package: string;
468
+ }>;
451
469
  };
452
470
  } | null | undefined;
471
+ /**
472
+ * Each file in the project's own source that uses the placeholder component
473
+ * the CLI writes for a missing icon, read only in a project that sets
474
+ * `experimental.iconSet`; null otherwise.
475
+ * Experimental: may change or be removed in a patch release.
476
+ */
477
+ iconPlaceholders?: {
478
+ file: string;
479
+ local: string;
480
+ meanings: string[] | null;
481
+ uses: number;
482
+ }[] | null | undefined;
453
483
  /**
454
484
  * - Why the CLI could not load the
455
485
  * project from its config, when it could not. Checks that need the loaded
@@ -42,6 +42,7 @@ import {
42
42
  } from '../../foundation/agent-docs/agent-docs.mjs';
43
43
  import {
44
44
  CLI_ROOT,
45
+ findPackageRoot,
45
46
  findProjectCoreDir,
46
47
  findInstalledPackage,
47
48
  } from '../../foundation/fs/paths.mjs';
@@ -107,6 +108,9 @@ import {
107
108
  projectCoreVersion,
108
109
  projectHasOwnCli,
109
110
  computerIntegrationsOutside,
111
+ findIconPlaceholders,
112
+ placeholderFixes,
113
+ unlistedIconSets,
110
114
  computerIntegrationsToAdd,
111
115
  displayHomePath,
112
116
  outsideProject,
@@ -157,9 +161,14 @@ import {checkBuilt} from './built-checks.mjs';
157
161
  * Combined project-level integration issues, including cross-package template replacement warnings.
158
162
  * @property {Error|null} [configError] - Error thrown while resolving the config
159
163
  * path (e.g. multiple config files present), surfaced by checkConfig as a FAIL.
160
- * @property {{sets: Array<{name: string, package: string}>, withdrawn: Array<{name: string, package: string, reason: string}>, choice: {status: 'selected'|'none'|'ambiguous'|'missing'|'withdrawn', reason: string, configured?: string}}|null} [iconSets]
164
+ * @property {{sets: Array<{name: string, package: string}>, withdrawn: Array<{name: string, package: string, reason: string}>, choice: {status: 'selected'|'none'|'ambiguous'|'missing'|'withdrawn', reason: string, configured?: string, backups?: Array<{selector: string, status: 'selected'|'ambiguous'|'missing'|'withdrawn', reason: string}>, order?: Array<{name: string, package: string}>}}|null} [iconSets]
161
165
  * The project's icon sets (AST-084 FR17), or null when they could not be read.
162
166
  * Experimental: may change or be removed in a patch release.
167
+ * @property {Array<{file: string, local: string, meanings: string[] | null, uses: number}>|null} [iconPlaceholders]
168
+ * Each file in the project's own source that uses the placeholder component
169
+ * the CLI writes for a missing icon, read only in a project that sets
170
+ * `experimental.iconSet`; null otherwise.
171
+ * Experimental: may change or be removed in a patch release.
163
172
  * @property {string|null} [projectError] - Why the CLI could not load the
164
173
  * project from its config, when it could not. Checks that need the loaded
165
174
  * project quote it when they skip, so a skip says what was found.
@@ -996,10 +1005,11 @@ export function checkIntegrationIssues(ctx) {
996
1005
  }
997
1006
 
998
1007
  /**
999
- * The project's icon sets (AST-084 FR17). Fails when `experimental.iconSet` names a set
1000
- * that is not installed, or one that is installed but withdrawn (with why);
1001
- * warns when two or more sets are installed and none is the project's, and
1002
- * for each other installed set that was withdrawn, with why.
1008
+ * The project's icon sets (AST-084 FR17). Fails when `experimental.iconSet`
1009
+ * names a set, main or backup, that is not installed, that is installed but
1010
+ * withdrawn (with why), or that two packages ship under one name; and when
1011
+ * more than one set is installed and the list leaves one out. Warns for each
1012
+ * other installed set that was withdrawn, with why.
1003
1013
  * Icon sets are experimental (spec:AST-017 FR27): only a project that sets
1004
1014
  * `experimental.iconSet` gets the check, so every other report is unchanged.
1005
1015
  * @param {DoctorContext} ctx
@@ -1013,38 +1023,66 @@ export function checkIconSets(ctx) {
1013
1023
  const {sets, withdrawn, choice} = state;
1014
1024
  // Only a project that opts in with `experimental.iconSet` gets the check.
1015
1025
  if (!choice.configured) return null;
1016
- if (choice.status === 'missing') {
1026
+ const list = [
1027
+ {selector: choice.configured, status: choice.status, reason: choice.reason},
1028
+ ...(choice.backups ?? []),
1029
+ ];
1030
+ const installFix =
1031
+ 'Install the package that ships the set, or remove it from experimental.iconSet in astryx.config (`astryx experimental icon --list` names the installed sets).';
1032
+ const missing = list.filter(each => each.status === 'missing');
1033
+ if (missing.length > 0) {
1017
1034
  return {
1018
1035
  id,
1019
1036
  label,
1020
1037
  status: 'fail',
1021
- message: choice.reason,
1022
- fix: 'Install the package that ships the set, or set experimental.iconSet in astryx.config to an installed set (`astryx experimental icon --list` names them).',
1038
+ message: missing.map(each => each.reason).join(' | '),
1039
+ fix: installFix,
1023
1040
  };
1024
1041
  }
1025
- if (choice.status === 'withdrawn') {
1042
+ const gone = list.filter(each => each.status === 'withdrawn');
1043
+ if (gone.length > 0) {
1026
1044
  return {
1027
1045
  id,
1028
1046
  label,
1029
1047
  status: 'fail',
1030
- message: choice.reason,
1031
- fix: 'Run `astryx integration check` in the package that ships the set, or install the module its data names; or set experimental.iconSet in astryx.config to a valid installed set (`astryx experimental icon --list` names them).',
1048
+ message: gone.map(each => each.reason).join(' | '),
1049
+ fix: 'Run `astryx integration check` in the package that ships the set, or install the module its data names; or remove it from experimental.iconSet in astryx.config (`astryx experimental icon --list` names the valid installed sets).',
1032
1050
  };
1033
1051
  }
1034
- /** @type {string[]} */
1035
- const problems = [];
1036
- if (choice.status === 'ambiguous') problems.push(choice.reason);
1037
- for (const each of withdrawn) problems.push(each.reason);
1038
- if (problems.length > 0) {
1052
+ const shared = list.filter(each => each.status === 'ambiguous');
1053
+ if (shared.length > 0) {
1054
+ return {
1055
+ id,
1056
+ label,
1057
+ status: 'fail',
1058
+ message: shared.map(each => each.reason).join(' | '),
1059
+ fix: 'Write each name in experimental.iconSet as "<package>/<set>" (`astryx experimental icon --list` names them).',
1060
+ };
1061
+ }
1062
+ const order = choice.order ?? [];
1063
+ const unlisted = sets.filter(set => !order.includes(set));
1064
+ if (sets.length > 1 && unlisted.length > 0) {
1065
+ const {problem, fix} = unlistedIconSets(
1066
+ sets.length,
1067
+ list[0].selector,
1068
+ list.slice(1).map(each => each.selector),
1069
+ unlisted.map(set => `${set.package}/${set.name}`),
1070
+ );
1071
+ return {
1072
+ id,
1073
+ label,
1074
+ status: 'fail',
1075
+ message: `${problem} (experimental.iconSet), so which set a copied template uses is not decided.`,
1076
+ fix,
1077
+ };
1078
+ }
1079
+ if (withdrawn.length > 0) {
1039
1080
  return {
1040
1081
  id,
1041
1082
  label,
1042
1083
  status: 'warn',
1043
- message: problems.join(' | '),
1044
- fix:
1045
- choice.status === 'ambiguous'
1046
- ? 'Set experimental.iconSet in astryx.config to "<package>/<set>" so templates use one set.'
1047
- : 'Run `astryx integration check` in the package that ships the set, or install its module.',
1084
+ message: withdrawn.map(each => each.reason).join(' | '),
1085
+ fix: 'Run `astryx integration check` in the package that ships the set, or install its module.',
1048
1086
  };
1049
1087
  }
1050
1088
  return {
@@ -1055,6 +1093,48 @@ export function checkIconSets(ctx) {
1055
1093
  };
1056
1094
  }
1057
1095
 
1096
+ /**
1097
+ * The placeholder component for a missing icon, still in the project's own
1098
+ * source (AST-084): each file and the meanings it stands for, with the commands that give each a
1099
+ * real icon. Only a project that sets `experimental.iconSet` gets the check.
1100
+ * @param {DoctorContext} ctx
1101
+ * @returns {DoctorCheck | null}
1102
+ */
1103
+ export function checkIconPlaceholders(ctx) {
1104
+ const choice = ctx.iconSets?.choice;
1105
+ if (!choice?.configured || ctx.iconPlaceholders == null) return null;
1106
+ const id = 'icon-placeholders';
1107
+ const label = 'Placeholder icons';
1108
+ const found = ctx.iconPlaceholders;
1109
+ if (found.length === 0) {
1110
+ return {
1111
+ id,
1112
+ label,
1113
+ status: 'pass',
1114
+ message: 'No copied code uses the placeholder for a missing icon.',
1115
+ };
1116
+ }
1117
+ const main = choice.configured;
1118
+ const meanings = [
1119
+ ...new Set(found.flatMap(each => each.meanings ?? [])),
1120
+ ].sort();
1121
+ const where = found
1122
+ .map(
1123
+ each =>
1124
+ `${each.file} (${each.meanings ? each.meanings.join(', ') : 'meaning not recorded'})`,
1125
+ )
1126
+ .join(', ');
1127
+ const uses = found.reduce((total, each) => total + each.uses, 0);
1128
+ const [mapFix, backupFix] = placeholderFixes(meanings, main);
1129
+ return {
1130
+ id,
1131
+ label,
1132
+ status: 'warn',
1133
+ message: `${uses} placeholder icon${uses === 1 ? '' : 's'} in copied code: no set in your list has ${meanings.length > 0 ? `"${meanings.join('", "')}"` : 'these icons'}. ${where}.`,
1134
+ fix: `Add a backup set that has each meaning (\`astryx discover --type icon-set\` lists sets and the definitions they map): \`${backupFix}\`; or, if you author your main set, map it there: \`${mapFix}\`. Then copy the template again, or replace each use of the placeholder (\`${found[0].local}\`) with the icon \`astryx experimental icon <meaning>\` prints, and delete its declaration. A built-in meaning (\`astryx experimental icon --definitions\` marks them) also renders from the theme by name: \`<Icon icon="<name>" />\`. Otherwise, use your own icon component (\`astryx docs icons custom-icons\`).`,
1135
+ };
1136
+ }
1137
+
1058
1138
  /**
1059
1139
  * Check 10 — report the detected package manager, and say so when the project's
1060
1140
  * own declaration disagrees with what is on disk.
@@ -2151,6 +2231,7 @@ export const SYNC_CHECKS = [
2151
2231
  checkProviderIdentity,
2152
2232
  checkIntegrationIssues,
2153
2233
  checkIconSets,
2234
+ checkIconPlaceholders,
2154
2235
  checkAgentDocs,
2155
2236
  checkPeerDeps,
2156
2237
  checkPackageManager,
@@ -2218,6 +2299,8 @@ export async function runChecks(options = {}) {
2218
2299
  let projectError = null;
2219
2300
  /** @type {DoctorContext['iconSets']} */
2220
2301
  let iconSets = null;
2302
+ /** @type {DoctorContext['iconPlaceholders']} */
2303
+ let iconPlaceholders = null;
2221
2304
  /** @type {import('../../foundation/config/project.mjs').Project | null} */
2222
2305
  let project = null;
2223
2306
  try {
@@ -2315,6 +2398,9 @@ export async function runChecks(options = {}) {
2315
2398
  }
2316
2399
  try {
2317
2400
  iconSets = await project.iconSets();
2401
+ if (iconSets.choice.configured) {
2402
+ iconPlaceholders = findIconPlaceholders(findPackageRoot(cwd) ?? cwd);
2403
+ }
2318
2404
  } catch {
2319
2405
  iconSets = null;
2320
2406
  }
@@ -2344,6 +2430,7 @@ export async function runChecks(options = {}) {
2344
2430
  configError,
2345
2431
  projectError,
2346
2432
  iconSets,
2433
+ iconPlaceholders,
2347
2434
  };
2348
2435
 
2349
2436
  /** @type {DoctorCheck[]} */
@@ -43,14 +43,17 @@ export function loadIconProject(cwd: string): Promise<{
43
43
  * `experimental.iconSet`, read from the project's package.json; null when
44
44
  * nothing says so.
45
45
  * @param {string} projectDir
46
+ * @param {{list?: boolean}} [options] `list`: the value written is a list
46
47
  * @returns {string | null}
47
48
  */
48
- export function iconSetConfigCliWarning(projectDir: string): string | null;
49
+ export function iconSetConfigCliWarning(projectDir: string, options?: {
50
+ list?: boolean;
51
+ }): string | null;
49
52
  export { astryxIconDefinitions } from "../../foundation/discovery/icon-definition-discovery.mjs";
50
- export { iconSetKey } from "../../foundation/discovery/icon-set-discovery.mjs";
51
- export { setConfigString } from "../../foundation/config/config-writer.mjs";
52
53
  export { exportValueKind } from "../../foundation/discovery/module-exports.mjs";
53
54
  export { iconRecord, importLine, leadingReading, matchIcon, answeringMatches, rankIcons, scoreIcon } from "../../foundation/discovery/icon-ranking.mjs";
55
+ export { iconSetKey, ICON_USE_ORDER_COMMAND, unlistedIconSets } from "../../foundation/discovery/icon-set-discovery.mjs";
56
+ export { setConfigString, setConfigValue } from "../../foundation/config/config-writer.mjs";
54
57
  export { assertIconExport, checkNewDefinition, definitionNameFromInput, definitionFile, findLocalIconSet, writeDefinitionFile, iconIdOf, closestNames, libraryIcons, mappableDefinitions, resolveIconSource, generatedModulePath, generatedNamesOf, generatedOrigins, iconModulePackage, sameIconModule, iconExportName, iconNaming, modulePrefix, moduleNaming, iconNamingWithoutLook, splitLook, setModuleNote, variantExportName, writeGeneratedIcons, writeIconSetData, definitionNameOf, iconIdIn, iconsWithWord, localSourceNote, THEME_MAP_NAME } from "../../foundation/discovery/icon-set-writer.mjs";
55
58
  export { addIconDefinition, addIconSet } from "./_authoring.mjs";
56
59
  export { checkVariantLook, drawnLooks } from "./_default-look.mjs";
@@ -34,7 +34,14 @@ export {
34
34
  } from '../../foundation/discovery/icon-ranking.mjs';
35
35
  export {astryxIconDefinitions} from '../../foundation/discovery/icon-definition-discovery.mjs';
36
36
  export {iconSetKey} from '../../foundation/discovery/icon-set-discovery.mjs';
37
- export {setConfigString} from '../../foundation/config/config-writer.mjs';
37
+ export {
38
+ setConfigString,
39
+ setConfigValue,
40
+ } from '../../foundation/config/config-writer.mjs';
41
+ export {
42
+ ICON_USE_ORDER_COMMAND,
43
+ unlistedIconSets,
44
+ } from '../../foundation/discovery/icon-set-discovery.mjs';
38
45
  export {
39
46
  assertIconExport,
40
47
  checkNewDefinition,
@@ -177,9 +184,10 @@ function findProjectDir(cwd) {
177
184
  * `experimental.iconSet`, read from the project's package.json; null when
178
185
  * nothing says so.
179
186
  * @param {string} projectDir
187
+ * @param {{list?: boolean}} [options] `list`: the value written is a list
180
188
  * @returns {string | null}
181
189
  */
182
- export function iconSetConfigCliWarning(projectDir) {
190
+ export function iconSetConfigCliWarning(projectDir, options = {}) {
183
191
  let pkg;
184
192
  try {
185
193
  pkg = JSON.parse(
@@ -188,5 +196,5 @@ export function iconSetConfigCliWarning(projectDir) {
188
196
  } catch {
189
197
  return null;
190
198
  }
191
- return projectIconSetCliWarning(pkg);
199
+ return projectIconSetCliWarning(pkg, options);
192
200
  }
@@ -128,6 +128,14 @@ export type IconSetEntry = {
128
128
  * - Whether this is the project's icon set.
129
129
  */
130
130
  project: boolean;
131
+ /**
132
+ * - The set's place in the project's list: its main set, a backup, or not listed.
133
+ */
134
+ role: "main" | "backup" | null;
135
+ /**
136
+ * - The set's position in the list, the main set at 1; null when it is not listed.
137
+ */
138
+ rank: number | null;
131
139
  };
132
140
  /**
133
141
  * Which icon definitions one set's `map` covers.
@@ -170,6 +178,13 @@ export type IconListResponse = {
170
178
  status: "selected" | "none" | "ambiguous" | "missing" | "withdrawn";
171
179
  projectSet: string | null;
172
180
  reason: string;
181
+ order: string[];
182
+ backups: Array<{
183
+ iconSet: string;
184
+ status: "selected" | "ambiguous" | "missing" | "withdrawn";
185
+ reason: string;
186
+ }>;
187
+ next?: string | undefined;
173
188
  sets: IconSetEntry[];
174
189
  withdrawn: IconWithdrawnEntry[];
175
190
  };
@@ -182,10 +197,16 @@ export type IconUseResponse = {
182
197
  data: {
183
198
  iconSet: string;
184
199
  set: string;
200
+ backups: Array<{
201
+ iconSet: string;
202
+ set: string;
203
+ }>;
185
204
  config: string;
186
205
  created: boolean;
187
- previous: string | null;
206
+ previous: string | string[] | null;
188
207
  changed: boolean;
208
+ unlisted: string[];
209
+ next?: string | undefined;
189
210
  warning?: string | undefined;
190
211
  };
191
212
  };
@@ -84,6 +84,8 @@
84
84
  * @property {string[] | null} variants - The variants the set declares: named looks such as outline or solid; size comes from CSS.
85
85
  * @property {IconSetCoverage} coverage - Which icon definitions the set's `map` covers.
86
86
  * @property {boolean} project - Whether this is the project's icon set.
87
+ * @property {'main' | 'backup' | null} role - The set's place in the project's list: its main set, a backup, or not listed.
88
+ * @property {number | null} rank - The set's position in the list, the main set at 1; null when it is not listed.
87
89
  */
88
90
 
89
91
  /**
@@ -113,7 +115,10 @@
113
115
  * @property {'selected' | 'none' | 'ambiguous' | 'missing' | 'withdrawn'} data.status - The project's icon-set state.
114
116
  * @property {string | null} data.projectSet - The project's set, as `<package>/<set>`, when selected.
115
117
  * @property {string} data.reason - Why the project has that set, or none.
116
- * @property {IconSetEntry[]} data.sets - Installed sets, the project's first.
118
+ * @property {string[]} data.order - The project's list as `<package>/<set>`: the main set, then each backup that resolves; empty when no main set is selected.
119
+ * @property {Array<{iconSet: string, status: 'selected' | 'ambiguous' | 'missing' | 'withdrawn', reason: string}>} data.backups - Each backup the list names, and whether it resolves.
120
+ * @property {string} [data.next] - Present when two or more sets are installed and none is the main set: the command that chooses the order.
121
+ * @property {IconSetEntry[]} data.sets - Installed sets in list order: the main set, its backups, then the rest.
117
122
  * @property {IconWithdrawnEntry[]} data.withdrawn - Sets found but withdrawn.
118
123
  */
119
124
 
@@ -123,12 +128,15 @@
123
128
  * @typedef {object} IconUseResponse
124
129
  * @property {'icon.use'} type
125
130
  * @property {object} data
126
- * @property {string} data.iconSet - The value written to `experimental.iconSet`.
127
- * @property {string} data.set - The set it names, as `<package>/<set>`.
131
+ * @property {string} data.iconSet - The main set, as written to `experimental.iconSet` (alone, or first in the list).
132
+ * @property {string} data.set - The main set it names, as `<package>/<set>`.
133
+ * @property {Array<{iconSet: string, set: string}>} data.backups - The backup sets, in order, as written and as `<package>/<set>`; empty when there are none.
128
134
  * @property {string} data.config - The config file, relative to the project.
129
135
  * @property {boolean} data.created - Whether the config file was created.
130
- * @property {string | null} data.previous - The `experimental.iconSet` value it replaced.
136
+ * @property {string | string[] | null} data.previous - The `experimental.iconSet` value it replaced.
131
137
  * @property {boolean} data.changed - Whether anything was written.
138
+ * @property {string[]} data.unlisted - Installed sets the list leaves out, as `<package>/<set>`. Doctor fails a project that installs more than one set and leaves one out.
139
+ * @property {string} [data.next] - Present when `unlisted` is not empty: the command that adds them as backups.
132
140
  * @property {string} [data.warning] - Present when the project's
133
141
  * `@astryxdesign/cli` range admits a release before 0.7.4, which refuses
134
142
  * an astryx.config that sets `experimental.iconSet`: what fails, and the
@@ -10,13 +10,21 @@ export const doc = {
10
10
  namespace: 'cli/api',
11
11
  stability: 'experimental',
12
12
  displayName: 'iconUse()',
13
- summary: "Choose the project's icon set.",
13
+ summary: "Choose the project's main icon set and backups.",
14
14
  description:
15
- "Writes `experimental.iconSet` in the project's astryx.config (creating astryx.config.mjs when there is none), so templates and blocks the project copies use that set's icons. The set must be installed; a name two installed packages both ship is refused with each `<package>/<set>` form. Experimental: it may change or be removed in a patch release.",
15
+ "Writes `experimental.iconSet` in the project's astryx.config (creating astryx.config.mjs when there is none), so templates and blocks the project copies use that set's icons. With no backups it writes the set name as a string. With backups it writes a list, the main set first and then each backup in order: a copied template takes each icon from the first set in the list that has its meaning, and draws a placeholder the copied file declares when none has it. Every set must be installed; a name two installed packages both ship is refused with each `<package>/<set>` form. The receipt lists installed sets the list leaves out (`unlisted`), which Doctor fails, with the command that adds them. Experimental: it may change or be removed in a patch release.",
16
16
  importPath: '@astryxdesign/cli/api',
17
17
  signature:
18
- 'iconUse(set: string, options?: {cwd?: string}): Promise<IconUseResponse>',
19
- keywords: ['icon', 'icon set', 'use', 'experimental.iconSet', 'config'],
18
+ 'iconUse(set: string, options?: {cwd?: string, backups?: string[]}): Promise<IconUseResponse>',
19
+ keywords: [
20
+ 'icon',
21
+ 'icon set',
22
+ 'use',
23
+ 'backup',
24
+ 'main set',
25
+ 'experimental.iconSet',
26
+ 'config',
27
+ ],
20
28
  params: [
21
29
  {
22
30
  name: 'set',
@@ -29,12 +37,18 @@ export const doc = {
29
37
  type: 'string',
30
38
  description: 'The project directory.',
31
39
  },
40
+ {
41
+ name: 'options.backups',
42
+ type: 'string[]',
43
+ description:
44
+ 'Backup sets, by name or `<package>/<set>`, in the order a copied template tries them after the main set.',
45
+ },
32
46
  ],
33
47
  returns: [
34
48
  {
35
49
  type: 'icon.use',
36
50
  description:
37
- "The value written, the set it names, the config file, whether it was created, the value it replaced, and a `warning` when the project's @astryxdesign/cli range admits a release before 0.7.4, which refuses the key.",
51
+ "The main set and the backups written, the sets they name, the config file, whether it was created, the value it replaced, the installed sets the list leaves out (`unlisted`, with a `next` command), and a `warning` when the project's @astryxdesign/cli range admits a release that refuses the value: before 0.7.4 for a string, before 0.7.12 for a list.",
38
52
  },
39
53
  ],
40
54
  throws: [
@@ -44,7 +58,7 @@ export const doc = {
44
58
  },
45
59
  {
46
60
  code: 'ERR_INVALID_ARGUMENT',
47
- when: 'two installed packages ship a set with the name',
61
+ when: 'two installed packages ship a set with the name, or the list names one set twice',
48
62
  },
49
63
  {
50
64
  code: 'ERR_WRITE_FAILED',
@@ -67,7 +81,13 @@ export const doc = {
67
81
  when: 'the set name is missing',
68
82
  },
69
83
  ],
70
- examples: [{label: 'Use a set', code: "await iconUse('outline');"}],
84
+ examples: [
85
+ {label: 'Use a set', code: "await iconUse('outline');"},
86
+ {
87
+ label: 'Use a main set with a backup',
88
+ code: "await iconUse('outline', {backups: ['@acme/brand-icons/brand']});",
89
+ },
90
+ ],
71
91
  command: 'experimental icon use',
72
92
  related: ['icon', 'iconMap', 'iconAdd'],
73
93
  };
@@ -2,14 +2,19 @@
2
2
 
3
3
  /**
4
4
  * @file `icon.list` leaf (AST-084 FR7): every installed icon set with the
5
- * icon definitions its `map` covers, the project's set, and the sets
6
- * withdrawn with why.
5
+ * icon definitions its `map` covers, the project's main set and backups in
6
+ * order, and the sets withdrawn with why.
7
7
  *
8
8
  * @output the `icon.list` envelope
9
9
  * @position api/icon/list — dispatched by ../icon.mjs
10
10
  */
11
11
 
12
- import {astryxIconDefinitions, loadIconSets} from '../_adapter.mjs';
12
+ import {
13
+ astryxIconDefinitions,
14
+ ICON_USE_ORDER_COMMAND,
15
+ iconSetKey,
16
+ loadIconSets,
17
+ } from '../_adapter.mjs';
13
18
 
14
19
  /**
15
20
  * List the installed icon sets.
@@ -20,6 +25,8 @@ export async function iconList({cwd}) {
20
25
  const {sets, withdrawn, choice} = await loadIconSets(cwd);
21
26
  const astryx = astryxIconDefinitions().definitions.map(each => each.doc);
22
27
  const builtIn = astryx.filter(doc => doc.builtIn).map(doc => doc.name);
28
+ const order = choice.status === 'selected' ? (choice.order ?? []) : [];
29
+ const unchosen = choice.status === 'ambiguous' && !choice.configured;
23
30
  return {
24
31
  type: 'icon.list',
25
32
  data: {
@@ -28,7 +35,19 @@ export async function iconList({cwd}) {
28
35
  ? `${choice.set.package}/${choice.set.name}`
29
36
  : null,
30
37
  reason: choice.reason,
38
+ order: order.map(iconSetKey),
39
+ backups: (choice.backups ?? []).map(each => ({
40
+ iconSet: each.selector,
41
+ status: each.status,
42
+ reason: each.reason,
43
+ })),
44
+ ...(unchosen
45
+ ? {
46
+ next: `${sets.length} icon sets are installed and none is your main set: run \`${ICON_USE_ORDER_COMMAND}\`.`,
47
+ }
48
+ : {}),
31
49
  sets: sets.map(set => {
50
+ const rank = order.indexOf(set);
32
51
  const mapped = new Set(Object.keys(set.data.map ?? {}));
33
52
  return {
34
53
  name: set.name,
@@ -52,6 +71,13 @@ export async function iconList({cwd}) {
52
71
  own: set.definitions.map(each => each.doc.name),
53
72
  },
54
73
  project: set === choice.set,
74
+ role:
75
+ rank === 0
76
+ ? /** @type {const} */ ('main')
77
+ : rank > 0
78
+ ? /** @type {const} */ ('backup')
79
+ : null,
80
+ rank: rank === -1 ? null : rank + 1,
55
81
  };
56
82
  }),
57
83
  withdrawn: withdrawn.map(each => ({