@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.
- package/CHANGELOG.md +37 -0
- package/design/define-call.ts +43 -0
- package/design/define-part.ts +28 -8
- package/design/embed-page.ts +37 -4
- package/design/icon-sheet.ts +151 -0
- package/design/mark-part.ts +79 -2
- package/design/motion-runtime.ts +212 -5
- package/design/spring.ts +53 -0
- package/design/token-sheet.ts +87 -0
- package/design/web/frame.ts +139 -24
- package/package.json +1 -1
- package/src/lib/design/calls-tsconfig.js +1 -2
- package/src/lib/design/calls-tsconfig.js.map +1 -1
- package/src/lib/design/design-command.js +34 -4
- package/src/lib/design/design-command.js.map +1 -1
- package/src/lib/design/force-interactions.d.ts +2 -1
- package/src/lib/design/force-interactions.js +7 -4
- package/src/lib/design/force-interactions.js.map +1 -1
- package/src/lib/design/mcp-command.d.ts +60 -0
- package/src/lib/design/mcp-command.js +327 -0
- package/src/lib/design/mcp-command.js.map +1 -0
- package/src/lib/design/motion-command.d.ts +71 -0
- package/src/lib/design/motion-command.js +314 -0
- package/src/lib/design/motion-command.js.map +1 -0
- package/src/lib/design/motion-endpoint.d.ts +61 -0
- package/src/lib/design/motion-endpoint.js +133 -0
- package/src/lib/design/motion-endpoint.js.map +1 -0
- package/src/lib/design/motion-flow.d.ts +5 -0
- package/src/lib/design/motion-flow.js +29 -0
- package/src/lib/design/motion-flow.js.map +1 -0
- package/src/lib/design/motion-shot.d.ts +43 -0
- package/src/lib/design/motion-shot.js +97 -0
- package/src/lib/design/motion-shot.js.map +1 -0
- package/src/lib/design/ready-shots.d.ts +7 -0
- package/src/lib/design/ready-shots.js +22 -1
- package/src/lib/design/ready-shots.js.map +1 -1
- package/src/lib/design/render-packages.d.ts +9 -0
- package/src/lib/design/render-packages.js +13 -3
- package/src/lib/design/render-packages.js.map +1 -1
- package/src/lib/design/render.js +3 -3
- package/src/lib/design/render.js.map +1 -1
- package/src/lib/design/serve.js +34 -3
- package/src/lib/design/serve.js.map +1 -1
- package/src/lib/design/shot-command.d.ts +8 -2
- package/src/lib/design/shot-command.js +12 -8
- package/src/lib/design/shot-command.js.map +1 -1
- package/src/lib/design/shot.d.ts +16 -0
- package/src/lib/design/shot.js +68 -4
- package/src/lib/design/shot.js.map +1 -1
- package/src/lib/design/sketch-command.js +78 -16
- package/src/lib/design/sketch-command.js.map +1 -1
- package/src/lib/design/studio-api.d.ts +19 -0
- package/src/lib/design/studio-api.js +52 -1
- package/src/lib/design/studio-api.js.map +1 -1
- package/src/lib/design/system/complete-command.d.ts +1 -1
- package/src/lib/design/system/complete-command.js +76 -6
- package/src/lib/design/system/complete-command.js.map +1 -1
- package/src/lib/design/system/complete.d.ts +107 -9
- package/src/lib/design/system/complete.js +592 -141
- package/src/lib/design/system/complete.js.map +1 -1
- package/src/lib/design/system/exceptions.d.ts +59 -0
- package/src/lib/design/system/exceptions.js +132 -0
- package/src/lib/design/system/exceptions.js.map +1 -0
- package/src/lib/design/system/flows/flow.d.ts +3 -0
- package/src/lib/design/system/flows/flow.js +5 -3
- package/src/lib/design/system/flows/flow.js.map +1 -1
- package/src/lib/design/system/handoff.d.ts +11 -0
- package/src/lib/design/system/handoff.js +30 -0
- package/src/lib/design/system/handoff.js.map +1 -0
- package/src/lib/design/system/inventory-approval.d.ts +41 -0
- package/src/lib/design/system/inventory-approval.js +71 -0
- package/src/lib/design/system/inventory-approval.js.map +1 -0
- package/src/lib/design/system/inventory.d.ts +65 -0
- package/src/lib/design/system/inventory.js +168 -0
- package/src/lib/design/system/inventory.js.map +1 -0
- package/src/lib/design/system/list-system.js +9 -2
- package/src/lib/design/system/list-system.js.map +1 -1
- package/src/lib/design/system/render-check.js +19 -1
- package/src/lib/design/system/render-check.js.map +1 -1
- package/src/lib/design/system/resolve-product.d.ts +2 -3
- package/src/lib/design/system/resolve-product.js +4 -6
- package/src/lib/design/system/resolve-product.js.map +1 -1
- package/src/lib/design/system/rules/stale-replace.js +41 -2
- package/src/lib/design/system/rules/stale-replace.js.map +1 -1
- package/src/lib/design/system/rules/token-selectors.js +1 -1
- package/src/lib/design/system/rules/token-selectors.js.map +1 -1
- package/src/lib/design/system/sketch/host-edit.d.ts +4 -0
- package/src/lib/design/system/sketch/host-edit.js +68 -0
- package/src/lib/design/system/sketch/host-edit.js.map +1 -0
- package/src/lib/design/system/sketch/merge.d.ts +3 -2
- package/src/lib/design/system/sketch/merge.js +47 -12
- package/src/lib/design/system/sketch/merge.js.map +1 -1
- package/src/lib/design/system/sketch/merged-parts.d.ts +10 -0
- package/src/lib/design/system/sketch/merged-parts.js +24 -3
- package/src/lib/design/system/sketch/merged-parts.js.map +1 -1
- package/src/lib/design/system/sketch/no-visible-change.d.ts +6 -0
- package/src/lib/design/system/sketch/no-visible-change.js +25 -1
- package/src/lib/design/system/sketch/no-visible-change.js.map +1 -1
- package/src/lib/design/system/sketch/overlay.js +11 -3
- package/src/lib/design/system/sketch/overlay.js.map +1 -1
- package/src/lib/design/system/sketch/sketch.d.ts +32 -0
- package/src/lib/design/system/sketch/sketch.js +73 -18
- package/src/lib/design/system/sketch/sketch.js.map +1 -1
- package/src/lib/design/system/sketch/unused-closure.d.ts +12 -0
- package/src/lib/design/system/sketch/unused-closure.js +185 -0
- package/src/lib/design/system/sketch/unused-closure.js.map +1 -0
- package/src/lib/design/system/system-modules.js +6 -9
- package/src/lib/design/system/system-modules.js.map +1 -1
- package/src/lib/design/system/system-tsconfig.d.ts +2 -1
- package/src/lib/design/system/system-tsconfig.js +9 -5
- package/src/lib/design/system/system-tsconfig.js.map +1 -1
package/design/motion-runtime.ts
CHANGED
|
@@ -105,14 +105,23 @@ const measure = (root: Element) => {
|
|
|
105
105
|
};
|
|
106
106
|
};
|
|
107
107
|
|
|
108
|
-
/**
|
|
109
|
-
|
|
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
|
-
|
|
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.
|
package/design/spring.ts
ADDED
|
@@ -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
|
+
});
|
package/design/web/frame.ts
CHANGED
|
@@ -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
|
|
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
|
|
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: (
|
|
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 (
|
|
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 = () =>
|
|
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
|
@@ -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: (
|
|
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,
|
|
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"}
|