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

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 (111) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/design/define-call.ts +43 -0
  3. package/design/define-part.ts +28 -8
  4. package/design/embed-page.ts +37 -4
  5. package/design/icon-sheet.ts +151 -0
  6. package/design/mark-part.ts +79 -2
  7. package/design/motion-runtime.ts +212 -5
  8. package/design/spring.ts +53 -0
  9. package/design/token-sheet.ts +87 -0
  10. package/design/web/frame.ts +139 -24
  11. package/package.json +1 -1
  12. package/src/lib/design/calls-tsconfig.js +1 -2
  13. package/src/lib/design/calls-tsconfig.js.map +1 -1
  14. package/src/lib/design/design-command.js +34 -4
  15. package/src/lib/design/design-command.js.map +1 -1
  16. package/src/lib/design/force-interactions.d.ts +2 -1
  17. package/src/lib/design/force-interactions.js +7 -4
  18. package/src/lib/design/force-interactions.js.map +1 -1
  19. package/src/lib/design/mcp-command.d.ts +60 -0
  20. package/src/lib/design/mcp-command.js +327 -0
  21. package/src/lib/design/mcp-command.js.map +1 -0
  22. package/src/lib/design/motion-command.d.ts +71 -0
  23. package/src/lib/design/motion-command.js +314 -0
  24. package/src/lib/design/motion-command.js.map +1 -0
  25. package/src/lib/design/motion-endpoint.d.ts +61 -0
  26. package/src/lib/design/motion-endpoint.js +133 -0
  27. package/src/lib/design/motion-endpoint.js.map +1 -0
  28. package/src/lib/design/motion-flow.d.ts +5 -0
  29. package/src/lib/design/motion-flow.js +29 -0
  30. package/src/lib/design/motion-flow.js.map +1 -0
  31. package/src/lib/design/motion-shot.d.ts +43 -0
  32. package/src/lib/design/motion-shot.js +97 -0
  33. package/src/lib/design/motion-shot.js.map +1 -0
  34. package/src/lib/design/ready-shots.d.ts +7 -0
  35. package/src/lib/design/ready-shots.js +22 -1
  36. package/src/lib/design/ready-shots.js.map +1 -1
  37. package/src/lib/design/render-packages.d.ts +9 -0
  38. package/src/lib/design/render-packages.js +13 -3
  39. package/src/lib/design/render-packages.js.map +1 -1
  40. package/src/lib/design/render.js +3 -3
  41. package/src/lib/design/render.js.map +1 -1
  42. package/src/lib/design/serve.js +34 -3
  43. package/src/lib/design/serve.js.map +1 -1
  44. package/src/lib/design/shot-command.d.ts +8 -2
  45. package/src/lib/design/shot-command.js +12 -8
  46. package/src/lib/design/shot-command.js.map +1 -1
  47. package/src/lib/design/shot.d.ts +16 -0
  48. package/src/lib/design/shot.js +68 -4
  49. package/src/lib/design/shot.js.map +1 -1
  50. package/src/lib/design/sketch-command.js +78 -16
  51. package/src/lib/design/sketch-command.js.map +1 -1
  52. package/src/lib/design/studio-api.d.ts +19 -0
  53. package/src/lib/design/studio-api.js +52 -1
  54. package/src/lib/design/studio-api.js.map +1 -1
  55. package/src/lib/design/system/complete-command.d.ts +1 -1
  56. package/src/lib/design/system/complete-command.js +76 -6
  57. package/src/lib/design/system/complete-command.js.map +1 -1
  58. package/src/lib/design/system/complete.d.ts +107 -9
  59. package/src/lib/design/system/complete.js +592 -141
  60. package/src/lib/design/system/complete.js.map +1 -1
  61. package/src/lib/design/system/exceptions.d.ts +59 -0
  62. package/src/lib/design/system/exceptions.js +132 -0
  63. package/src/lib/design/system/exceptions.js.map +1 -0
  64. package/src/lib/design/system/flows/flow.d.ts +3 -0
  65. package/src/lib/design/system/flows/flow.js +5 -3
  66. package/src/lib/design/system/flows/flow.js.map +1 -1
  67. package/src/lib/design/system/handoff.d.ts +11 -0
  68. package/src/lib/design/system/handoff.js +30 -0
  69. package/src/lib/design/system/handoff.js.map +1 -0
  70. package/src/lib/design/system/inventory-approval.d.ts +41 -0
  71. package/src/lib/design/system/inventory-approval.js +71 -0
  72. package/src/lib/design/system/inventory-approval.js.map +1 -0
  73. package/src/lib/design/system/inventory.d.ts +65 -0
  74. package/src/lib/design/system/inventory.js +168 -0
  75. package/src/lib/design/system/inventory.js.map +1 -0
  76. package/src/lib/design/system/list-system.js +9 -2
  77. package/src/lib/design/system/list-system.js.map +1 -1
  78. package/src/lib/design/system/render-check.js +19 -1
  79. package/src/lib/design/system/render-check.js.map +1 -1
  80. package/src/lib/design/system/resolve-product.d.ts +2 -3
  81. package/src/lib/design/system/resolve-product.js +4 -6
  82. package/src/lib/design/system/resolve-product.js.map +1 -1
  83. package/src/lib/design/system/rules/stale-replace.js +41 -2
  84. package/src/lib/design/system/rules/stale-replace.js.map +1 -1
  85. package/src/lib/design/system/rules/token-selectors.js +1 -1
  86. package/src/lib/design/system/rules/token-selectors.js.map +1 -1
  87. package/src/lib/design/system/sketch/host-edit.d.ts +4 -0
  88. package/src/lib/design/system/sketch/host-edit.js +68 -0
  89. package/src/lib/design/system/sketch/host-edit.js.map +1 -0
  90. package/src/lib/design/system/sketch/merge.d.ts +3 -2
  91. package/src/lib/design/system/sketch/merge.js +47 -12
  92. package/src/lib/design/system/sketch/merge.js.map +1 -1
  93. package/src/lib/design/system/sketch/merged-parts.d.ts +10 -0
  94. package/src/lib/design/system/sketch/merged-parts.js +24 -3
  95. package/src/lib/design/system/sketch/merged-parts.js.map +1 -1
  96. package/src/lib/design/system/sketch/no-visible-change.d.ts +6 -0
  97. package/src/lib/design/system/sketch/no-visible-change.js +25 -1
  98. package/src/lib/design/system/sketch/no-visible-change.js.map +1 -1
  99. package/src/lib/design/system/sketch/overlay.js +11 -3
  100. package/src/lib/design/system/sketch/overlay.js.map +1 -1
  101. package/src/lib/design/system/sketch/sketch.d.ts +32 -0
  102. package/src/lib/design/system/sketch/sketch.js +73 -18
  103. package/src/lib/design/system/sketch/sketch.js.map +1 -1
  104. package/src/lib/design/system/sketch/unused-closure.d.ts +12 -0
  105. package/src/lib/design/system/sketch/unused-closure.js +185 -0
  106. package/src/lib/design/system/sketch/unused-closure.js.map +1 -0
  107. package/src/lib/design/system/system-modules.js +6 -9
  108. package/src/lib/design/system/system-modules.js.map +1 -1
  109. package/src/lib/design/system/system-tsconfig.d.ts +2 -1
  110. package/src/lib/design/system/system-tsconfig.js +9 -5
  111. package/src/lib/design/system/system-tsconfig.js.map +1 -1
@@ -105,14 +105,23 @@ const measure = (root: Element) => {
105
105
  };
106
106
  };
107
107
 
108
- /** What a switch of `root` to `markup` does: `motion` names its `play` and `viewTransition`, `clock` the time `ctx.at` runs on. */
109
- export type StateSwitch = { root: Element; markup: string; motion?: Motion; clock?: MotionClock };
108
+ /**
109
+ * What a switch of `root` to `markup` does: `motion` names its `play` and `viewTransition`, `clock` the time `ctx.at`
110
+ * runs on, and `transitioned` hears the View Transition when one starts.
111
+ */
112
+ export type StateSwitch = {
113
+ root: Element;
114
+ markup: string;
115
+ motion?: Motion;
116
+ clock?: MotionClock;
117
+ transitioned?: (transition: ViewTransition) => void;
118
+ };
110
119
 
111
120
  /**
112
121
  * Morphs `root` to `markup`, inside a View Transition when the motion asks for one and the browser has it, then runs
113
122
  * the motion's `play`. `ctx.rect` measures in the DOM before the morph. Resolves once `play` has run.
114
123
  */
115
- export const switchState = ({ root, markup, motion, clock }: StateSwitch): Promise<void> => {
124
+ export const switchState = ({ root, markup, motion, clock, transitioned }: StateSwitch): Promise<void> => {
116
125
  const rect = motion?.play ? measure(root) : null;
117
126
  const apply = () => morph(root, markup);
118
127
  const play = () => {
@@ -125,8 +134,13 @@ export const switchState = ({ root, markup, motion, clock }: StateSwitch): Promi
125
134
  };
126
135
  const document = root.ownerDocument;
127
136
 
128
- if (motion?.viewTransition && typeof document.startViewTransition === 'function')
129
- return document.startViewTransition(apply).updateCallbackDone.then(play);
137
+ if (motion?.viewTransition && typeof document.startViewTransition === 'function') {
138
+ const transition = document.startViewTransition(apply);
139
+
140
+ transitioned?.(transition);
141
+
142
+ return transition.updateCallbackDone.then(play);
143
+ }
130
144
 
131
145
  apply();
132
146
  play();
@@ -134,6 +148,199 @@ export const switchState = ({ root, markup, motion, clock }: StateSwitch): Promi
134
148
  return Promise.resolve();
135
149
  };
136
150
 
151
+ /** One animation of a motion, in ms from the start of the motion. `iterations` and `end` are `null` when it never ends. */
152
+ export type MotionTrack = {
153
+ element: string;
154
+ kind: 'transition' | 'animation' | 'script';
155
+ name?: string;
156
+ properties: string[];
157
+ start: number;
158
+ delay: number;
159
+ duration: number;
160
+ iterations: number | null;
161
+ easing: string;
162
+ end: number | null;
163
+ };
164
+
165
+ /** A motion paused at its start: `seek` moves it forward to a time in ms, `tracks` lists what ran so far. */
166
+ export type SteppedMotion = {
167
+ seek: (ms: number) => void;
168
+ tracks: () => MotionTrack[];
169
+ end: () => number;
170
+ bounds: () => MotionBox;
171
+ };
172
+
173
+ /** A rectangle in page coordinates. */
174
+ export type MotionBox = { left: number; top: number; right: number; bottom: number };
175
+
176
+ const KEYFRAME_FIELDS = new Set(['offset', 'computedOffset', 'easing', 'composite']);
177
+
178
+ const elementOf = (element: Element | null | undefined) =>
179
+ element?.hasAttribute('data-et-key')
180
+ ? `[data-et-key="${element.getAttribute('data-et-key')}"]`
181
+ : element
182
+ ? [
183
+ element.tagName.toLowerCase(),
184
+ element.id ? `#${element.id}` : '',
185
+ ...[...element.classList].map((name) => `.${name}`),
186
+ ].join('')
187
+ : '';
188
+
189
+ const trackOf = (animation: Animation, start: number): MotionTrack => {
190
+ const effect = animation.effect as KeyframeEffect | null;
191
+ const timing = effect?.getComputedTiming();
192
+ const keyframes = effect && typeof effect.getKeyframes === 'function' ? effect.getKeyframes() : [];
193
+ const own = effect?.getTiming().easing ?? 'linear';
194
+ const keyed = keyframes.find((frame) => frame.easing && frame.easing !== 'linear')?.easing;
195
+ const iterations = timing?.iterations ?? 1;
196
+ const endTime = Number(timing?.endTime ?? 0);
197
+ const properties = [
198
+ ...new Set(
199
+ keyframes.flatMap((frame) =>
200
+ Object.keys(frame)
201
+ .filter((field) => !KEYFRAME_FIELDS.has(field))
202
+ .map((field) => field.replace(/[A-Z]/gu, (letter) => `-${letter.toLowerCase()}`)),
203
+ ),
204
+ ),
205
+ ];
206
+ const kind = 'transitionProperty' in animation ? 'transition' : 'animationName' in animation ? 'animation' : 'script';
207
+
208
+ return {
209
+ element: `${elementOf(effect?.target)}${effect?.pseudoElement ?? ''}`,
210
+ kind,
211
+ ...(kind === 'animation' ? { name: String((animation as CSSAnimation).animationName) } : {}),
212
+ properties: kind === 'transition' ? [String((animation as CSSTransition).transitionProperty)] : properties,
213
+ start,
214
+ delay: Number(timing?.delay ?? 0),
215
+ duration: Number(timing?.duration ?? 0),
216
+ iterations: Number.isFinite(iterations) ? iterations : null,
217
+ easing: own === 'linear' ? (keyed ?? own) : own,
218
+ end: Number.isFinite(endTime) ? start + endTime : null,
219
+ };
220
+ };
221
+
222
+ /**
223
+ * Switches `root` to `markup` with `motion` as `switchState` does, on a manual clock, and pauses every animation it
224
+ * starts, so a caller can step through the motion and draw it at any time from its start. A motion with `interrupt`
225
+ * switches back to `reverse.markup` at that time, with `reverse.motion` (its `ctx.at` counts from the interrupt and a
226
+ * View Transition is not used); a forward `ctx.at` step after it is dropped and a cancelled animation ends there.
227
+ */
228
+ export const stepMotion = (options: {
229
+ root: Element;
230
+ markup: string;
231
+ motion: Motion;
232
+ reverse?: { markup: string; motion?: Motion };
233
+ }): Promise<SteppedMotion> => {
234
+ const { root, markup, motion, reverse } = options;
235
+ const document = root.ownerDocument;
236
+ const clock = manualClock();
237
+ const started = new Map<Animation, MotionTrack>();
238
+ const cut = new Set<Animation>();
239
+ const interrupt = motion.interrupt;
240
+ let interrupted = false;
241
+ let lastTask = 0;
242
+ let ready: Promise<unknown> = Promise.resolve();
243
+
244
+ if (interrupt !== undefined && !reverse)
245
+ throw new Error('design-explore: a motion with interrupt needs the markup of its from state to switch back to');
246
+
247
+ const collect = (start: number) => {
248
+ for (const animation of started.keys()) {
249
+ const track = started.get(animation);
250
+
251
+ if (cut.has(animation) || animation.playState !== 'idle' || !track) continue;
252
+
253
+ cut.add(animation);
254
+ track.end = Math.min(track.end ?? start, start);
255
+ }
256
+
257
+ for (const animation of document.getAnimations()) {
258
+ if (started.has(animation)) continue;
259
+
260
+ animation.pause();
261
+ started.set(animation, trackOf(animation, start));
262
+ }
263
+ };
264
+
265
+ const pose = (ms: number) => {
266
+ for (const [animation, track] of started)
267
+ if (!cut.has(animation)) animation.currentTime = Math.max(0, ms - track.start);
268
+ };
269
+
270
+ const timedFrom = (offset: number, live: () => boolean): MotionClock => ({
271
+ ...clock,
272
+ at: (ms, fn) =>
273
+ clock.at(offset + ms, () => {
274
+ if (!live()) return;
275
+
276
+ lastTask = Math.max(lastTask, offset + ms);
277
+ pose(offset + ms);
278
+ fn();
279
+ collect(offset + ms);
280
+ }),
281
+ });
282
+ const timed = timedFrom(0, () => !interrupted);
283
+
284
+ return switchState({
285
+ root,
286
+ markup,
287
+ motion,
288
+ clock: timed,
289
+ transitioned: (transition) => {
290
+ ready = transition.ready.catch(() => undefined);
291
+ },
292
+ })
293
+ .then(() => ready)
294
+ .then(() => {
295
+ collect(0);
296
+
297
+ if (interrupt !== undefined && reverse) {
298
+ clock.at(interrupt, () => {
299
+ interrupted = true;
300
+ lastTask = Math.max(lastTask, interrupt);
301
+ pose(interrupt);
302
+ void switchState({
303
+ root,
304
+ markup: reverse.markup,
305
+ motion: reverse.motion ? { ...reverse.motion, viewTransition: false } : undefined,
306
+ clock: timedFrom(interrupt, () => true),
307
+ });
308
+ collect(interrupt);
309
+ });
310
+ }
311
+
312
+ return {
313
+ seek: (ms) => {
314
+ if (ms < clock.now())
315
+ throw new Error(`design-explore: a motion steps forward only, from ${clock.now()} ms to ${ms} ms`);
316
+
317
+ clock.advance(ms - clock.now());
318
+
319
+ pose(ms);
320
+ },
321
+ tracks: () => [...started.values()],
322
+ end: () => Math.max(lastTask, ...[...started.values()].map((track) => track.end ?? 0)),
323
+ bounds: () => {
324
+ const view = document.defaultView;
325
+ const boxes = [
326
+ root,
327
+ ...[...started.keys()].map((animation) => (animation.effect as KeyframeEffect | null)?.target),
328
+ ]
329
+ .filter((element): element is Element => !!element)
330
+ .map((element) => element.getBoundingClientRect())
331
+ .filter((rect, index) => index === 0 || rect.width > 0 || rect.height > 0);
332
+
333
+ return {
334
+ left: Math.min(...boxes.map((rect) => rect.left)) + (view?.scrollX ?? 0),
335
+ top: Math.min(...boxes.map((rect) => rect.top)) + (view?.scrollY ?? 0),
336
+ right: Math.max(...boxes.map((rect) => rect.right)) + (view?.scrollX ?? 0),
337
+ bottom: Math.max(...boxes.map((rect) => rect.bottom)) + (view?.scrollY ?? 0),
338
+ };
339
+ },
340
+ };
341
+ });
342
+ };
343
+
137
344
  /**
138
345
  * How `runRules` reaches the part: the state drawn first, its `rules` and `motions`, and the markup of a state. `clock`
139
346
  * makes the clock of each switch, `switched` hears each finished switch and `failed` each error.
@@ -0,0 +1,53 @@
1
+ /** A spring as a CSS easing: `easing` is a `linear(...)` function, `duration` the ms until it settles; as a string, `<duration>ms <easing>`. */
2
+ export type Spring = { easing: string; duration: number; toString: () => string };
3
+
4
+ const SETTLED = 0.001;
5
+ const LONGEST_MS = 10000;
6
+
7
+ const positionOf = (damping: number, frequency: number) => {
8
+ const natural = 2 * Math.PI * frequency;
9
+
10
+ if (damping < 1) {
11
+ const damped = natural * Math.sqrt(1 - damping * damping);
12
+ const decay = damping * natural;
13
+
14
+ return (seconds: number) =>
15
+ 1 - Math.exp(-decay * seconds) * (Math.cos(damped * seconds) + (decay / damped) * Math.sin(damped * seconds));
16
+ }
17
+
18
+ if (damping === 1) return (seconds: number) => 1 - Math.exp(-natural * seconds) * (1 + natural * seconds);
19
+
20
+ const root = Math.sqrt(damping * damping - 1);
21
+ const slow = -natural * (damping - root);
22
+ const fast = -natural * (damping + root);
23
+
24
+ return (seconds: number) => 1 - (fast * Math.exp(slow * seconds) - slow * Math.exp(fast * seconds)) / (fast - slow);
25
+ };
26
+
27
+ /**
28
+ * Samples a spring that moves from 0 to 1 at rest, for a motion token in `foundations`, e.g.
29
+ * `--motion-capsule: ${spring({ damping: 0.6, frequency: 2 })}`. `damping` is the damping ratio (below 1 it
30
+ * overshoots) and `frequency` the undamped frequency in Hz.
31
+ */
32
+ export const spring = (options: { damping: number; frequency: number }): Spring => {
33
+ const { damping, frequency } = options;
34
+
35
+ if (!(damping > 0) || !(frequency > 0) || !Number.isFinite(damping) || !Number.isFinite(frequency))
36
+ throw new Error(
37
+ `design-explore: spring() needs a damping and a frequency above 0, got ${damping} and ${frequency}`,
38
+ );
39
+
40
+ const position = positionOf(damping, frequency);
41
+ let last = 0;
42
+
43
+ for (let ms = 0; ms <= LONGEST_MS; ms++) if (Math.abs(1 - position(ms / 1000)) >= SETTLED) last = ms;
44
+
45
+ const duration = Math.max(1, last + 1);
46
+ const count = Math.min(200, Math.max(10, Math.ceil(duration / 16)));
47
+ const points = Array.from({ length: count + 1 }, (_, index) =>
48
+ index === count ? 1 : Math.round(position(((index / count) * duration) / 1000) * 10000) / 10000,
49
+ );
50
+ const easing = `linear(${points.join(', ')})`;
51
+
52
+ return { easing, duration, toString: () => `${duration}ms ${easing}` };
53
+ };
@@ -0,0 +1,87 @@
1
+ import { Drawing, html } from './define-call';
2
+
3
+ export type TokenSheetOptions = {
4
+ title: string;
5
+ note?: string;
6
+ scope?: string;
7
+ css: string;
8
+ sample: string;
9
+ sampleOf: (name: string) => string;
10
+ sampleClass?: string;
11
+ only?: (name: string) => boolean;
12
+ pixels?: boolean;
13
+ styles?: string;
14
+ setup?: (root: HTMLElement) => void;
15
+ };
16
+
17
+ const SHEET_STYLES = `
18
+ .ts-root { position: relative; display: flex; flex-direction: column; gap: var(--space-4, 1.6rem); width: 960px; padding: var(--space-6, 2.4rem); box-sizing: border-box; background: var(--surface, oklch(0.17 0 0)); color: var(--text, oklch(0.94 0 0)); font-family: var(--font-ui, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif); font-size: var(--text-md, 1.3rem); line-height: var(--text-md-line, 1.8rem); }
19
+ .ts-title { margin: 0; font-size: var(--text-xl, 1.7rem); line-height: var(--text-xl-line, 2.2rem); font-weight: var(--weight-strong, 600); }
20
+ .ts-note { margin: 0; max-width: 64rem; color: var(--text-muted, oklch(0.72 0 0)); }
21
+ .ts-panel { margin: 0; padding: var(--space-4, 1.6rem); background: var(--surface, oklch(0.17 0 0)); border: 1px solid var(--surface-border, oklch(0.34 0 0)); border-radius: var(--radius-control-lg, 1rem); }
22
+ .ts-list { list-style: none; display: flex; flex-direction: column; gap: var(--space-1, 0.4rem); }
23
+ .ts-row { display: grid; grid-template-columns: 200px 240px 1fr; align-items: center; gap: var(--space-3, 1.2rem); min-height: var(--row, 2.4rem); }
24
+ .ts-name { font-family: var(--font-code, ui-monospace, "SF Mono", Menlo, Consolas, monospace); font-size: var(--text-sm, 1.15rem); line-height: var(--text-sm-line, 1.6rem); color: var(--text-muted, oklch(0.72 0 0)); }
25
+ .ts-value { font-family: var(--font-code, ui-monospace, "SF Mono", Menlo, Consolas, monospace); font-size: var(--text-sm, 1.15rem); line-height: var(--text-sm-line, 1.6rem); overflow-wrap: anywhere; }
26
+ .ts-sample { display: flex; align-items: center; min-width: 0; }
27
+ .ts-probe { position: absolute; left: 0; top: 0; height: 0; visibility: hidden; }
28
+ `;
29
+
30
+ const tokenNamesOf = (css: string, only: (name: string) => boolean) => [
31
+ ...new Set([...css.matchAll(/^\s*--([\w-]+):/gmu)].map(([, name]) => name ?? '').filter(only)),
32
+ ];
33
+
34
+ const fillTokenValues = (root: HTMLElement, pixels: boolean) => {
35
+ const sheet = root.querySelector<HTMLElement>('.ts-root') ?? root;
36
+ const style = getComputedStyle(sheet);
37
+ const probe = sheet.querySelector<HTMLElement>('.ts-probe');
38
+
39
+ for (const cell of sheet.querySelectorAll<HTMLElement>('[data-token]')) {
40
+ const value = style.getPropertyValue(`--${cell.dataset['token']}`).trim();
41
+ let suffix = '';
42
+ if (pixels && probe && /^[\d.]+rem$/u.test(value)) {
43
+ probe.style.width = value;
44
+ suffix = ` · ${Math.round(probe.getBoundingClientRect().width * 10) / 10}px`;
45
+ }
46
+ cell.textContent = value + suffix;
47
+ }
48
+ };
49
+
50
+ /**
51
+ * Draws a foundation sheet: a sample panel, then one row per custom property declared in `css` (narrowed by `only`)
52
+ * with its value as the drawing resolves it, in px too for rem values when `pixels` is set.
53
+ * `scope` adds classes to the sheet root, so tokens scoped to a theme class resolve; `note` is markup under the title.
54
+ */
55
+ export const tokenSheet = ({
56
+ title,
57
+ note,
58
+ scope,
59
+ css,
60
+ sample,
61
+ sampleOf,
62
+ sampleClass = '',
63
+ only = () => true,
64
+ pixels = false,
65
+ styles = '',
66
+ setup,
67
+ }: TokenSheetOptions): Drawing => ({
68
+ body: html`<div class="ts-root${scope ? ` ${scope}` : ''}">
69
+ <h2 class="ts-title">${title}</h2>
70
+ ${note && html`<p class="ts-note">${note}</p>`}
71
+ <section class="ts-panel ${sampleClass}">${sample}</section>
72
+ <ul class="ts-panel ts-list">
73
+ ${tokenNamesOf(css, only)
74
+ .map(
75
+ (name) =>
76
+ html`<li class="ts-row">
77
+ <span class="ts-name">--${name}</span><span class="ts-value" data-token="${name}"></span
78
+ ><span class="ts-sample">${sampleOf(name)}</span>
79
+ </li>`,
80
+ )
81
+ .join('')}
82
+ </ul>
83
+ <span class="ts-probe"></span>
84
+ </div>`,
85
+ styles: `${SHEET_STYLES}${styles}`,
86
+ setup: [(root: HTMLElement) => fillTokenValues(root, pixels), ...(setup ? [setup] : [])],
87
+ });
@@ -1,7 +1,7 @@
1
- import { CallVariant, Drawing, mountDrawing, placeOverlays } from '../define-call';
1
+ import { CallVariant, Drawing, fitOutOfFlow, mountDrawing, placeOverlays } from '../define-call';
2
2
  import { Part } from '../define-part';
3
- import { fitEmbed } from '../embed-page';
4
- import { runRules } from '../motion-runtime';
3
+ import { applyEmbed, fitEmbed } from '../embed-page';
4
+ import { runRules, stepMotion, SteppedMotion } from '../motion-runtime';
5
5
  import { calls } from 'virtual:design-explore';
6
6
  import { loadEnv, loadProductEnv } from 'virtual:design-explore/env';
7
7
  import { parts, sketchParts, sketchTokens, tokens } from 'virtual:design-explore/system';
@@ -17,6 +17,8 @@ const fit = product !== null && params.has('fit');
17
17
  const interaction = product ? params.get('interaction') : null;
18
18
  const propsSketch = product ? params.get('props') : null;
19
19
  const propsVariant = params.get('props-variant') ?? '';
20
+ const motionName = product ? params.get('motion') : null;
21
+ const flowTo = product ? params.get('flow-to') : null;
20
22
 
21
23
  // Listens before the drawing loads, so an error it throws while loading counts; `report` exists only once `reporting` is set.
22
24
  let errored = false;
@@ -79,6 +81,17 @@ const showSetupError = (message: string) => {
79
81
 
80
82
  const messageOf = (error: unknown) => (error instanceof Error ? error.message : String(error));
81
83
 
84
+ const RETRY_DELAYS = [300, 1500];
85
+
86
+ // A part file that changes while frames load can fail its import once; Chromium fetches a failed module again.
87
+ const retried = <T>(load: () => Promise<T>, attempt = 0): Promise<T> =>
88
+ load().catch((error: unknown) => {
89
+ const delay = RETRY_DELAYS[attempt];
90
+ if (delay === undefined) throw error;
91
+
92
+ return new Promise<void>((resolve) => setTimeout(resolve, delay)).then(() => retried(load, attempt + 1));
93
+ });
94
+
82
95
  let drawnPart: { part: Part<unknown>; states: Record<string, unknown> } | null = null;
83
96
 
84
97
  const drawCall = async (): Promise<Drawing | null> => {
@@ -110,7 +123,7 @@ const drawPart = async (name: string): Promise<Drawing | null> => {
110
123
  );
111
124
 
112
125
  try {
113
- const [{ default: part }, { default: holder }] = await Promise.all([load(), loadProps()]);
126
+ const [{ default: part }, { default: holder }] = await Promise.all([retried(load), retried(loadProps)]);
114
127
 
115
128
  if (!(key in holder.states)) return fail(`design-explore: part "${partKey}" has no state "${key}"`);
116
129
 
@@ -141,7 +154,7 @@ const force = (element: Element, name: string) => {
141
154
  };
142
155
 
143
156
  const forced = async (css: string) => {
144
- if (!/:(?:hover|focus-visible|active)/iu.test(css)) return css;
157
+ if (!/:(?:hover|focus-visible|active|focus-within)/iu.test(css)) return css;
145
158
 
146
159
  const response = await fetch('/__design/force', { method: 'POST', body: css }).catch(() => null);
147
160
 
@@ -168,27 +181,123 @@ const report = () => {
168
181
  );
169
182
  };
170
183
 
171
- const runPartRules = ({ part, states }: NonNullable<typeof drawnPart>, styles: string) => {
184
+ const stateMarkupOf = ({ part, states }: NonNullable<typeof drawnPart>, styles: string) => {
172
185
  const sheets = new Set([styles]);
173
186
 
187
+ return (state: string) =>
188
+ Promise.resolve(part.render(states[state])).then((drawn) =>
189
+ sheets.has(drawn.styles)
190
+ ? drawn.body
191
+ : forced(drawn.styles).then((css) => {
192
+ const style = document.createElement('style');
193
+
194
+ sheets.add(drawn.styles);
195
+ style.textContent = css;
196
+ document.head.append(style);
197
+
198
+ return drawn.body;
199
+ }),
200
+ );
201
+ };
202
+
203
+ const exposeStepped = (options: { start: () => Promise<SteppedMotion>; interrupt: number | null }) => {
204
+ let stepped: SteppedMotion | null = null;
205
+ const steppedOf = () => {
206
+ if (!stepped) throw new Error('design-explore: start the motion first');
207
+
208
+ return stepped;
209
+ };
210
+
211
+ Object.assign(window, {
212
+ etMotion: {
213
+ start: () =>
214
+ options.start().then((started) => {
215
+ stepped = started;
216
+
217
+ return true;
218
+ }),
219
+ seek: (ms: number) => steppedOf().seek(ms),
220
+ bounds: () => steppedOf().bounds(),
221
+ timeline: () => ({ end: steppedOf().end(), tracks: steppedOf().tracks(), interrupt: options.interrupt }),
222
+ },
223
+ });
224
+ };
225
+
226
+ const exposeMotion = (options: { drawn: NonNullable<typeof drawnPart>; name: string; styles: string }) => {
227
+ const { drawn, name, styles } = options;
228
+ const motion = drawn.part.motions?.[name];
229
+
230
+ if (!motion) {
231
+ const known = Object.keys(drawn.part.motions ?? {});
232
+
233
+ showSetupError(`design-explore: part "${partKey}" has no motion "${name}" - it has: ${known.join(', ') || 'none'}`);
234
+
235
+ return;
236
+ }
237
+
238
+ const markupOf = stateMarkupOf(drawn, styles);
239
+
240
+ exposeStepped({
241
+ 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
+ ),
261
+ });
262
+ };
263
+
264
+ const exposeFlowLine = (options: { product: string; to: string; styles: string }) => {
265
+ const [toPart = '', toState = ''] = options.to.split('#');
266
+ const load = parts[options.product]?.[toPart];
267
+
268
+ if (!load) {
269
+ showSetupError(`design-explore: product "${options.product}" has no part "${toPart}"`);
270
+
271
+ return;
272
+ }
273
+
274
+ exposeStepped({
275
+ interrupt: null,
276
+ start: () =>
277
+ retried(load).then(({ default: part }) => {
278
+ const states = (part as Part<unknown>).states;
279
+
280
+ if (!(toState in states)) throw new Error(`design-explore: part "${toPart}" has no state "${toState}"`);
281
+
282
+ // The styles of the to page ride in the markup: in <head> they would restyle the from page before the View Transition captures it.
283
+
284
+ return Promise.resolve((part as Part<unknown>).render(states[toState]))
285
+ .then((drawn) =>
286
+ drawn.styles === options.styles
287
+ ? drawn.body
288
+ : forced(drawn.styles).then((css) => `<style>${css}</style>${drawn.body}`),
289
+ )
290
+ .then((markup) => stepMotion({ root, markup, motion: { from: key, to: toState, viewTransition: true } }));
291
+ }),
292
+ });
293
+ };
294
+
295
+ const runPartRules = (drawn: NonNullable<typeof drawnPart>, styles: string) => {
174
296
  runRules(root, {
175
297
  state: key,
176
- rules: part.rules ?? [],
177
- motions: part.motions,
178
- render: (state) =>
179
- Promise.resolve(part.render(states[state])).then((drawn) =>
180
- sheets.has(drawn.styles)
181
- ? drawn.body
182
- : forced(drawn.styles).then((css) => {
183
- const style = document.createElement('style');
184
-
185
- sheets.add(drawn.styles);
186
- style.textContent = css;
187
- document.head.append(style);
188
-
189
- return drawn.body;
190
- }),
191
- ),
298
+ rules: drawn.part.rules ?? [],
299
+ motions: drawn.part.motions,
300
+ render: stateMarkupOf(drawn, styles),
192
301
  switched: () => {
193
302
  const partRoot = partRootOf(root);
194
303
 
@@ -225,10 +334,16 @@ const drawFrame = async () => {
225
334
  if (error)
226
335
  showSetupError(`design-explore: setup of ${product ? 'state' : 'variant'} "${key}" failed\n${messageOf(error)}`);
227
336
 
228
- if (drawnPart?.part.rules?.length) runPartRules(drawnPart, loaded.styles ?? '');
337
+ if (flowTo && product) exposeFlowLine({ product, to: flowTo, styles: loaded.styles ?? '' });
338
+ else if (motionName && drawnPart) exposeMotion({ drawn: drawnPart, name: motionName, styles: loaded.styles ?? '' });
339
+ else if (drawnPart?.part.rules?.length) runPartRules(drawnPart, loaded.styles ?? '');
229
340
  }
230
341
 
231
- const place = () => placeOverlays(root);
342
+ const place = () => {
343
+ applyEmbed(frameElement, root);
344
+ placeOverlays(root);
345
+ fitOutOfFlow(root, { sideways: fit });
346
+ };
232
347
 
233
348
  new MutationObserver(place).observe(root, { childList: true, subtree: true });
234
349
  new ResizeObserver(place).observe(root);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ethlete/cli",
3
- "version": "3.0.0-next.24",
3
+ "version": "3.0.0-next.25",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/ethlete-io/ethdk",
@@ -24,12 +24,11 @@ exports.DRAWING_COMPILER_OPTIONS = {
24
24
  * tsconfig an editor finds for a call file.
25
25
  */
26
26
  const writeCallsTsconfig = (target) => {
27
- var _a, _b, _c;
28
27
  const work = (0, paths_1.workRootOf)(target);
29
28
  const path = (0, path_1.resolve)(work, 'tsconfig.json');
30
29
  const defineCall = (0, path_1.resolve)((0, paths_1.installedAssetRoot)(), 'define-call.ts');
31
30
  const inlineCss = (0, path_1.resolve)((0, paths_1.installedAssetRoot)(), 'css-inline.d.ts');
32
- const typePaths = (0, render_packages_1.typePathsOf)({ packages: (_c = (_b = (_a = (0, paths_1.readConfig)(target)) === null || _a === void 0 ? void 0 : _a.render) === null || _b === void 0 ? void 0 : _b.packages) !== null && _c !== void 0 ? _c : {}, target });
31
+ const typePaths = (0, render_packages_1.typePathsOf)({ packages: (0, render_packages_1.renderPackagesOf)({ target, config: (0, paths_1.readConfig)(target) }), target });
33
32
  const config = {
34
33
  compilerOptions: Object.assign(Object.assign({}, exports.DRAWING_COMPILER_OPTIONS), { paths: Object.assign(Object.assign({}, typePaths), { '@design-explore': [defineCall] }) }),
35
34
  include: [`${(0, paths_1.callsRootOf)(target)}/**/*.ts`, defineCall, inlineCss],
@@ -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,uDAAgD;AAEnC,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,MAAA,MAAA,MAAA,IAAA,kBAAU,EAAC,MAAM,CAAC,0CAAE,MAAM,0CAAE,QAAQ,mCAAI,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;IAEhG,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,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"}