@ethlete/cli 3.0.0-next.25 → 3.0.0-next.26

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 (140) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/design/define-call.ts +29 -3
  3. package/design/define-part.ts +15 -1
  4. package/design/logo-sheet.ts +52 -0
  5. package/design/motion-drive.ts +215 -0
  6. package/design/motion-runtime.ts +14 -2
  7. package/design/motion-tokens.ts +177 -0
  8. package/design/web/frame-fill.ts +4 -0
  9. package/design/web/frame-scheme.ts +44 -0
  10. package/design/web/frame.ts +122 -29
  11. package/package.json +1 -1
  12. package/src/lib/design/calls-tsconfig.d.ts +2 -0
  13. package/src/lib/design/calls-tsconfig.js +10 -3
  14. package/src/lib/design/calls-tsconfig.js.map +1 -1
  15. package/src/lib/design/check.d.ts +1 -0
  16. package/src/lib/design/check.js +62 -6
  17. package/src/lib/design/check.js.map +1 -1
  18. package/src/lib/design/contact-sheet.d.ts +9 -0
  19. package/src/lib/design/contact-sheet.js +38 -4
  20. package/src/lib/design/contact-sheet.js.map +1 -1
  21. package/src/lib/design/design-command.js +27 -8
  22. package/src/lib/design/design-command.js.map +1 -1
  23. package/src/lib/design/draw-ledger.d.ts +36 -0
  24. package/src/lib/design/draw-ledger.js +52 -0
  25. package/src/lib/design/draw-ledger.js.map +1 -0
  26. package/src/lib/design/mcp-command.d.ts +23 -2
  27. package/src/lib/design/mcp-command.js +105 -10
  28. package/src/lib/design/mcp-command.js.map +1 -1
  29. package/src/lib/design/motion-command.js +9 -15
  30. package/src/lib/design/motion-command.js.map +1 -1
  31. package/src/lib/design/paths.d.ts +14 -0
  32. package/src/lib/design/paths.js +50 -1
  33. package/src/lib/design/paths.js.map +1 -1
  34. package/src/lib/design/render-packages.d.ts +5 -0
  35. package/src/lib/design/render-packages.js +6 -1
  36. package/src/lib/design/render-packages.js.map +1 -1
  37. package/src/lib/design/render-worker.js +1 -0
  38. package/src/lib/design/render-worker.js.map +1 -1
  39. package/src/lib/design/serve.d.ts +2 -2
  40. package/src/lib/design/serve.js +158 -19
  41. package/src/lib/design/serve.js.map +1 -1
  42. package/src/lib/design/shot-command.d.ts +5 -2
  43. package/src/lib/design/shot-command.js +18 -16
  44. package/src/lib/design/shot-command.js.map +1 -1
  45. package/src/lib/design/shot-store.d.ts +24 -0
  46. package/src/lib/design/shot-store.js +120 -0
  47. package/src/lib/design/shot-store.js.map +1 -0
  48. package/src/lib/design/shot.d.ts +21 -1
  49. package/src/lib/design/shot.js +118 -31
  50. package/src/lib/design/shot.js.map +1 -1
  51. package/src/lib/design/sketch-command.js +121 -37
  52. package/src/lib/design/sketch-command.js.map +1 -1
  53. package/src/lib/design/ssr-shims.d.ts +2 -0
  54. package/src/lib/design/ssr-shims.js +12 -1
  55. package/src/lib/design/ssr-shims.js.map +1 -1
  56. package/src/lib/design/studio-api.d.ts +12 -0
  57. package/src/lib/design/studio-api.js +68 -2
  58. package/src/lib/design/studio-api.js.map +1 -1
  59. package/src/lib/design/system/check-system.d.ts +6 -2
  60. package/src/lib/design/system/check-system.js +102 -20
  61. package/src/lib/design/system/check-system.js.map +1 -1
  62. package/src/lib/design/system/colour.d.ts +4 -1
  63. package/src/lib/design/system/colour.js +54 -2
  64. package/src/lib/design/system/colour.js.map +1 -1
  65. package/src/lib/design/system/complete-command.js +6 -2
  66. package/src/lib/design/system/complete-command.js.map +1 -1
  67. package/src/lib/design/system/complete.d.ts +67 -2
  68. package/src/lib/design/system/complete.js +227 -53
  69. package/src/lib/design/system/complete.js.map +1 -1
  70. package/src/lib/design/system/contrast-check.d.ts +6 -1
  71. package/src/lib/design/system/contrast-check.js +167 -12
  72. package/src/lib/design/system/contrast-check.js.map +1 -1
  73. package/src/lib/design/system/drawn-open-points.d.ts +16 -0
  74. package/src/lib/design/system/drawn-open-points.js +43 -0
  75. package/src/lib/design/system/drawn-open-points.js.map +1 -0
  76. package/src/lib/design/system/list-system.d.ts +4 -1
  77. package/src/lib/design/system/list-system.js +2 -1
  78. package/src/lib/design/system/list-system.js.map +1 -1
  79. package/src/lib/design/system/open-points.d.ts +101 -0
  80. package/src/lib/design/system/open-points.js +156 -0
  81. package/src/lib/design/system/open-points.js.map +1 -0
  82. package/src/lib/design/system/part-ready.d.ts +52 -0
  83. package/src/lib/design/system/part-ready.js +101 -0
  84. package/src/lib/design/system/part-ready.js.map +1 -0
  85. package/src/lib/design/system/read-open-points.d.ts +27 -0
  86. package/src/lib/design/system/read-open-points.js +81 -0
  87. package/src/lib/design/system/read-open-points.js.map +1 -0
  88. package/src/lib/design/system/ready-command.d.ts +7 -0
  89. package/src/lib/design/system/ready-command.js +65 -0
  90. package/src/lib/design/system/ready-command.js.map +1 -0
  91. package/src/lib/design/system/render-check.d.ts +2 -0
  92. package/src/lib/design/system/render-check.js +197 -31
  93. package/src/lib/design/system/render-check.js.map +1 -1
  94. package/src/lib/design/system/resolve-product.d.ts +2 -1
  95. package/src/lib/design/system/resolve-product.js +20 -3
  96. package/src/lib/design/system/resolve-product.js.map +1 -1
  97. package/src/lib/design/system/rules/implements.d.ts +7 -0
  98. package/src/lib/design/system/rules/implements.js +43 -0
  99. package/src/lib/design/system/rules/implements.js.map +1 -0
  100. package/src/lib/design/system/rules/motion.d.ts +2 -0
  101. package/src/lib/design/system/rules/motion.js +26 -1
  102. package/src/lib/design/system/rules/motion.js.map +1 -1
  103. package/src/lib/design/system/rules/option-values.d.ts +15 -0
  104. package/src/lib/design/system/rules/option-values.js +145 -0
  105. package/src/lib/design/system/rules/option-values.js.map +1 -0
  106. package/src/lib/design/system/rules/stale-replace.js +41 -3
  107. package/src/lib/design/system/rules/stale-replace.js.map +1 -1
  108. package/src/lib/design/system/sketch/baseline.d.ts +11 -2
  109. package/src/lib/design/system/sketch/baseline.js +4 -2
  110. package/src/lib/design/system/sketch/baseline.js.map +1 -1
  111. package/src/lib/design/system/sketch/check-sketches.d.ts +1 -0
  112. package/src/lib/design/system/sketch/check-sketches.js +4 -2
  113. package/src/lib/design/system/sketch/check-sketches.js.map +1 -1
  114. package/src/lib/design/system/sketch/merge.js +32 -0
  115. package/src/lib/design/system/sketch/merge.js.map +1 -1
  116. package/src/lib/design/system/sketch/merged-parts.d.ts +1 -1
  117. package/src/lib/design/system/sketch/merged-parts.js +1 -1
  118. package/src/lib/design/system/sketch/merged-parts.js.map +1 -1
  119. package/src/lib/design/system/sketch/part-definition.d.ts +36 -0
  120. package/src/lib/design/system/sketch/part-definition.js +56 -2
  121. package/src/lib/design/system/sketch/part-definition.js.map +1 -1
  122. package/src/lib/design/system/sketch/sketch.d.ts +15 -0
  123. package/src/lib/design/system/sketch/sketch.js +65 -27
  124. package/src/lib/design/system/sketch/sketch.js.map +1 -1
  125. package/src/lib/design/system/sketch/unchanged-states.d.ts +4 -1
  126. package/src/lib/design/system/sketch/unchanged-states.js +9 -5
  127. package/src/lib/design/system/sketch/unchanged-states.js.map +1 -1
  128. package/src/lib/design/system/sketch/unused-closure.js +12 -2
  129. package/src/lib/design/system/sketch/unused-closure.js.map +1 -1
  130. package/src/lib/design/system/sketch/unused-files.d.ts +1 -1
  131. package/src/lib/design/system/sketch/unused-files.js +6 -3
  132. package/src/lib/design/system/sketch/unused-files.js.map +1 -1
  133. package/src/lib/design/system/system-tsconfig.js +2 -2
  134. package/src/lib/design/system/system-tsconfig.js.map +1 -1
  135. package/src/lib/design/system/ts-source.d.ts +3 -0
  136. package/src/lib/design/system/ts-source.js +21 -4
  137. package/src/lib/design/system/ts-source.js.map +1 -1
  138. package/src/lib/design/thumb.d.ts +31 -4
  139. package/src/lib/design/thumb.js +105 -40
  140. package/src/lib/design/thumb.js.map +1 -1
@@ -0,0 +1,177 @@
1
+ import { MotionTrack } from './motion-runtime';
2
+
3
+ /** A CSS custom property declared on `:root`, with its computed value. */
4
+ export type MotionToken = { name: string; value: string };
5
+
6
+ /** A track with the names of the tokens whose value equals its duration or easing, in declaration order. */
7
+ export type NamedMotionTrack = MotionTrack & { durationTokens?: string[]; easingTokens?: string[] };
8
+
9
+ const KEYWORD_EASINGS: Record<string, string> = {
10
+ ease: 'cubic-bezier(0.25,0.1,0.25,1)',
11
+ 'ease-in': 'cubic-bezier(0.42,0,1,1)',
12
+ 'ease-out': 'cubic-bezier(0,0,0.58,1)',
13
+ 'ease-in-out': 'cubic-bezier(0.42,0,0.58,1)',
14
+ 'step-start': 'steps(1,jump-start)',
15
+ 'step-end': 'steps(1,jump-end)',
16
+ };
17
+
18
+ const TIME = /^(-?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?)(ms|s)$/u;
19
+ const EASING_FUNCTION = /^(cubic-bezier|linear|steps)\((.*)\)$/u;
20
+
21
+ const roundedOf = (value: number) => String(Math.round(value * 10000) / 10000);
22
+
23
+ const msOf = (value: string) => {
24
+ const match = TIME.exec(value.trim().toLowerCase());
25
+
26
+ return match ? Number(match[1]) * (match[2] === 's' ? 1000 : 1) : null;
27
+ };
28
+
29
+ const linearOf = (body: string) => {
30
+ const points: { output: number; input: number | null }[] = [];
31
+
32
+ for (const stop of body.split(',')) {
33
+ const [output = '', ...inputs] = stop.trim().split(/\s+/u);
34
+ const value = Number(output);
35
+
36
+ if (output === '' || !Number.isFinite(value) || inputs.length > 2) return null;
37
+
38
+ const percents = inputs.map((input) => (input.endsWith('%') ? Number(input.slice(0, -1)) : Number.NaN));
39
+
40
+ if (percents.some((percent) => !Number.isFinite(percent))) return null;
41
+
42
+ if (percents.length === 0) points.push({ output: value, input: null });
43
+ for (const percent of percents) points.push({ output: value, input: percent });
44
+ }
45
+
46
+ if (points.length < 2) return null;
47
+
48
+ const first = points[0];
49
+ const last = points[points.length - 1];
50
+
51
+ if (first && first.input === null) first.input = 0;
52
+ if (last && last.input === null) last.input = Math.max(100, ...points.map((point) => point.input ?? 0));
53
+
54
+ let highest = 0;
55
+
56
+ for (const point of points) {
57
+ if (point.input !== null) point.input = Math.max(point.input, highest);
58
+ highest = point.input ?? highest;
59
+ }
60
+
61
+ for (let index = 1; index < points.length; index++) {
62
+ if (points[index]?.input !== null) continue;
63
+
64
+ const before = index - 1;
65
+ let after = index;
66
+
67
+ while (points[after]?.input === null) after++;
68
+
69
+ const from = points[before]?.input ?? 0;
70
+ const to = points[after]?.input ?? 100;
71
+
72
+ for (let gap = index; gap < after; gap++) {
73
+ const point = points[gap];
74
+
75
+ if (point) point.input = from + ((to - from) * (gap - before)) / (after - before);
76
+ }
77
+ }
78
+
79
+ return `linear(${points.map((point) => `${roundedOf(point.output)} ${roundedOf(point.input ?? 0)}%`).join(',')})`;
80
+ };
81
+
82
+ /** An easing in one spelling, so `ease` equals `cubic-bezier(.25, .1, .25, 1)`; `null` when it is not an easing. */
83
+ export const normalisedEasingOf = (value: string): string | null => {
84
+ const easing = value.trim().toLowerCase().replace(/\s+/gu, ' ');
85
+
86
+ if (easing === 'linear') return 'linear';
87
+ if (KEYWORD_EASINGS[easing]) return KEYWORD_EASINGS[easing];
88
+
89
+ const match = EASING_FUNCTION.exec(easing);
90
+
91
+ if (!match) return null;
92
+
93
+ const [, kind = '', body = ''] = match;
94
+
95
+ if (kind === 'linear') return linearOf(body);
96
+
97
+ const args = body.split(',').map((arg) => arg.trim());
98
+
99
+ if (kind === 'steps') return args.length === 1 || args.length === 2 ? `steps(${args[0]},${args[1] ?? 'end'})` : null;
100
+
101
+ const numbers = args.map(Number);
102
+
103
+ return numbers.length === 4 && numbers.every(Number.isFinite)
104
+ ? `cubic-bezier(${numbers.map(roundedOf).join(',')})`
105
+ : null;
106
+ };
107
+
108
+ /** What a token stands for: a time, an easing, or both as a `spring()` token stores them (`<duration>ms linear(...)`). */
109
+ const meaningOf = (value: string) => {
110
+ const trimmed = value.trim();
111
+ const ms = msOf(trimmed);
112
+
113
+ if (ms !== null) return { ms, easing: null };
114
+
115
+ const [head = '', ...rest] = trimmed.split(/\s+/u);
116
+ const leading = msOf(head);
117
+
118
+ if (leading !== null) {
119
+ const easing = normalisedEasingOf(rest.join(' '));
120
+
121
+ return easing ? { ms: leading, easing } : null;
122
+ }
123
+
124
+ const easing = normalisedEasingOf(trimmed);
125
+
126
+ return easing ? { ms: null, easing } : null;
127
+ };
128
+
129
+ /**
130
+ * Adds the names of the `tokens` whose value equals a track's duration (`durationTokens`) or easing (`easingTokens`),
131
+ * in the order of `tokens`; a field without a match is left out.
132
+ */
133
+ export const withTokenNames = (track: MotionTrack, tokens: readonly MotionToken[]): NamedMotionTrack => {
134
+ const easing = normalisedEasingOf(track.easing);
135
+ const durationTokens: string[] = [];
136
+ const easingTokens: string[] = [];
137
+
138
+ for (const token of tokens) {
139
+ const meaning = meaningOf(token.value);
140
+
141
+ if (!meaning) continue;
142
+ if (meaning.ms !== null && Math.abs(meaning.ms - track.duration) < 0.001) durationTokens.push(token.name);
143
+ if (meaning.easing !== null && easing !== null && meaning.easing === easing) easingTokens.push(token.name);
144
+ }
145
+
146
+ return {
147
+ ...track,
148
+ ...(durationTokens.length ? { durationTokens } : {}),
149
+ ...(easingTokens.length ? { easingTokens } : {}),
150
+ };
151
+ };
152
+
153
+ const rootNamesOf = (rules: CSSRuleList, names: Set<string>) => {
154
+ for (const rule of [...rules]) {
155
+ if (rule instanceof CSSStyleRule && /^(:root|html)$/u.test(rule.selectorText.trim()))
156
+ for (const property of [...rule.style]) if (property.startsWith('--')) names.add(property);
157
+
158
+ if ('cssRules' in rule && rule.cssRules instanceof CSSRuleList) rootNamesOf(rule.cssRules, names);
159
+ }
160
+ };
161
+
162
+ /** The custom properties declared on `:root` in the stylesheets of `document`, in declaration order, as computed there. */
163
+ export const rootTokensOf = (document: Document): MotionToken[] => {
164
+ const names = new Set<string>();
165
+
166
+ for (const sheet of [...document.styleSheets]) {
167
+ try {
168
+ rootNamesOf(sheet.cssRules, names);
169
+ } catch {
170
+ continue;
171
+ }
172
+ }
173
+
174
+ const computed = getComputedStyle(document.documentElement);
175
+
176
+ return [...names].map((name) => ({ name, value: computed.getPropertyValue(name).trim() }));
177
+ };
@@ -0,0 +1,4 @@
1
+ /** Paints `body` with the token `--<fill>` over the product's page colour, which shows when the product has no such token. */
2
+ export const paintFill = (body: HTMLElement, fill: string) => {
3
+ body.style.backgroundImage = `linear-gradient(var(--${fill}), var(--${fill}))`;
4
+ };
@@ -0,0 +1,44 @@
1
+ export type ColorScheme = 'light' | 'dark';
2
+
3
+ const SCHEME_FEATURE = /\(\s*prefers-color-scheme\s*:\s*(light|dark)\s*\)/giu;
4
+
5
+ /** `media` with each `prefers-color-scheme` feature replaced by one that always or never matches, as `scheme` decides. */
6
+ export const pinnedMediaText = (media: string, scheme: ColorScheme) =>
7
+ media.replace(SCHEME_FEATURE, (_, wanted: string) =>
8
+ wanted.toLowerCase() === scheme ? '(width >= 0)' : '(width < 0)',
9
+ );
10
+
11
+ const pinRules = (rules: CSSRuleList, scheme: ColorScheme) => {
12
+ for (const rule of Array.from(rules)) {
13
+ if (rule instanceof CSSMediaRule) {
14
+ const pinned = pinnedMediaText(rule.media.mediaText, scheme);
15
+
16
+ if (pinned !== rule.media.mediaText) rule.media.mediaText = pinned;
17
+ }
18
+
19
+ if ('cssRules' in rule) pinRules((rule as CSSGroupingRule).cssRules, scheme);
20
+ }
21
+ };
22
+
23
+ const pinSheets = (document: Document, scheme: ColorScheme) => {
24
+ for (const sheet of Array.from(document.styleSheets)) {
25
+ try {
26
+ pinRules(sheet.cssRules, scheme);
27
+ } catch {
28
+ // A cross-origin sheet hides its rules.
29
+ }
30
+ }
31
+ };
32
+
33
+ export const pinScheme = (document: Document, scheme: ColorScheme) => {
34
+ document.documentElement.style.colorScheme = scheme;
35
+ pinSheets(document, scheme);
36
+
37
+ new MutationObserver((records) => {
38
+ pinSheets(document, scheme);
39
+
40
+ for (const record of records)
41
+ for (const node of Array.from(record.addedNodes))
42
+ if (node instanceof HTMLLinkElement) node.addEventListener('load', () => pinSheets(document, scheme));
43
+ }).observe(document.head, { childList: true, subtree: true, characterData: true });
44
+ };
@@ -1,7 +1,11 @@
1
1
  import { CallVariant, Drawing, fitOutOfFlow, mountDrawing, placeOverlays } from '../define-call';
2
- import { Part } from '../define-part';
2
+ import { Motion, Part } from '../define-part';
3
3
  import { applyEmbed, fitEmbed } from '../embed-page';
4
+ import { paintFill } from './frame-fill';
5
+ import { pinScheme } from './frame-scheme';
6
+ import { createMotionDriver, emulateReducedMotion, motionDriveMessageOf } from '../motion-drive';
4
7
  import { runRules, stepMotion, SteppedMotion } from '../motion-runtime';
8
+ import { rootTokensOf, withTokenNames } from '../motion-tokens';
5
9
  import { calls } from 'virtual:design-explore';
6
10
  import { loadEnv, loadProductEnv } from 'virtual:design-explore/env';
7
11
  import { parts, sketchParts, sketchTokens, tokens } from 'virtual:design-explore/system';
@@ -40,7 +44,11 @@ const fill = params.get('fill');
40
44
  // On <html> a theme class ties with the `:root` defaults of the theme CSS, which come later and win.
41
45
  if (surface) root.classList.add(`et-surface--${surface}`);
42
46
  if (color) root.classList.add(`et-color--${color}`);
43
- if (fill) document.body.style.background = `var(--${fill})`;
47
+ if (fill) paintFill(document.body, fill);
48
+
49
+ const scheme = params.get('scheme');
50
+
51
+ if (scheme === 'light' || scheme === 'dark') pinScheme(document, scheme);
44
52
  if (fit) {
45
53
  document.documentElement.style.overflow = 'hidden';
46
54
  root.style.width = 'max-content';
@@ -66,6 +74,12 @@ const fail = (message: string) => {
66
74
  return null;
67
75
  };
68
76
 
77
+ const failMissing = (message: string) => {
78
+ document.documentElement.setAttribute('data-et-missing', '');
79
+
80
+ return fail(message);
81
+ };
82
+
69
83
  const showSetupError = (message: string) => {
70
84
  markFailed(message);
71
85
 
@@ -96,10 +110,10 @@ let drawnPart: { part: Part<unknown>; states: Record<string, unknown> } | null =
96
110
 
97
111
  const drawCall = async (): Promise<Drawing | null> => {
98
112
  const call = calls[slug];
99
- if (!call) return fail(`design-explore: no call "${slug}"`);
113
+ if (!call) return failMissing(`design-explore: no call "${slug}"`);
100
114
 
101
115
  const variant: CallVariant | undefined = (await call()).default.variants.find((o) => o.key === key);
102
- if (!variant) return fail(`design-explore: call "${slug}" has no variant "${key}"`);
116
+ if (!variant) return failMissing(`design-explore: call "${slug}" has no variant "${key}"`);
103
117
 
104
118
  return variant.load().then(
105
119
  (module) => module.default,
@@ -110,7 +124,7 @@ const drawCall = async (): Promise<Drawing | null> => {
110
124
  const drawPart = async (name: string): Promise<Drawing | null> => {
111
125
  const load = sketch ? sketchParts[name]?.[sketch]?.[sketchVariant]?.[partKey] : parts[name]?.[partKey];
112
126
  if (!load)
113
- return fail(
127
+ return failMissing(
114
128
  sketch
115
129
  ? `design-explore: variant "${sketchVariant}" of the sketch on ${sketch} in product "${name}" has no part "${partKey}"`
116
130
  : `design-explore: product "${name}" has no part "${partKey}"`,
@@ -118,14 +132,14 @@ const drawPart = async (name: string): Promise<Drawing | null> => {
118
132
 
119
133
  const loadProps = propsSketch ? sketchParts[name]?.[propsSketch]?.[propsVariant]?.[partKey] : load;
120
134
  if (!loadProps)
121
- return fail(
135
+ return failMissing(
122
136
  `design-explore: variant "${propsVariant}" of the sketch on ${propsSketch} in product "${name}" has no part "${partKey}"`,
123
137
  );
124
138
 
125
139
  try {
126
140
  const [{ default: part }, { default: holder }] = await Promise.all([retried(load), retried(loadProps)]);
127
141
 
128
- if (!(key in holder.states)) return fail(`design-explore: part "${partKey}" has no state "${key}"`);
142
+ if (!(key in holder.states)) return failMissing(`design-explore: part "${partKey}" has no state "${key}"`);
129
143
 
130
144
  drawnPart = { part: part as Part<unknown>, states: holder.states };
131
145
 
@@ -181,6 +195,11 @@ const report = () => {
181
195
  );
182
196
  };
183
197
 
198
+ const reportPainted = () =>
199
+ requestAnimationFrame(() =>
200
+ requestAnimationFrame(() => parent.postMessage({ type: 'design-explore:painted', variant: key }, '*')),
201
+ );
202
+
184
203
  const stateMarkupOf = ({ part, states }: NonNullable<typeof drawnPart>, styles: string) => {
185
204
  const sheets = new Set([styles]);
186
205
 
@@ -218,11 +237,53 @@ const exposeStepped = (options: { start: () => Promise<SteppedMotion>; interrupt
218
237
  }),
219
238
  seek: (ms: number) => steppedOf().seek(ms),
220
239
  bounds: () => steppedOf().bounds(),
221
- timeline: () => ({ end: steppedOf().end(), tracks: steppedOf().tracks(), interrupt: options.interrupt }),
240
+ timeline: () => {
241
+ const tokens = rootTokensOf(document);
242
+
243
+ return {
244
+ end: steppedOf().end(),
245
+ tracks: steppedOf()
246
+ .tracks()
247
+ .map((track) => withTokenNames(track, tokens)),
248
+ interrupt: options.interrupt,
249
+ };
250
+ },
222
251
  },
223
252
  });
224
253
  };
225
254
 
255
+ const motionStartOf = (options: {
256
+ drawn: NonNullable<typeof drawnPart>;
257
+ motion: Motion;
258
+ markupOf: (state: string) => Promise<string>;
259
+ switching?: () => void;
260
+ }) => {
261
+ const { drawn, motion, markupOf } = options;
262
+
263
+ return () =>
264
+ Promise.all([markupOf(motion.to), motion.interrupt === undefined ? null : markupOf(motion.from)]).then(
265
+ ([markup, back]) => {
266
+ options.switching?.();
267
+
268
+ return stepMotion({
269
+ root,
270
+ markup,
271
+ motion,
272
+ ...(back === null
273
+ ? {}
274
+ : {
275
+ reverse: {
276
+ markup: back,
277
+ motion: Object.values(drawn.part.motions ?? {}).find(
278
+ (other) => other.from === motion.to && other.to === motion.from,
279
+ ),
280
+ },
281
+ }),
282
+ });
283
+ },
284
+ );
285
+ };
286
+
226
287
  const exposeMotion = (options: { drawn: NonNullable<typeof drawnPart>; name: string; styles: string }) => {
227
288
  const { drawn, name, styles } = options;
228
289
  const motion = drawn.part.motions?.[name];
@@ -235,29 +296,57 @@ const exposeMotion = (options: { drawn: NonNullable<typeof drawnPart>; name: str
235
296
  return;
236
297
  }
237
298
 
238
- const markupOf = stateMarkupOf(drawn, styles);
239
-
240
299
  exposeStepped({
241
300
  interrupt: motion.interrupt ?? null,
242
- start: () =>
243
- Promise.all([markupOf(motion.to), motion.interrupt === undefined ? null : markupOf(motion.from)]).then(
244
- ([markup, back]) =>
245
- stepMotion({
246
- root,
247
- markup,
248
- motion,
249
- ...(back === null
250
- ? {}
251
- : {
252
- reverse: {
253
- markup: back,
254
- motion: Object.values(drawn.part.motions ?? {}).find(
255
- (other) => other.from === motion.to && other.to === motion.from,
256
- ),
257
- },
258
- }),
259
- }),
260
- ),
301
+ start: motionStartOf({ drawn, motion, markupOf: stateMarkupOf(drawn, styles) }),
302
+ });
303
+ };
304
+
305
+ const settleAnimations = () => {
306
+ for (const animation of document.getAnimations()) {
307
+ if (Number.isFinite(animation.effect?.getComputedTiming().endTime)) animation.finish();
308
+ else animation.cancel();
309
+ }
310
+ };
311
+
312
+ const listenForDrive = (drawn: NonNullable<typeof drawnPart>, styles: string) => {
313
+ const markupOf = stateMarkupOf(drawn, styles);
314
+ const drawnMarkup = root.innerHTML;
315
+ let restore: () => void = () => undefined;
316
+
317
+ const driver = createMotionDriver({
318
+ document,
319
+ reply: (answer) => parent.postMessage({ ...answer, variant: key }, '*'),
320
+ released: () => {
321
+ restore();
322
+ restore = () => undefined;
323
+ },
324
+ start: (name, reduced) => {
325
+ const motion = drawn.part.motions?.[name];
326
+
327
+ if (!motion) return Promise.reject(new Error(`design-explore: part "${partKey}" has no motion "${name}"`));
328
+ if (motion.from !== key)
329
+ return Promise.reject(new Error(`design-explore: motion "${name}" starts at "${motion.from}", not "${key}"`));
330
+
331
+ settleAnimations();
332
+ restore();
333
+ root.innerHTML = drawnMarkup;
334
+
335
+ return motionStartOf({
336
+ drawn,
337
+ motion,
338
+ markupOf,
339
+ switching: () => {
340
+ restore = emulateReducedMotion(document, reduced);
341
+ },
342
+ })();
343
+ },
344
+ });
345
+
346
+ addEventListener('message', (event) => {
347
+ const message = event.source === parent ? motionDriveMessageOf(event.data) : null;
348
+
349
+ if (message) void driver.handle(message);
261
350
  });
262
351
  };
263
352
 
@@ -337,6 +426,8 @@ const drawFrame = async () => {
337
426
  if (flowTo && product) exposeFlowLine({ product, to: flowTo, styles: loaded.styles ?? '' });
338
427
  else if (motionName && drawnPart) exposeMotion({ drawn: drawnPart, name: motionName, styles: loaded.styles ?? '' });
339
428
  else if (drawnPart?.part.rules?.length) runPartRules(drawnPart, loaded.styles ?? '');
429
+
430
+ if (!flowTo && !motionName && drawnPart) listenForDrive(drawnPart, loaded.styles ?? '');
340
431
  }
341
432
 
342
433
  const place = () => {
@@ -355,6 +446,7 @@ const drawFrame = async () => {
355
446
  if (fit) reported.observe(root);
356
447
  reporting = true;
357
448
  report();
449
+ reportPainted();
358
450
 
359
451
  addEventListener('keydown', (event) => {
360
452
  if (event.key === 'Escape') parent.postMessage({ type: 'design-explore:escape', variant: key }, '*');
@@ -371,4 +463,5 @@ void drawFrame().catch((error: unknown) => {
371
463
  fail(`design-explore: frame failed\n${messageOf(error)}`);
372
464
  reporting = true;
373
465
  report();
466
+ reportPainted();
374
467
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ethlete/cli",
3
- "version": "3.0.0-next.25",
3
+ "version": "3.0.0-next.26",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/ethlete-io/ethdk",
@@ -16,4 +16,6 @@ export declare const DRAWING_COMPILER_OPTIONS: {
16
16
  * this config for itself. The same config goes to `.ethlete/design/tsconfig.json`, the nearest
17
17
  * tsconfig an editor finds for a call file.
18
18
  */
19
+ /** Writes `text` unless the file already holds it: Vite reloads every frame on any write to a tsconfig it watches. */
20
+ export declare const writeTsconfigText: (path: string, text: string) => void;
19
21
  export declare const writeCallsTsconfig: (target: string) => string;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.writeCallsTsconfig = exports.DRAWING_COMPILER_OPTIONS = void 0;
3
+ exports.writeCallsTsconfig = exports.writeTsconfigText = exports.DRAWING_COMPILER_OPTIONS = void 0;
4
4
  const fs_1 = require("fs");
5
5
  const path_1 = require("path");
6
6
  const paths_1 = require("./paths");
@@ -23,6 +23,13 @@ exports.DRAWING_COMPILER_OPTIONS = {
23
23
  * this config for itself. The same config goes to `.ethlete/design/tsconfig.json`, the nearest
24
24
  * tsconfig an editor finds for a call file.
25
25
  */
26
+ /** Writes `text` unless the file already holds it: Vite reloads every frame on any write to a tsconfig it watches. */
27
+ const writeTsconfigText = (path, text) => {
28
+ if ((0, fs_1.existsSync)(path) && (0, fs_1.readFileSync)(path, 'utf8') === text)
29
+ return;
30
+ (0, fs_1.writeFileSync)(path, text);
31
+ };
32
+ exports.writeTsconfigText = writeTsconfigText;
26
33
  const writeCallsTsconfig = (target) => {
27
34
  const work = (0, paths_1.workRootOf)(target);
28
35
  const path = (0, path_1.resolve)(work, 'tsconfig.json');
@@ -36,9 +43,9 @@ const writeCallsTsconfig = (target) => {
36
43
  const text = `${JSON.stringify(config, null, 2)}\n`;
37
44
  const editorPath = (0, path_1.resolve)(target, paths_1.DESIGN_DIR, 'tsconfig.json');
38
45
  (0, fs_1.mkdirSync)(work, { recursive: true });
39
- (0, fs_1.writeFileSync)(path, text);
46
+ (0, exports.writeTsconfigText)(path, text);
40
47
  (0, fs_1.mkdirSync)((0, path_1.dirname)(editorPath), { recursive: true });
41
- (0, fs_1.writeFileSync)(editorPath, text);
48
+ (0, exports.writeTsconfigText)(editorPath, text);
42
49
  return path;
43
50
  };
44
51
  exports.writeCallsTsconfig = writeCallsTsconfig;
@@ -1 +1 @@
1
- {"version":3,"file":"calls-tsconfig.js","sourceRoot":"","sources":["../../../../../libs/cli/src/lib/design/calls-tsconfig.ts"],"names":[],"mappings":";;;AAAA,2BAA8C;AAC9C,+BAAwC;AACxC,mCAA8F;AAC9F,uDAAkE;AAErD,QAAA,wBAAwB,GAAG;IACtC,KAAK,EAAE,EAAE;IACT,MAAM,EAAE,QAAQ;IAChB,MAAM,EAAE,UAAU;IAClB,gBAAgB,EAAE,SAAS;IAC3B,GAAG,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC;IACtB,MAAM,EAAE,IAAI;IACZ,wBAAwB,EAAE,IAAI;IAC9B,eAAe,EAAE,IAAI;IACrB,YAAY,EAAE,IAAI;IAClB,MAAM,EAAE,IAAI;CACb,CAAC;AAEF;;;;;GAKG;AACI,MAAM,kBAAkB,GAAG,CAAC,MAAc,EAAE,EAAE;IACnD,MAAM,IAAI,GAAG,IAAA,kBAAU,EAAC,MAAM,CAAC,CAAC;IAChC,MAAM,IAAI,GAAG,IAAA,cAAO,EAAC,IAAI,EAAE,eAAe,CAAC,CAAC;IAC5C,MAAM,UAAU,GAAG,IAAA,cAAO,EAAC,IAAA,0BAAkB,GAAE,EAAE,gBAAgB,CAAC,CAAC;IACnE,MAAM,SAAS,GAAG,IAAA,cAAO,EAAC,IAAA,0BAAkB,GAAE,EAAE,iBAAiB,CAAC,CAAC;IACnE,MAAM,SAAS,GAAG,IAAA,6BAAW,EAAC,EAAE,QAAQ,EAAE,IAAA,kCAAgB,EAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAA,kBAAU,EAAC,MAAM,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;IAE9G,MAAM,MAAM,GAAG;QACb,eAAe,kCACV,gCAAwB,KAC3B,KAAK,kCAAO,SAAS,KAAE,iBAAiB,EAAE,CAAC,UAAU,CAAC,MACvD;QACD,OAAO,EAAE,CAAC,GAAG,IAAA,mBAAW,EAAC,MAAM,CAAC,UAAU,EAAE,UAAU,EAAE,SAAS,CAAC;KACnE,CAAC;IAEF,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;IACpD,MAAM,UAAU,GAAG,IAAA,cAAO,EAAC,MAAM,EAAE,kBAAU,EAAE,eAAe,CAAC,CAAC;IAEhE,IAAA,cAAS,EAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACrC,IAAA,kBAAa,EAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC1B,IAAA,cAAS,EAAC,IAAA,cAAO,EAAC,UAAU,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,IAAA,kBAAa,EAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IAEhC,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAxBW,QAAA,kBAAkB,sBAwB7B"}
1
+ {"version":3,"file":"calls-tsconfig.js","sourceRoot":"","sources":["../../../../../libs/cli/src/lib/design/calls-tsconfig.ts"],"names":[],"mappings":";;;AAAA,2BAAwE;AACxE,+BAAwC;AACxC,mCAA8F;AAC9F,uDAAkE;AAErD,QAAA,wBAAwB,GAAG;IACtC,KAAK,EAAE,EAAE;IACT,MAAM,EAAE,QAAQ;IAChB,MAAM,EAAE,UAAU;IAClB,gBAAgB,EAAE,SAAS;IAC3B,GAAG,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC;IACtB,MAAM,EAAE,IAAI;IACZ,wBAAwB,EAAE,IAAI;IAC9B,eAAe,EAAE,IAAI;IACrB,YAAY,EAAE,IAAI;IAClB,MAAM,EAAE,IAAI;CACb,CAAC;AAEF;;;;;GAKG;AACH,sHAAsH;AAC/G,MAAM,iBAAiB,GAAG,CAAC,IAAY,EAAE,IAAY,EAAE,EAAE;IAC9D,IAAI,IAAA,eAAU,EAAC,IAAI,CAAC,IAAI,IAAA,iBAAY,EAAC,IAAI,EAAE,MAAM,CAAC,KAAK,IAAI;QAAE,OAAO;IAEpE,IAAA,kBAAa,EAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AAC5B,CAAC,CAAC;AAJW,QAAA,iBAAiB,qBAI5B;AAEK,MAAM,kBAAkB,GAAG,CAAC,MAAc,EAAE,EAAE;IACnD,MAAM,IAAI,GAAG,IAAA,kBAAU,EAAC,MAAM,CAAC,CAAC;IAChC,MAAM,IAAI,GAAG,IAAA,cAAO,EAAC,IAAI,EAAE,eAAe,CAAC,CAAC;IAC5C,MAAM,UAAU,GAAG,IAAA,cAAO,EAAC,IAAA,0BAAkB,GAAE,EAAE,gBAAgB,CAAC,CAAC;IACnE,MAAM,SAAS,GAAG,IAAA,cAAO,EAAC,IAAA,0BAAkB,GAAE,EAAE,iBAAiB,CAAC,CAAC;IACnE,MAAM,SAAS,GAAG,IAAA,6BAAW,EAAC,EAAE,QAAQ,EAAE,IAAA,kCAAgB,EAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAA,kBAAU,EAAC,MAAM,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;IAE9G,MAAM,MAAM,GAAG;QACb,eAAe,kCACV,gCAAwB,KAC3B,KAAK,kCAAO,SAAS,KAAE,iBAAiB,EAAE,CAAC,UAAU,CAAC,MACvD;QACD,OAAO,EAAE,CAAC,GAAG,IAAA,mBAAW,EAAC,MAAM,CAAC,UAAU,EAAE,UAAU,EAAE,SAAS,CAAC;KACnE,CAAC;IAEF,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC;IACpD,MAAM,UAAU,GAAG,IAAA,cAAO,EAAC,MAAM,EAAE,kBAAU,EAAE,eAAe,CAAC,CAAC;IAEhE,IAAA,cAAS,EAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACrC,IAAA,yBAAiB,EAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC9B,IAAA,cAAS,EAAC,IAAA,cAAO,EAAC,UAAU,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,IAAA,yBAAiB,EAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IAEpC,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAxBW,QAAA,kBAAkB,sBAwB7B"}
@@ -4,6 +4,7 @@
4
4
  * error draws an overlay on top of the drawing, so a screenshot of one looks like a design -
5
5
  * never screenshot before this says `ok`. With `--call`, the call's status follows the check, but
6
6
  * a check that cannot run (no browser, no server, a stale server) leaves the status it found.
7
+ * `--receipt <path>` also writes what the check said and whether it passed to that file.
7
8
  */
8
9
  export declare const checkDesign: (options: {
9
10
  target: string;
@@ -11,6 +11,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.checkDesign = void 0;
13
13
  const child_process_1 = require("child_process");
14
+ const fs_1 = require("fs");
15
+ const path_1 = require("path");
14
16
  const browser_1 = require("./browser");
15
17
  const calls_tsconfig_1 = require("./calls-tsconfig");
16
18
  const paths_1 = require("./paths");
@@ -186,7 +188,7 @@ const runChecks = (options) => __awaiter(void 0, void 0, void 0, function* () {
186
188
  const wanted = (_c = flags['--variant']) === null || _c === void 0 ? void 0 : _c[0];
187
189
  const tsconfig = '--tsconfig' in flags ? ((_e = (_d = flags['--tsconfig']) === null || _d === void 0 ? void 0 : _d[0]) !== null && _e !== void 0 ? _e : (0, calls_tsconfig_1.writeCallsTsconfig)(target)) : null;
188
190
  if (lintFiles.length === 0 && !tsconfig && !slug) {
189
- console.error(`usage: ${invocation} check [--lint <file>…] [--tsconfig [path]] [--call <slug> [--variant <key>]]`);
191
+ console.error(`usage: ${invocation} check [--lint <file>…] [--tsconfig [path]] [--call <slug> [--variant <key>]] [--receipt <path>]`);
190
192
  return 2;
191
193
  }
192
194
  if (lintFiles.length > 0) {
@@ -218,28 +220,83 @@ const runChecks = (options) => __awaiter(void 0, void 0, void 0, function* () {
218
220
  const base = (_f = process.env['DE_URL']) !== null && _f !== void 0 ? _f : `http://localhost:${config ? (0, paths_1.portOf)(config) : paths_1.DEFAULT_PORT}`;
219
221
  return runBrowser({ target, base, slug, wanted, fails });
220
222
  });
223
+ const receiptArgvOf = (argv) => {
224
+ var _a;
225
+ const index = argv.indexOf('--receipt');
226
+ if (index === -1)
227
+ return { receipt: null, rest: argv };
228
+ return { receipt: (_a = argv[index + 1]) !== null && _a !== void 0 ? _a : null, rest: [...argv.slice(0, index), ...argv.slice(index + 2)] };
229
+ };
230
+ const writeReceipt = (options) => {
231
+ var _a, _b, _c, _d;
232
+ const flags = parse(options.argv, ['--lint', '--tsconfig', '--call', '--variant', '--product']);
233
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(options.path), { recursive: true });
234
+ (0, fs_1.writeFileSync)(options.path, JSON.stringify({
235
+ call: (_b = (_a = flags['--call']) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : null,
236
+ variant: (_d = (_c = flags['--variant']) === null || _c === void 0 ? void 0 : _c[0]) !== null && _d !== void 0 ? _d : null,
237
+ ok: options.ok,
238
+ at: Math.floor(Date.now() / 1000),
239
+ said: options.said.trim(),
240
+ }));
241
+ };
221
242
  /**
222
243
  * Finds out why a call does not render, in the cheapest order: eslint on the named files, then
223
244
  * `tsc --noEmit` over the checkout's calls, then the live page in a headless browser. A compile
224
245
  * error draws an overlay on top of the drawing, so a screenshot of one looks like a design -
225
246
  * never screenshot before this says `ok`. With `--call`, the call's status follows the check, but
226
247
  * a check that cannot run (no browser, no server, a stale server) leaves the status it found.
248
+ * `--receipt <path>` also writes what the check said and whether it passed to that file.
227
249
  */
228
250
  const checkDesign = (options) => __awaiter(void 0, void 0, void 0, function* () {
251
+ const { receipt, rest } = receiptArgvOf(options.argv);
252
+ if (!receipt)
253
+ return checkWithStatus(options);
254
+ const said = [];
255
+ const log = console.log;
256
+ const error = console.error;
257
+ const tee = (write) => (...lines) => {
258
+ said.push(lines.map(String).join(' '));
259
+ write(...lines);
260
+ };
261
+ console.log = tee(log);
262
+ console.error = tee(error);
263
+ try {
264
+ const code = yield checkWithStatus(Object.assign(Object.assign({}, options), { argv: rest }));
265
+ writeReceipt({ path: receipt, argv: rest, ok: code === 0, said: said.join('\n') });
266
+ return code;
267
+ }
268
+ catch (thrown) {
269
+ said.push(thrown instanceof Error ? thrown.message : String(thrown));
270
+ writeReceipt({ path: receipt, argv: rest, ok: false, said: said.join('\n') });
271
+ throw thrown;
272
+ }
273
+ finally {
274
+ console.log = log;
275
+ console.error = error;
276
+ }
277
+ });
278
+ exports.checkDesign = checkDesign;
279
+ const checkWithStatus = (options) => __awaiter(void 0, void 0, void 0, function* () {
229
280
  var _a, _b;
230
- const { target, argv, invocation } = options;
281
+ const { target, invocation } = options;
282
+ const complete = options.argv.includes('--complete');
283
+ const argv = options.argv.filter((argument) => argument !== '--complete');
231
284
  const flags = parse(argv, ['--lint', '--tsconfig', '--call', '--variant', '--product']);
232
285
  const products = flags['--product'];
233
286
  if (products) {
234
287
  if (products.length === 0) {
235
- console.error(`usage: ${invocation} check --product <name>…`);
288
+ console.error(`usage: ${invocation} check --product <name>… [--complete]`);
236
289
  return 2;
237
290
  }
238
- return (0, check_system_1.checkSystemRendered)({ target, config: (_a = (0, paths_1.readConfig)(target)) !== null && _a !== void 0 ? _a : {}, products });
291
+ return (0, check_system_1.checkSystemRendered)({ target, config: (_a = (0, paths_1.readConfig)(target)) !== null && _a !== void 0 ? _a : {}, products, complete });
239
292
  }
240
293
  const config = argv.length === 0 ? (0, paths_1.readConfig)(target) : null;
241
294
  if ((config === null || config === void 0 ? void 0 : config.products) && Object.keys(config.products).length > 0)
242
- return (0, check_system_1.checkSystemRendered)({ target, config, products: Object.keys(config.products) });
295
+ return (0, check_system_1.checkSystemRendered)({ target, config, products: Object.keys(config.products), complete });
296
+ if (complete) {
297
+ console.error(`usage: ${invocation} check [--product <name>…] --complete - --complete checks products only`);
298
+ return 2;
299
+ }
243
300
  const slug = (_b = flags['--call']) === null || _b === void 0 ? void 0 : _b[0];
244
301
  let failure = '';
245
302
  let blocked = false;
@@ -280,5 +337,4 @@ const checkDesign = (options) => __awaiter(void 0, void 0, void 0, function* ()
280
337
  throw error;
281
338
  }
282
339
  });
283
- exports.checkDesign = checkDesign;
284
340
  //# sourceMappingURL=check.js.map