@astryxdesign/cli 0.5.0-canary.32a62fd → 0.5.0-canary.38e58e4

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 (28) hide show
  1. package/api/template/data-token-fallbacks.test.mjs +85 -0
  2. package/api/theme/build/build.mjs +31 -2
  3. package/api/theme/themeTargets.doc.mjs +3 -3
  4. package/assets/templates/blocks/components/Step/StepContent.doc.mjs +1 -1
  5. package/assets/templates/blocks/components/Step/StepIndicator.doc.mjs +1 -1
  6. package/assets/templates/blocks/components/Step/StepShowcase.doc.mjs +1 -1
  7. package/assets/templates/blocks/components/Stepper/StepperCustomContent.doc.mjs +1 -1
  8. package/assets/templates/blocks/components/Stepper/StepperShowcase.doc.mjs +1 -1
  9. package/assets/templates/pages/dashboard-cohort-funnel/page.tsx +1 -1
  10. package/assets/templates/pages/dashboard-data/page.tsx +1 -1
  11. package/assets/templates/pages/dashboard-portfolio/page.tsx +5 -5
  12. package/assets/templates/pages/dashboard-service-monitoring/page.tsx +1 -1
  13. package/assets/templates/pages/settings-dialog/page.tsx +2543 -757
  14. package/assets/templates/pages/settings-dialog/template.doc.mjs +1 -2
  15. package/assets/templates/pages/table-filter/page.tsx +4093 -0
  16. package/assets/templates/pages/table-filter/template.doc.mjs +12 -0
  17. package/assets/templates/pages/theme-showcase/page.tsx +36 -17
  18. package/assets/templates/themes/stone/stoneTheme.ts +3 -1
  19. package/clients/cli/commands/build-theme.color-scheme.test.mjs +7 -6
  20. package/clients/cli/commands/build-theme.data-tokens.test.mjs +144 -0
  21. package/clients/cli/commands/build-theme.variants.test.mjs +3 -0
  22. package/clients/cli/commands/theme-targets.doc.mjs +1 -1
  23. package/foundation/discovery/component-discovery.mjs +10 -9
  24. package/foundation/discovery/hook-discovery.mjs +2 -1
  25. package/foundation/fs/paths.d.mts +16 -0
  26. package/foundation/fs/paths.mjs +36 -0
  27. package/foundation/fs/paths.test.mjs +29 -1
  28. package/package.json +9 -9
@@ -0,0 +1,85 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Asserts every `var(--color-data-*, <hex>)` fallback in the dashboard
5
+ * templates equals the token's own default.
6
+ *
7
+ * The fallback is what paints when the token does not resolve, so a fallback
8
+ * that differs from its token makes the same chart series two different colours
9
+ * depending on whether a theme is present. Three of them had drifted
10
+ * (`#22c55e`, `#E5484D`, `#008E80`), which is also why "the defaults are
11
+ * byte-identical to the template fallbacks" was not true.
12
+ */
13
+
14
+ import {describe, it, expect} from 'vitest';
15
+ import * as fs from 'node:fs';
16
+ import * as path from 'node:path';
17
+ import {fileURLToPath} from 'node:url';
18
+ import {dataTokenDefaults} from '@astryxdesign/core/theme';
19
+
20
+ const TEMPLATES_DIR = path.resolve(
21
+ path.dirname(fileURLToPath(import.meta.url)),
22
+ '../../assets/templates',
23
+ );
24
+ const CALL_SITE = /var\(\s*(--color-data-[a-z0-9-]+)\s*,\s*([^)]+?)\s*\)/g;
25
+
26
+ /** The light side of a `light-dark(a, b)` pair — what a template falls back to. */
27
+ function lightSide(value) {
28
+ const match = /^light-dark\(\s*([^,]+?)\s*,/.exec(value);
29
+ return match ? match[1] : value;
30
+ }
31
+
32
+ function walk(dir) {
33
+ return fs.readdirSync(dir, {withFileTypes: true}).flatMap(entry => {
34
+ const full = path.join(dir, entry.name);
35
+ return entry.isDirectory() ? walk(full) : [full];
36
+ });
37
+ }
38
+
39
+ function callSites() {
40
+ const sites = [];
41
+ for (const file of walk(TEMPLATES_DIR)) {
42
+ if (!/\.(tsx|ts|jsx|js|mjs|css)$/.test(file)) {
43
+ continue;
44
+ }
45
+ fs.readFileSync(file, 'utf-8')
46
+ .split('\n')
47
+ .forEach((line, index) => {
48
+ for (const match of line.matchAll(CALL_SITE)) {
49
+ sites.push({
50
+ where: `${path.relative(TEMPLATES_DIR, file)}:${index + 1}`,
51
+ token: match[1],
52
+ fallback: match[2],
53
+ });
54
+ }
55
+ });
56
+ }
57
+ return sites;
58
+ }
59
+
60
+ describe('template --color-data-* fallbacks', () => {
61
+ const sites = callSites();
62
+
63
+ it('finds the call sites at all', () => {
64
+ expect(sites.length).toBeGreaterThan(40);
65
+ });
66
+
67
+ it('names a real token at every call site', () => {
68
+ const unknown = sites.filter(site => !dataTokenDefaults[site.token]);
69
+ expect(unknown.map(site => `${site.where} ${site.token}`)).toEqual([]);
70
+ });
71
+
72
+ it('matches each token default exactly', () => {
73
+ const drifted = sites
74
+ .filter(site => dataTokenDefaults[site.token])
75
+ .filter(
76
+ site => site.fallback !== lightSide(dataTokenDefaults[site.token]),
77
+ )
78
+ .map(
79
+ site =>
80
+ `${site.where} ${site.token}: ${site.fallback} != ${lightSide(dataTokenDefaults[site.token])}`,
81
+ );
82
+
83
+ expect(drifted).toEqual([]);
84
+ });
85
+ });
@@ -62,12 +62,14 @@ import {
62
62
  /** @type {any} */ let _defineTheme = null;
63
63
  /** @type {any} */ let _generateThemeRulesSplit = null;
64
64
  /** @type {any} */ let _generateOnMediaCSS = null;
65
+ /** @type {any} */ let _dataTokenDefaults = null;
65
66
  /** @type {any} */ let _coreImportError = null;
66
67
  try {
67
68
  const coreTheme = await import('@astryxdesign/core/theme');
68
69
  _defineTheme = coreTheme.defineTheme;
69
70
  _generateThemeRulesSplit = coreTheme.generateThemeRulesSplit;
70
71
  _generateOnMediaCSS = coreTheme.generateOnMediaCSS;
72
+ _dataTokenDefaults = coreTheme.dataTokenDefaults;
71
73
  } catch (e) {
72
74
  // Capture the reason so the theme action can surface a precise, actionable
73
75
  // error. We don't throw here: this module is imported eagerly by the CLI
@@ -1166,8 +1168,15 @@ export async function themeBuild(
1166
1168
  if (component.length > 0) {
1167
1169
  const componentInner = component.join('\n\n');
1168
1170
  const componentScope = `@scope (${scopeSelector}) to (${scopeTo}) {\n${componentInner}\n}`;
1169
- // #3658: also emit attribute-specific rules so <Theme mode> can override color-scheme
1170
- const colorSchemeDecl = componentScope.includes('light-dark(')
1171
+ // #3658: also emit attribute-specific rules so <Theme mode> can override color-scheme.
1172
+ // Decided from the theme's own values, not the generated CSS: that CSS
1173
+ // also carries the data-token defaults, which are light-dark() pairs, so
1174
+ // a substring check on it would fire for every theme.
1175
+ const themeOwnValues = JSON.stringify([
1176
+ resolvedTheme.tokens ?? {},
1177
+ resolvedTheme.components ?? {},
1178
+ ]);
1179
+ const colorSchemeDecl = themeOwnValues.includes('light-dark(')
1171
1180
  ? ' :root { color-scheme: light dark; }\n html[data-theme="light"] { color-scheme: light; }\n html[data-theme="dark"] { color-scheme: dark; }\n\n'
1172
1181
  : '';
1173
1182
  cssParts.push(
@@ -1185,6 +1194,26 @@ export async function themeBuild(
1185
1194
  logger.log('No overrides found — nothing to build.');
1186
1195
  return null;
1187
1196
  }
1197
+ // The data-token defaults are theme-independent and go in @layer
1198
+ // astryx-base, below the theme's own overrides. Formatted here from the
1199
+ // public `dataTokenDefaults` export, byte for byte as the `<Theme>`
1200
+ // runtime emits it — build-theme.data-tokens.test.mjs is the drift guard.
1201
+ // Placed after the reset block and before the theme block: a layer's order
1202
+ // is fixed by where it is first declared, so emitting it anywhere else in
1203
+ // the file would invert reset < astryx-base < astryx-theme for a consumer
1204
+ // who imports this stylesheet on its own.
1205
+ const baseCss = _dataTokenDefaults
1206
+ ? `:root {\n${Object.entries(_dataTokenDefaults)
1207
+ .map(([name, value]) => ` ${name}: ${value};`)
1208
+ .join('\n')}\n}`
1209
+ : '';
1210
+ if (baseCss) {
1211
+ cssParts.splice(
1212
+ prose.length > 0 ? 1 : 0,
1213
+ 0,
1214
+ `@layer astryx-base {\n${baseCss}\n}`,
1215
+ );
1216
+ }
1188
1217
  css = cssParts.join('\n\n') + '\n';
1189
1218
  }
1190
1219
 
@@ -18,9 +18,9 @@ export const doc = {
18
18
  'Enumerates the whole themeable surface: each `defineTheme` components key, the stable ' +
19
19
  'class it paints, the component that declares it, and the props and states that are legal ' +
20
20
  'override keys under it. Same source as the Theming table `astryx component <Name>` prints ' +
21
- '— the component docs — so the list cannot drift from the components, and `theme build` ' +
21
+ '(the component docs), so the list cannot drift from the components, and `theme build` ' +
22
22
  'validates overrides against this exact set. A filter naming a component gives that ' +
23
- 'component\u2019s set; anything else is a substring search over the keys.',
23
+ 'component\'s set; anything else is a substring search over the keys.',
24
24
  importPath: '@astryxdesign/cli/api',
25
25
  signature:
26
26
  'themeTargets(filter?: string, ctx?: {cwd?: string}): Promise<ThemeTargetsResponse>',
@@ -35,7 +35,7 @@ export const doc = {
35
35
  {
36
36
  name: 'ctx.cwd',
37
37
  type: 'string',
38
- description: 'Directory the project’s @astryxdesign/core is resolved from.',
38
+ description: 'Directory the project\'s @astryxdesign/core is resolved from.',
39
39
  },
40
40
  ],
41
41
  returns: [
@@ -7,7 +7,7 @@ export const doc = {
7
7
  name: 'Step — Content Slot',
8
8
  displayName: 'Step — Content Slot',
9
9
  description:
10
- 'Children passed to a Step render below its description, indented to line up with the label rather than the indicator, and stay outside the clickable label area so buttons inside remain their own targets. In a full flow you gate the slot on the step being active — that is what turns a vertical stepper into an expanding one.',
10
+ 'Children passed to a Step render below its description, indented to line up with the label rather than the indicator, and stay outside the clickable label area so buttons inside remain their own targets. In a full flow you gate the slot on the step being active. That is what turns a vertical stepper into an expanding one.',
11
11
  isReady: true,
12
12
  aspectRatio: 4 / 3,
13
13
  componentsUsed: ['Stepper', 'Step', 'TextInput', 'Button'],
@@ -7,7 +7,7 @@ export const doc = {
7
7
  name: 'Step — Indicator',
8
8
  displayName: 'Step — Indicator',
9
9
  description:
10
- 'Everything the indicator prop accepts: the auto default, an always-number badge, a custom ReactNode, and none — each on its own completed Step so the prop is the only difference between them. Every variant occupies the same 16px box, so a step swapping its number for a check as it completes never shifts the label beside it. The last cell shows that a custom node can be live rather than static: a Spinner on a step that is in progress, shaded `inherit` so it picks up the step\u2019s own tint like any other glyph.',
10
+ 'Everything the indicator prop accepts: the auto default, an always-number badge, a custom ReactNode, and none, each on its own completed Step so the prop is the only difference between them. Every variant occupies the same 16px box, so a step swapping its number for a check as it completes never shifts the label beside it. The last cell shows that a custom node can be live rather than static: a Spinner on a step that is in progress, shaded `inherit` so it picks up the step\'s own tint like any other glyph.',
11
11
  isReady: true,
12
12
  aspectRatio: 4 / 3,
13
13
  componentsUsed: ['Stepper', 'Step', 'Icon', 'Spinner', 'Text'],
@@ -7,7 +7,7 @@ export const doc = {
7
7
  name: 'Step',
8
8
  displayName: 'Step',
9
9
  description:
10
- 'A single Step, with every part it can render: the indicator, the label with its optional marker and trailing endContent, and the description beneath. A Step never sets its own completed/current state — it declares its index and derives the rest from the parent Stepper, so one Step in one Stepper is a complete example.',
10
+ 'A single Step, with every part it can render: the indicator, the label with its optional marker and trailing endContent, and the description beneath. A Step never sets its own completed/current state. It declares its index and derives the rest from the parent Stepper, so one Step in one Stepper is a complete example.',
11
11
  isReady: true,
12
12
  isShowcase: true,
13
13
  aspectRatio: 16 / 9,
@@ -7,7 +7,7 @@ export const doc = {
7
7
  name: 'Stepper — Custom Content',
8
8
  displayName: 'Stepper — Custom Content',
9
9
  description:
10
- 'A vertical stepper where each step owns a slice of the page. The content slot takes any node — form fields, a summary panel, a banner — so a stepper is not limited to multi-step forms. Rendering the slot only for the active step is what makes the flow expand one step at a time.',
10
+ 'A vertical stepper where each step owns a slice of the page. The content slot takes any node (form fields, a summary panel, a banner), so a stepper is not limited to multi-step forms. Rendering the slot only for the active step is what makes the flow expand one step at a time.',
11
11
  isReady: true,
12
12
  aspectRatio: 4 / 3,
13
13
  componentsUsed: [
@@ -7,7 +7,7 @@ export const doc = {
7
7
  name: 'Stepper — Checkout Progress',
8
8
  displayName: 'Stepper — Checkout Progress',
9
9
  description:
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.',
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,
@@ -79,7 +79,7 @@ const COLORS = {
79
79
  green: 'var(--color-data-categorical-green, #0B991F)',
80
80
  orange: 'var(--color-data-categorical-orange, #EB6E00)',
81
81
  purple: 'var(--color-data-categorical-purple, #6B1EFD)',
82
- teal: 'var(--color-data-categorical-teal, #008E80)',
82
+ teal: 'var(--color-data-categorical-teal, #08A3A3)',
83
83
  };
84
84
  const GRID_STROKE = 'var(--color-border, rgba(5, 54, 89, 0.1))';
85
85
  const AXIS_TICK = {
@@ -98,7 +98,7 @@ const COLORS = {
98
98
  green: 'var(--color-data-categorical-green, #0B991F)',
99
99
  orange: 'var(--color-data-categorical-orange, #EB6E00)',
100
100
  purple: 'var(--color-data-categorical-purple, #6B1EFD)',
101
- teal: 'var(--color-data-categorical-teal, #008E80)',
101
+ teal: 'var(--color-data-categorical-teal, #08A3A3)',
102
102
  neutral: 'var(--color-data-neutral, #8494A3)',
103
103
  };
104
104
  const GRID_STROKE = 'var(--color-border, rgba(5, 54, 89, 0.1))';
@@ -370,12 +370,12 @@ function PortfolioChart() {
370
370
  <linearGradient id="portfolioGradient" x1="0" y1="0" x2="0" y2="1">
371
371
  <stop
372
372
  offset="5%"
373
- stopColor="var(--color-data-categorical-green, #22c55e)"
373
+ stopColor="var(--color-data-categorical-green, #0B991F)"
374
374
  stopOpacity={0.3}
375
375
  />
376
376
  <stop
377
377
  offset="95%"
378
- stopColor="var(--color-data-categorical-green, #22c55e)"
378
+ stopColor="var(--color-data-categorical-green, #0B991F)"
379
379
  stopOpacity={0.05}
380
380
  />
381
381
  </linearGradient>
@@ -417,7 +417,7 @@ function PortfolioChart() {
417
417
  <Area
418
418
  type="linear"
419
419
  dataKey="value"
420
- stroke="var(--color-data-categorical-green, #22c55e)"
420
+ stroke="var(--color-data-categorical-green, #0B991F)"
421
421
  strokeWidth={1.5}
422
422
  fill="url(#portfolioGradient)"
423
423
  dot={false}
@@ -434,7 +434,7 @@ function Sparkline({data, positive}: {data: number[]; positive: boolean}) {
434
434
  const chartData = data.map((v, i) => ({i, v}));
435
435
  const color = positive
436
436
  ? 'var(--color-data-categorical-green, #0B991F)'
437
- : 'var(--color-data-categorical-red, #E5484D)';
437
+ : 'var(--color-data-categorical-red, #F5394F)';
438
438
  return (
439
439
  <ResponsiveContainer width="100%" height={40}>
440
440
  <LineChart
@@ -503,7 +503,7 @@ function TrendSparkline({data, positive}: {data: number[]; positive: boolean}) {
503
503
  const chartData = data.map((v, i) => ({i, v}));
504
504
  const color = positive
505
505
  ? 'var(--color-data-categorical-green, #0B991F)'
506
- : 'var(--color-data-categorical-red, #E5484D)';
506
+ : 'var(--color-data-categorical-red, #F5394F)';
507
507
  return (
508
508
  <ResponsiveContainer width="100%" height={24}>
509
509
  <LineChart
@@ -727,7 +727,7 @@ interface ChartLine {
727
727
  const BLUE = 'var(--color-data-categorical-blue, #0171E3)';
728
728
  const ORANGE = 'var(--color-data-categorical-orange, #EB6E00)';
729
729
  const PURPLE = 'var(--color-data-categorical-purple, #6B1EFD)';
730
- const TEAL = 'var(--color-data-categorical-teal, #008E80)';
730
+ const TEAL = 'var(--color-data-categorical-teal, #08A3A3)';
731
731
 
732
732
  const LATENCY_LINES: ChartLine[] = [
733
733
  {key: 'p50', name: 'p50', color: BLUE},