@astryxdesign/cli 0.4.6 → 0.4.7-canary.019ae5a

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 (73) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +53 -51
  3. package/api/docs/docs.doc.mjs +2 -2
  4. package/api/index.d.mts +1 -1
  5. package/api/index.mjs +1 -1
  6. package/api/search/search.mjs +50 -4
  7. package/api/search/search.test.mjs +71 -0
  8. package/api/template/template.doc.mjs +2 -2
  9. package/api/theme/build/build.mjs +8 -38
  10. package/api/theme/targets/targets.d.mts +18 -0
  11. package/api/theme/targets/targets.mjs +87 -0
  12. package/api/theme/targets/targets.test.mjs +65 -0
  13. package/api/theme/theme.d.mts +1 -0
  14. package/api/theme/theme.mjs +3 -1
  15. package/api/theme/theme.type.d.mts +23 -0
  16. package/api/theme/theme.type.mjs +21 -1
  17. package/api/theme/themeTargets.doc.d.mts +11 -0
  18. package/api/theme/themeTargets.doc.mjs +58 -0
  19. package/api/theme/themeTemplate.doc.mjs +3 -3
  20. package/assets/codemods/transforms/next/__tests__/next-codemods.test.mjs +127 -0
  21. package/assets/codemods/transforms/next/banner-collapsible-content.mjs +171 -0
  22. package/assets/codemods/transforms/next/index.mjs +11 -1
  23. package/assets/docs/README.md +50 -0
  24. package/assets/docs/cli-integrations.doc.mjs +4 -4
  25. package/assets/docs/theme.doc.dense.mjs +1 -1
  26. package/assets/docs/theme.doc.mjs +1 -1
  27. package/assets/docs/typography.doc.mjs +2 -2
  28. package/assets/templates/blocks/components/Banner/BannerCollapsibleContent.doc.mjs +1 -1
  29. package/assets/templates/blocks/components/Banner/BannerCollapsibleContent.tsx +1 -1
  30. package/assets/templates/blocks/components/Dialog/DialogAdaptivePresentation.doc.mjs +25 -0
  31. package/assets/templates/blocks/components/Dialog/DialogAdaptivePresentation.tsx +167 -0
  32. package/assets/templates/blocks/components/Dialog/DialogScrollingContent.tsx +1 -1
  33. package/assets/templates/blocks/components/Step/StepContent.doc.mjs +14 -0
  34. package/assets/templates/blocks/components/Step/StepContent.tsx +32 -0
  35. package/assets/templates/blocks/components/Step/StepIndicator.doc.mjs +14 -0
  36. package/assets/templates/blocks/components/Step/StepIndicator.tsx +60 -0
  37. package/assets/templates/blocks/components/Step/StepShowcase.doc.mjs +15 -0
  38. package/assets/templates/blocks/components/Step/StepShowcase.tsx +26 -0
  39. package/assets/templates/blocks/components/Step/StepStates.doc.mjs +14 -0
  40. package/assets/templates/blocks/components/Step/StepStates.tsx +46 -0
  41. package/assets/templates/blocks/components/Stepper/StepperCustomContent.doc.mjs +22 -0
  42. package/assets/templates/blocks/components/Stepper/StepperCustomContent.tsx +126 -0
  43. package/assets/templates/blocks/components/Stepper/StepperIndicatorModes.doc.mjs +1 -1
  44. package/assets/templates/blocks/components/Stepper/StepperIndicatorModes.tsx +17 -5
  45. package/assets/templates/blocks/components/Stepper/StepperOnTrackHorizontal.doc.mjs +14 -0
  46. package/assets/templates/blocks/components/Stepper/StepperOnTrackHorizontal.tsx +25 -0
  47. package/assets/templates/blocks/components/Stepper/StepperOnTrackVertical.doc.mjs +2 -2
  48. package/assets/templates/blocks/components/Stepper/StepperOnTrackVertical.tsx +1 -1
  49. package/assets/templates/blocks/components/Stepper/StepperShowcase.doc.mjs +1 -1
  50. package/assets/templates/blocks/components/Stepper/StepperShowcase.tsx +6 -7
  51. package/assets/templates/blocks/components/Stepper/StepperStatus.tsx +1 -1
  52. package/assets/templates/pages/mixed-gallery/page.tsx +12 -3
  53. package/assets/templates/pages/table-grouped/page.tsx +151 -144
  54. package/assets/templates/themes/neutral/neutralTheme.ts +13 -8
  55. package/clients/cli/commands/build-theme.mjs +85 -0
  56. package/clients/cli/commands/dialog-adaptive-template.test.mjs +24 -0
  57. package/clients/cli/commands/theme-targets.behavior.test.mjs +64 -0
  58. package/clients/cli/commands/theme-targets.doc.mjs +38 -0
  59. package/clients/cli/commands/theme-template.doc.mjs +2 -2
  60. package/clients/cli/commands/theme.doc.mjs +4 -2
  61. package/clients/cli/index.mjs +1 -0
  62. package/clients/cli/lib/manifest.mjs +2 -0
  63. package/foundation/discovery/theming-targets.d.mts +86 -0
  64. package/foundation/discovery/theming-targets.mjs +202 -0
  65. package/foundation/discovery/theming-targets.test.mjs +245 -0
  66. package/foundation/response/response-types.doc.mjs +7 -2
  67. package/package.json +9 -9
  68. package/assets/templates/blocks/components/Stepper/StepperHorizontal.doc.mjs +0 -14
  69. package/assets/templates/blocks/components/Stepper/StepperHorizontal.tsx +0 -24
  70. package/assets/templates/blocks/components/Stepper/StepperMultiStepForm.doc.mjs +0 -14
  71. package/assets/templates/blocks/components/Stepper/StepperMultiStepForm.tsx +0 -92
  72. package/assets/templates/blocks/components/Stepper/StepperVerticalOnboarding.doc.mjs +0 -14
  73. package/assets/templates/blocks/components/Stepper/StepperVerticalOnboarding.tsx +0 -40
@@ -0,0 +1,14 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('@astryxdesign/cli/authoring').TemplateDoc} */
4
+ export const doc = {
5
+ type: 'block',
6
+ exampleFor: 'Stepper',
7
+ name: 'Stepper — On-Track Horizontal',
8
+ displayName: 'Stepper — On-Track Horizontal',
9
+ description:
10
+ 'The on-track layout in horizontal orientation: instead of sitting beside the label, each indicator is slotted into the connector itself, so the numbered nodes read as beads on one continuous line. Labels center under their node.',
11
+ isReady: true,
12
+ aspectRatio: 16 / 9,
13
+ componentsUsed: ['Stepper'],
14
+ };
@@ -0,0 +1,25 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ import {useState} from 'react';
6
+ import {Stepper, Step} from '@astryxdesign/core/Stepper';
7
+
8
+ export default function StepperOnTrackHorizontal() {
9
+ const [active, setActive] = useState(2);
10
+ return (
11
+ <div style={{width: 560}}>
12
+ <Stepper
13
+ activeStep={active}
14
+ orientation="horizontal"
15
+ indicatorPosition="on-track"
16
+ onStepClick={setActive}>
17
+ <Step step={0} label="Workspace" indicator="number" />
18
+ <Step step={1} label="Team" indicator="number" />
19
+ <Step step={2} label="Integrations" indicator="number" />
20
+ <Step step={3} label="Import" indicator="number" />
21
+ <Step step={4} label="Launch" indicator="number" />
22
+ </Stepper>
23
+ </div>
24
+ );
25
+ }
@@ -7,8 +7,8 @@ export const doc = {
7
7
  name: 'Stepper — On-Track Vertical',
8
8
  displayName: 'Stepper — On-Track Vertical',
9
9
  description:
10
- 'The on-track layout in vertical orientation: indicators sit inline on a continuous connector rail, with each label and description beside its node.',
10
+ 'The on-track layout in vertical orientation: indicators sit inline on a continuous connector rail, with each label and description beside its node. Compare with the separated layout, where the indicator sits alongside its own bar segment instead of on a shared rail.',
11
11
  isReady: true,
12
12
  aspectRatio: 4 / 3,
13
- componentsUsed: ['Stepper'],
13
+ componentsUsed: ['Stepper', 'Step'],
14
14
  };
@@ -3,7 +3,7 @@
3
3
  'use client';
4
4
 
5
5
  import {useState} from 'react';
6
- import {Stepper, Step} from '@astryxdesign/lab';
6
+ import {Stepper, Step} from '@astryxdesign/core/Stepper';
7
7
 
8
8
  export default function StepperOnTrackVertical() {
9
9
  const [active, setActive] = useState(2);
@@ -7,7 +7,7 @@ export const doc = {
7
7
  name: 'Stepper — Checkout Progress',
8
8
  displayName: 'Stepper — Checkout Progress',
9
9
  description:
10
- 'A horizontal on-track stepper for a checkout flow: numbered nodes sit on a continuous connector, with completed and current steps filled. Click any step to jump.',
10
+ 'The default stepper: a horizontal track where every step owns an equal segment of the progress bar above its label. The default auto indicator resolves itself per step — a check once the step is done, a ring on the current step, a number for the ones still ahead. Click any step to jump.',
11
11
  isReady: true,
12
12
  isShowcase: true,
13
13
  aspectRatio: 16 / 9,
@@ -3,7 +3,7 @@
3
3
  'use client';
4
4
 
5
5
  import {useState} from 'react';
6
- import {Stepper, Step} from '@astryxdesign/lab';
6
+ import {Stepper, Step} from '@astryxdesign/core/Stepper';
7
7
 
8
8
  export default function StepperShowcase() {
9
9
  const [active, setActive] = useState(2);
@@ -12,13 +12,12 @@ export default function StepperShowcase() {
12
12
  <Stepper
13
13
  activeStep={active}
14
14
  orientation="horizontal"
15
- indicatorPosition="on-track"
16
15
  onStepClick={setActive}>
17
- <Step step={0} label="Cart" indicator="number" />
18
- <Step step={1} label="Shipping" indicator="number" />
19
- <Step step={2} label="Payment" indicator="number" />
20
- <Step step={3} label="Review" indicator="number" />
21
- <Step step={4} label="Confirm" indicator="number" />
16
+ <Step step={0} label="Cart" />
17
+ <Step step={1} label="Shipping" />
18
+ <Step step={2} label="Payment" />
19
+ <Step step={3} label="Review" />
20
+ <Step step={4} label="Confirm" />
22
21
  </Stepper>
23
22
  </div>
24
23
  );
@@ -3,7 +3,7 @@
3
3
  'use client';
4
4
 
5
5
  import {useState} from 'react';
6
- import {Stepper, Step} from '@astryxdesign/lab';
6
+ import {Stepper, Step} from '@astryxdesign/core/Stepper';
7
7
 
8
8
  export default function StepperStatus() {
9
9
  const [active, setActive] = useState(3);
@@ -36,7 +36,11 @@ const clipStyle: CSSProperties = {
36
36
  // 3 columns on desktop, dropping straight to 1 column below 720px (no 2-col
37
37
  // middle state). minmax(0, 1fr) (not 1fr) so tracks split evenly and ignore the
38
38
  // images' intrinsic min-width. The hero spans 2 columns on desktop, then fills
39
- // the row once it's single-column.
39
+ // the row once it's single-column — and switches from 3:1 to 3:2 with a plain
40
+ // aspect-ratio override. AspectRatio's ratio is a class-level declaration in
41
+ // @layer astryx-base, and this <style> tag is unlayered, so the override wins
42
+ // regardless of specificity — every stacked tile is uniform without rendering
43
+ // the hero twice.
40
44
  const GALLERY_CSS = `
41
45
  .mixed-gallery-grid {
42
46
  display: grid;
@@ -52,6 +56,7 @@ const GALLERY_CSS = `
52
56
  }
53
57
  .mixed-gallery-hero {
54
58
  grid-column: 1 / -1;
59
+ aspect-ratio: 3 / 2;
55
60
  }
56
61
  }
57
62
  `;
@@ -134,9 +139,13 @@ export default function MixedGalleryTemplate() {
134
139
  of three. Every tile is 3:2 except the hero, which is 3:1 so that
135
140
  (being 2 columns wide) it matches the row height exactly. All
136
141
  rows are therefore the same height. Responsive via @container:
137
- 3 columns → 1 column at ≤720px. */}
142
+ 3 columns → 1 column at ≤720px, where the hero drops to 3:2 so
143
+ the stacked tiles are uniform. */}
138
144
  <div className="mixed-gallery-grid">
139
- {/* Hero — spans 2 columns; 3:1 keeps it level with the sidebar */}
145
+ {/* Hero — spans 2 columns; 3:1 keeps it level with the sidebar.
146
+ Stacked, the unlayered aspect-ratio override switches it to
147
+ 3:2 (see GALLERY_CSS) — one element, no duplicate hero
148
+ markup. */}
140
149
  <GalleryCard
141
150
  image={IMAGES[0]}
142
151
  ratio={3 / 1}
@@ -37,6 +37,7 @@ import {Divider} from '@astryxdesign/core/Divider';
37
37
  import {MetadataList, MetadataListItem} from '@astryxdesign/core/MetadataList';
38
38
  import {
39
39
  Table,
40
+ TableBody,
40
41
  TableRow,
41
42
  TableCell,
42
43
  proportional,
@@ -963,161 +964,167 @@ export default function DataTableTemplate() {
963
964
  />
964
965
  ))}
965
966
  </colgroup>
966
- {groupKeys.map(key => {
967
- const tasks = grouped.get(key);
968
- if (!tasks || tasks.length === 0) {
969
- return null;
970
- }
971
- const isExpanded = expandedGroups.has(key);
967
+ <TableBody>
968
+ {groupKeys.map(key => {
969
+ const tasks = grouped.get(key);
970
+ if (!tasks || tasks.length === 0) {
971
+ return null;
972
+ }
973
+ const isExpanded = expandedGroups.has(key);
972
974
 
973
- return (
974
- <React.Fragment key={key}>
975
- {groupBy !== 'none' && (
976
- <TableRow
977
- role="button"
978
- tabIndex={0}
979
- onClick={() => toggleGroup(key)}
980
- onKeyDown={e => {
981
- if (e.key === 'Enter' || e.key === ' ') {
982
- e.preventDefault();
983
- toggleGroup(key);
984
- }
985
- }}>
986
- <TableCell colSpan={COL_COUNT} style={groupHeaderCell}>
987
- <HStack gap={2} vAlign="center">
988
- <Icon
989
- icon={
990
- isExpanded ? ChevronDownIcon : ChevronRightIcon
991
- }
992
- size="sm"
993
- color="secondary"
994
- />
995
- <Text type="body" weight="bold">
996
- {getGroupLabel(groupBy, key)}
997
- </Text>
998
- <Badge
999
- variant="neutral"
1000
- label={String(tasks.length)}
1001
- />
1002
- </HStack>
1003
- </TableCell>
1004
- </TableRow>
1005
- )}
1006
- {(groupBy === 'none' || isExpanded) &&
1007
- tasks.map(task => (
975
+ return (
976
+ <React.Fragment key={key}>
977
+ {groupBy !== 'none' && (
1008
978
  <TableRow
1009
- key={task.id}
1010
- onClick={() => setSelectedTask(task)}>
1011
- <TableCell>
1012
- <Center axis="horizontal">
1013
- <StatusDot
1014
- variant={STATUS_DOT_VARIANT[task.status]}
1015
- label={STATUS_LABEL[task.status]}
1016
- />
1017
- </Center>
1018
- </TableCell>
1019
- <TableCell>
1020
- <HStack gap={3} vAlign="center">
979
+ role="button"
980
+ tabIndex={0}
981
+ onClick={() => toggleGroup(key)}
982
+ onKeyDown={e => {
983
+ if (e.key === 'Enter' || e.key === ' ') {
984
+ e.preventDefault();
985
+ toggleGroup(key);
986
+ }
987
+ }}>
988
+ <TableCell
989
+ colSpan={COL_COUNT}
990
+ style={groupHeaderCell}>
991
+ <HStack gap={2} vAlign="center">
1021
992
  <Icon
1022
- icon={ChartBarIcon}
993
+ icon={
994
+ isExpanded
995
+ ? ChevronDownIcon
996
+ : ChevronRightIcon
997
+ }
1023
998
  size="sm"
1024
- color={PRIORITY_COLOR[task.priority]}
999
+ color="secondary"
1025
1000
  />
1026
- <Text type="supporting" color="secondary">
1027
- {task.taskId}
1001
+ <Text type="body" weight="bold">
1002
+ {getGroupLabel(groupBy, key)}
1028
1003
  </Text>
1029
- <Text type="body" maxLines={1}>
1030
- {task.title}
1031
- </Text>
1032
- {task.subtitle && (
1033
- <Text
1034
- type="body"
1035
- color="secondary"
1036
- maxLines={1}>
1037
- › {task.subtitle}
1038
- </Text>
1039
- )}
1004
+ <Badge
1005
+ variant="neutral"
1006
+ label={String(tasks.length)}
1007
+ />
1040
1008
  </HStack>
1041
1009
  </TableCell>
1042
- <TableCell>
1043
- {task.project ? (
1044
- <Text type="body" maxLines={1}>
1045
- {task.project}
1010
+ </TableRow>
1011
+ )}
1012
+ {(groupBy === 'none' || isExpanded) &&
1013
+ tasks.map(task => (
1014
+ <TableRow
1015
+ key={task.id}
1016
+ onClick={() => setSelectedTask(task)}>
1017
+ <TableCell>
1018
+ <Center axis="horizontal">
1019
+ <StatusDot
1020
+ variant={STATUS_DOT_VARIANT[task.status]}
1021
+ label={STATUS_LABEL[task.status]}
1022
+ />
1023
+ </Center>
1024
+ </TableCell>
1025
+ <TableCell>
1026
+ <HStack gap={3} vAlign="center">
1027
+ <Icon
1028
+ icon={ChartBarIcon}
1029
+ size="sm"
1030
+ color={PRIORITY_COLOR[task.priority]}
1031
+ />
1032
+ <Text type="supporting" color="secondary">
1033
+ {task.taskId}
1034
+ </Text>
1035
+ <Text type="body" maxLines={1}>
1036
+ {task.title}
1037
+ </Text>
1038
+ {task.subtitle && (
1039
+ <Text
1040
+ type="body"
1041
+ color="secondary"
1042
+ maxLines={1}>
1043
+ › {task.subtitle}
1044
+ </Text>
1045
+ )}
1046
+ </HStack>
1047
+ </TableCell>
1048
+ <TableCell>
1049
+ {task.project ? (
1050
+ <Text type="body" maxLines={1}>
1051
+ {task.project}
1052
+ </Text>
1053
+ ) : (
1054
+ <Text type="supporting" color="secondary">
1055
+
1056
+ </Text>
1057
+ )}
1058
+ </TableCell>
1059
+ <TableCell>
1060
+ <Text type="supporting" color="secondary">
1061
+ {task.created}
1046
1062
  </Text>
1047
- ) : (
1063
+ </TableCell>
1064
+ <TableCell>
1048
1065
  <Text type="supporting" color="secondary">
1049
-
1066
+ {task.updated}
1050
1067
  </Text>
1051
- )}
1052
- </TableCell>
1053
- <TableCell>
1054
- <Text type="supporting" color="secondary">
1055
- {task.created}
1056
- </Text>
1057
- </TableCell>
1058
- <TableCell>
1059
- <Text type="supporting" color="secondary">
1060
- {task.updated}
1061
- </Text>
1062
- </TableCell>
1063
- <TableCell>
1064
- <Avatar name={task.assignee} size="sm" />
1065
- </TableCell>
1066
- <TableCell>
1067
- <DropdownMenu
1068
- button={{
1069
- label: 'Actions',
1070
- variant: 'ghost',
1071
- size: 'sm',
1072
- icon: (
1073
- <Icon
1074
- icon={EllipsisHorizontalIcon}
1075
- size="sm"
1076
- />
1077
- ),
1078
- isIconOnly: true,
1079
- }}
1080
- hasChevron={false}
1081
- items={[
1082
- {
1083
- label: 'Edit issue',
1084
- icon: PencilIcon,
1085
- onClick: () => {},
1086
- },
1087
- {
1088
- label: 'Assign to...',
1089
- icon: UserIcon,
1090
- onClick: () => {},
1091
- },
1092
- {
1093
- label: 'Add label',
1094
- icon: TagIcon,
1095
- onClick: () => {},
1096
- },
1097
- {
1098
- label: 'Duplicate',
1099
- icon: DocumentDuplicateIcon,
1100
- onClick: () => {},
1101
- },
1102
- {
1103
- label: 'Move to project',
1104
- icon: ArrowRightIcon,
1105
- onClick: () => {},
1106
- },
1107
- {type: 'divider' as const},
1108
- {
1109
- label: 'Delete issue',
1110
- icon: TrashIcon,
1111
- onClick: () => {},
1112
- },
1113
- ]}
1114
- />
1115
- </TableCell>
1116
- </TableRow>
1117
- ))}
1118
- </React.Fragment>
1119
- );
1120
- })}
1068
+ </TableCell>
1069
+ <TableCell>
1070
+ <Avatar name={task.assignee} size="sm" />
1071
+ </TableCell>
1072
+ <TableCell>
1073
+ <DropdownMenu
1074
+ button={{
1075
+ label: 'Actions',
1076
+ variant: 'ghost',
1077
+ size: 'sm',
1078
+ icon: (
1079
+ <Icon
1080
+ icon={EllipsisHorizontalIcon}
1081
+ size="sm"
1082
+ />
1083
+ ),
1084
+ isIconOnly: true,
1085
+ }}
1086
+ hasChevron={false}
1087
+ items={[
1088
+ {
1089
+ label: 'Edit issue',
1090
+ icon: PencilIcon,
1091
+ onClick: () => {},
1092
+ },
1093
+ {
1094
+ label: 'Assign to...',
1095
+ icon: UserIcon,
1096
+ onClick: () => {},
1097
+ },
1098
+ {
1099
+ label: 'Add label',
1100
+ icon: TagIcon,
1101
+ onClick: () => {},
1102
+ },
1103
+ {
1104
+ label: 'Duplicate',
1105
+ icon: DocumentDuplicateIcon,
1106
+ onClick: () => {},
1107
+ },
1108
+ {
1109
+ label: 'Move to project',
1110
+ icon: ArrowRightIcon,
1111
+ onClick: () => {},
1112
+ },
1113
+ {type: 'divider' as const},
1114
+ {
1115
+ label: 'Delete issue',
1116
+ icon: TrashIcon,
1117
+ onClick: () => {},
1118
+ },
1119
+ ]}
1120
+ />
1121
+ </TableCell>
1122
+ </TableRow>
1123
+ ))}
1124
+ </React.Fragment>
1125
+ );
1126
+ })}
1127
+ </TableBody>
1121
1128
  </Table>
1122
1129
  </LayoutContent>
1123
1130
  }
@@ -24,7 +24,8 @@
24
24
  * icon = T30 / T80
25
25
  * text = T30 / T80
26
26
  *
27
- * All 9 saturated badge values pass WCAG AA (5.6–9.6 contrast range).
27
+ * All 9 saturated badge values pass WCAG AA against their label (>= 4.5:1);
28
+ * `scripts/check-badge-contrast.test.mjs` holds every theme to that.
28
29
  *
29
30
  * Only overrides tokens that differ from the defaults.
30
31
  */
@@ -432,10 +433,14 @@ export const neutralTheme = defineTheme({
432
433
  color: '#171717',
433
434
  },
434
435
  'variant:error': {
435
- // Light: T55 #e33f4a (palette saturated stop)
436
+ // Light: T58 #c9303a. The T55 stop #e33f4a pairs with white at only
437
+ // 4.14:1 — the label is 12px/500, so AA wants 4.5, not the 3:1
438
+ // large-text allowance. One tonal step down holds the hue
439
+ // (OKLCH H 21.9 -> 22.8, C 0.200 -> 0.189) and reaches 5.29:1.
436
440
  // Dark : T60 stop from dark-mode tonal palette of Tailwind red-600
437
- // source #dc2626 (kept on H=27 alarm-red rather than coral)
438
- backgroundColor: 'light-dark(#e33f4a, #ff705d)',
441
+ // source #dc2626 (kept on H=27 alarm-red rather than coral).
442
+ // Dark text on it is 6.60:1 and unchanged.
443
+ backgroundColor: 'light-dark(#c9303a, #ff705d)',
439
444
  color: 'light-dark(#ffffff, #171717)',
440
445
  },
441
446
 
@@ -500,7 +505,7 @@ export const neutralTheme = defineTheme({
500
505
  //
501
506
  // success → badge success bg (green T45 / dark-ramp T60)
502
507
  // warning → badge warning bg (yellow T85, same hex both modes)
503
- // error → badge error bg (red T55 / dark-ramp T60)
508
+ // error → badge error bg (red T58 / dark-ramp T60)
504
509
  // accent → badge info bg (blue T50 / dark-ramp T60) — the
505
510
  // StatusDot "accent" is the info/attention color, so it
506
511
  // pairs with the info badge rather than --color-accent
@@ -515,7 +520,7 @@ export const neutralTheme = defineTheme({
515
520
  statusdot: {
516
521
  'variant:success': {backgroundColor: 'light-dark(#198100, #64af4c)'},
517
522
  'variant:warning': {backgroundColor: '#ffce2f'},
518
- 'variant:error': {backgroundColor: 'light-dark(#e33f4a, #ff705d)'},
523
+ 'variant:error': {backgroundColor: 'light-dark(#c9303a, #ff705d)'},
519
524
  'variant:accent': {backgroundColor: 'light-dark(#0074e2, #6d9cfe)'},
520
525
  },
521
526
 
@@ -611,8 +616,8 @@ export const neutralTheme = defineTheme({
611
616
  '--color-warning': '#ffce2f',
612
617
  },
613
618
  'variant:error': {
614
- // Red T55 saturated stop (= variant:error badge bg)
615
- '--color-error': '#e33f4a',
619
+ // Red T58 saturated stop (= variant:error badge bg)
620
+ '--color-error': '#c9303a',
616
621
  },
617
622
  },
618
623
 
@@ -38,6 +38,7 @@ import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
38
38
  import {themeAdd} from '../../../api/theme/add/add.mjs';
39
39
  import {themeTemplate} from '../../../api/theme/template/template.mjs';
40
40
  import {themeList} from '../../../api/theme/list/list.mjs';
41
+ import {themeTargets} from '../../../api/theme/targets/targets.mjs';
41
42
  import {themeBuild, importSpecifier} from '../../../api/theme/build/build.mjs';
42
43
  import {defineCommand} from '../lib/define-command.mjs';
43
44
  import {doc as themeGroup} from './theme.doc.mjs';
@@ -45,10 +46,12 @@ import {doc as themeBuildCommand} from './theme-build.doc.mjs';
45
46
  import {doc as themeListCommand} from './theme-list.doc.mjs';
46
47
  import {doc as themeAddCommand} from './theme-add.doc.mjs';
47
48
  import {doc as themeTemplateCommand} from './theme-template.doc.mjs';
49
+ import {doc as themeTargetsCommand} from './theme-targets.doc.mjs';
48
50
  import {doc as themeBuildFn} from '../../../api/theme/themeBuild.doc.mjs';
49
51
  import {doc as themeListFn} from '../../../api/theme/themeList.doc.mjs';
50
52
  import {doc as themeAddFn} from '../../../api/theme/themeAdd.doc.mjs';
51
53
  import {doc as themeTemplateFn} from '../../../api/theme/themeTemplate.doc.mjs';
54
+ import {doc as themeTargetsFn} from '../../../api/theme/themeTargets.doc.mjs';
52
55
 
53
56
  /**
54
57
  * Path to this CLI's real entry (clients/cli/bin/astryx.mjs), resolved from
@@ -194,6 +197,36 @@ function printThemeList(themes) {
194
197
  );
195
198
  }
196
199
 
200
+ /**
201
+ * Render the targets as one greppable line each, under an aligned header. A
202
+ * `records()` block would be five lines per target — over a thousand for the
203
+ * full surface, which is the view this command exists to make readable.
204
+ * @param {import('../../../api/theme/theme.type.mjs').ThemeTargetEntry[]} targets
205
+ * @returns {string}
206
+ */
207
+ function formatTargetsTable(targets) {
208
+ const rows = targets.map(t => ({
209
+ key: t.key,
210
+ component: t.component,
211
+ props: t.props.join(', ') || '-',
212
+ states: t.states.join(', ') || '-',
213
+ }));
214
+ const head = {key: 'key', component: 'component', props: 'props', states: 'states'};
215
+ const width = (/** @type {'key'|'component'|'props'} */ field) =>
216
+ [head, ...rows].reduce((max, r) => Math.max(max, r[field].length), 0);
217
+ const w = {key: width('key'), component: width('component'), props: width('props')};
218
+ const line = (/** @type {typeof head} */ r) =>
219
+ [
220
+ r.key.padEnd(w.key),
221
+ r.component.padEnd(w.component),
222
+ r.props.padEnd(w.props),
223
+ r.states,
224
+ ]
225
+ .join(' ')
226
+ .trimEnd();
227
+ return [line(head), ...rows.map(line)].join('\n');
228
+ }
229
+
197
230
  /**
198
231
  * @param {import('commander').Command} program
199
232
  */
@@ -225,6 +258,18 @@ export function registerTheme(program) {
225
258
  },
226
259
  });
227
260
 
261
+ // Theming questions are asked at `theme`, but per-component overrides live
262
+ // under `component`. Without this pointer the group reads as a build-tool
263
+ // menu, and the component targets are unreachable from the noun the user
264
+ // started at.
265
+ theme.addHelpText(
266
+ 'after',
267
+ `\nComponent style overrides:\n` +
268
+ ` ${getCliInvocation()} theme targets Every themeable class, with its props and states\n` +
269
+ ` ${getCliInvocation()} component <Name> One component's theming table\n` +
270
+ ` ${getCliInvocation()} docs theme How component overrides work\n`,
271
+ );
272
+
228
273
  defineCommand(theme, themeBuildCommand, {
229
274
  fn: themeBuildFn,
230
275
  action: async (
@@ -502,4 +547,44 @@ export function registerTheme(program) {
502
547
  );
503
548
  },
504
549
  });
550
+
551
+ defineCommand(theme, themeTargetsCommand, {
552
+ fn: themeTargetsFn,
553
+ action: async (/** @type {string | undefined} */ filter) => {
554
+ const json = program.opts().json || false;
555
+
556
+ /** @type {import('../../../api/theme/theme.type.mjs').ThemeTargetsResponse} */
557
+ let result;
558
+ try {
559
+ result = await themeTargets(filter, {cwd: process.cwd()});
560
+ } catch (e) {
561
+ const err =
562
+ /** @type {import('../../../api/error.mjs').AstryxError} */ (e);
563
+ cliError(err.message, {
564
+ suggestions: err.suggestions || [],
565
+ code: err.code,
566
+ });
567
+ return;
568
+ }
569
+
570
+ if (json) return jsonOut(result);
571
+
572
+ const run = getCliInvocation();
573
+ const {targets, componentCount} = result.data;
574
+ emit(
575
+ section(
576
+ 'Theming targets',
577
+ `${targets.length} across ${componentCount} component${componentCount === 1 ? '' : 's'}`,
578
+ ),
579
+ text(formatTargetsTable(targets)),
580
+ text(
581
+ [
582
+ `Each key goes under \`components\` in defineTheme; it paints \`.astryx-<key>\`.`,
583
+ `Props take a value (\`variant:secondary\`); states are written bare (\`checked\`).`,
584
+ `One component in full: ${run} component <Name>`,
585
+ ].join('\n'),
586
+ ),
587
+ );
588
+ },
589
+ });
505
590
  }