@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,94 @@
1
1
  # @ethlete/cli
2
2
 
3
+ ## 3.0.0-next.26
4
+
5
+ ### Minor Changes
6
+
7
+ - `et design check` measures text contrast in each colour scheme a product renders (light and dark when its stylesheets use `prefers-color-scheme` or `light-dark()`) and names the scheme in a CONTRAST finding.
8
+ - `et design` takes a product `base` from `node_modules`: `render.packages` defaults to `node_modules` without `dist/libs`, and an installed base gets no generated `tsconfig.json`.
9
+ - The `et design mcp` `check` tool checks a call and its variant, and `et design check --receipt <path>` writes the result to a JSON file.
10
+ - The new `call` tool of `et design mcp` reads a call: its `call.ts`, its `fixture.ts`, the variant file and the frame URL.
11
+ - A design part can name its component with `implements`, and `et design check` prints the open points of each part.
12
+ - A design product can declare the colour schemes it draws with `schemes` in `.ethlete/design/config.json`; both when left out.
13
+ - The design server answers `GET /__design/queue` with its queued and drawing shots, the age of the oldest, and the last shots that failed.
14
+ - A design product can list `generatedFiles` in its config. The part hash skips them, so regenerating a file no longer makes `design check` warn that the part changed outside a sketch.
15
+ - Add `iconSheet` to `@design-system`: a shared icon foundation sheet with a `catalogue` and a `construction` view, drawing each icon on its own grid (from its viewBox or `grid`) with centre lines and the padding box.
16
+ - `design complete` counts an option or a `row` control with `data-active-source="keyboard"` as its focus-visible look, the same as an option named by `aria-activedescendant`.
17
+ - `@design-system` exports `logoSheet({ marks })`, which draws a `foundations/logo` part: each mark at its sizes, with an optional light bar.
18
+ - `et design shot` and `et design motion` take `--scheme <light|dark>` (MCP: `scheme`) to draw a part in that colour scheme.
19
+ - Add `et design sketch note <layer>/<name> <variant> <text> --by <model id>`: it sets the note of one variant in sketch.json and refuses an unknown sketch or variant.
20
+ - `et design check`, `sketch check`, `sketch ready` and `design complete` fail interaction states on a part that declares no control (`CONTROL-STATE`). `design complete` hints a state that changes under 10 % of its frame.
21
+ - `et design check` lists missing motions as open points: every ordered state pair, except focus, and a looping motion in an ongoing-work state. `definePart({ unreachable })` exempts a pair with a reason, checked by the `MOTION-UNREACHABLE-*` rules.
22
+ - The design render check measures non-text contrast: the border of each control (and the frame around an input) and each svg icon must reach 3:1 against what it sits on. Decorative borders give no finding.
23
+ - `et design check` lists two more open points: a component part with no controls declared, and the contrasts the render check found. With `--complete`, it also lists each control that lacks a drawn state or surface.
24
+ - The design server answers `GET /__design/open-points?product=&part=`, the open-point list of one part or of every part; `draw=true` adds the contrasts and missing control states of one part.
25
+ - Design checks report missing mock option values as hints for merged parts and block newly introduced gaps in sketches; use `omits` to record intentional omissions.
26
+ - `et design ready <part>` and `POST /__design/part-ready` mark a part ready in `ready.json`, refused while the part has open points; the mark drops when the part changes.
27
+ - `et design` shot requests take `view` (a number raised per view change) and `rank=active|visible`: the active frame of the shown view draws first, then its visible cells, then the rest, then older views. A hung-up request's queued shot is dropped.
28
+ - `et design sketch ready` refuses while a target in a live variant has open points the sketch can close: no controls declared or a missing motion.
29
+ - The Studio motion timeline makes the motion's start frame live and drives it: the slider pauses its animations at that time, play and speed run them, closing resumes them at 1x.
30
+ - Motion timeline tracks name the foundations duration and easing tokens they use.
31
+ - `et design sketch ready` now runs the render check on the sketch's variants and refuses on any render issue, as `sketch check` reports it. Skipped elements are honoured; without a browser the check is skipped.
32
+
33
+ ### Patch Changes
34
+
35
+ - `et design check` now lists the findings of a base product's parts per rule and says they do not block the product, instead of pointing to `--product <base>`.
36
+ - A design product without its own `tokens` takes the token parts of its base, so frames of a product that extends a base draw under the base's tokens.
37
+ - `et design complete` no longer crashes on an option that is also a `data-et-control` in an open surface.
38
+ - The design contrast check measures text against a `z-index: -1` fill behind it (a positioned sibling or a `::before`/`::after`) instead of the panel under that fill.
39
+ - `et design complete` no longer fails a page on the gaps of a control part it uses from its base product; those gaps stay open points of the base part.
40
+ - `et design check` no longer fails a product on the findings of a part it takes from its base product. It counts them in one line that points to the base product's own check.
41
+ - `et design check` prints one `RENDER CHECK NOT RUN` line when failing checks stop the render, naming the rules and how many problems are in the base.
42
+ - `et design` commands that load design modules (`system --json`, `sketch ready`) no longer share the dependency cache of the running design server, so they stop deleting its optimized deps and blanking every live frame.
43
+ - `et design complete` reports `CONTROL-UNDECLARED` as a hint; the sketch check fails on a control a sketch adds. For a native control the hint says to mark it with `control(markup, name)` and declare the name in `controls`.
44
+ - `et design` listens on `DE_PORT` when it is set, also when Studio did not start it. Before, it took a free port.
45
+ - `design complete` no longer reports the focus container of a composite widget (a `menu`, `listbox`, `tree`, `grid`, `tablist` or similar with `tabindex="-1"` or `aria-activedescendant`) as an undeclared control; its items are still checked.
46
+ - A package rebuild that writes a file back unchanged no longer redraws the design shots that use it.
47
+ - `et design complete` requires hover, focus and active of a menu item but no selected look; a select still requires the selected look.
48
+ - The design server keeps its part shots on disk, so a restart serves them without redrawing until a file they draw from changes.
49
+ - `et design complete` names a `hover:`, `active:` or `focus:` class no stylesheet defines instead of reporting an unchanged box, and the Studio Tailwind theme gains the `surface-pressed` colour.
50
+ - A design frame no longer grows below a scroll container for an absolute element that the container clips, such as the scroll sentinels of an overlay body.
51
+ - A design frame no longer grows to the viewport for a fixed element on all four edges, such as an overlay backdrop.
52
+ - A part frame now grows to cover absolutely or fixed positioned content, so an open overlay panel no longer gets cut off at the trigger's height.
53
+ - Design tool: a forced `focus-visible` now also matches `:focus-within` on the element and its ancestors, so floating labels and focus rings draw as they do in a real focus.
54
+ - A shot of a missing part, state or sketch variant now always answers 404, a queued shot of a removed sketch is dropped, and the render queue no longer lists failures of removed sketches.
55
+ - A part shot is cut from the page with a clip of the root's box instead of an element screenshot, and the thumb queue draws eight shots at once instead of four, so a grid of forty states draws faster.
56
+ - `et design shot --crop changed` also crops the states the merged part does not draw, comparing them with its first state, so a new hover state is no longer cut off.
57
+ - `et design sketch check` and `sketch ready` print one count line per variant for the problems that were there before, instead of repeating each of them per variant.
58
+ - The design server lists sketches about three times faster: each part source is parsed once, and the product's rule problems are found once per list. A run of merges no longer stalls frames and Studio requests for seconds.
59
+ - The sketch check warns of an export in a sketch's `shared/` folder only when no variant imports it. An export that only some variants use no longer warns.
60
+ - The `STALE-REPLACE` design check no longer warns on a `.replace` on a function parameter that a call fills with markup from a package.
61
+ - `et design check` no longer reports an `rgb()` or `rgba()` as a hard-coded colour when every channel comes from `var()`, for example `rgb(var(--shadow-rgb) / 0.25)`. A literal channel is still reported.
62
+ - Draw part and call shots on a transparent page in both colour schemes, as a live frame does.
63
+ - `et design check` reports `CONTROL-PART-INPUT` again: an import cycle left the plain-control selector undefined, so the rule matched no element.
64
+ - The CONTROL-PART-INPUT hint now covers the same plain controls as `design complete`: `summary`, focusable `[tabindex]` and every interactive role.
65
+ - CONTROL-STATE counts an interaction word only as the last word of a state name, drops modifier words from the element name, and asks a component without controls to declare them (Q164).
66
+ - Design commands that run at the same time no longer share one Vite cache folder: the first keeps the shared cache, every other one draws from a folder of its own that it removes when it closes.
67
+ - The design contrast check no longer reports decorative separators, toolbar outlines or hidden svg lines, and measures text on a frame's page fill.
68
+ - The contrast check measures the border of a segmented group: a bordered wrapper counts as a control frame when all its controls together fill at least half of it. Toolbars stay excluded.
69
+ - Design checks report a drawn button, link, input or control role without `data-et-control` as `CONTROL-PART-INPUT`: a hint on merged parts, an error when a sketch adds one.
70
+ - Design checks now list existing interaction states on non-control parts as open points while blocking new or changed states in sketches.
71
+ - Design sketch checks accept unchanged interaction states when unrelated styles change.
72
+ - The design server no longer crashes when a sketch folder disappears while sketches are listed.
73
+ - The design server's parts list names each part's module file, relative to the workspace.
74
+ - The design server no longer pre-bundles its own authoring modules (`@design-system`, `@design-system/kit`, `@design-explore`), so a cold start no longer reloads every frame when Vite finds one of them late.
75
+ - The design server draws a rebuilt `render.packages` folder (such as `dist/libs/components`) without a restart, instead of serving the modules it loaded before the rebuild.
76
+ - Serve a shot from the disk cache after the design server restarts, instead of drawing it again
77
+ - Every design shot response carries an `x-design-shot` header (`disk`, `memory` or `drawn`) that says where the picture came from.
78
+ - `sketch check` no longer prints Angular's development mode line per render, and prints a finding that repeats across states, schemes and variants once, with a count.
79
+ - `et design sketch merge` formats the files it writes with the Prettier of the target repo. A format failure prints a warning and keeps the merge.
80
+ - `et design sketch ready` blocks only on open points a variant adds to the merged part; inherited points are listed as a note. A new part still blocks on all of its points.
81
+ - `et design sketch variant --from` keeps relative symlinks in the copied variant, so its `shared/` links point at the same files as the source variant.
82
+ - `et design` no longer rewrites an unchanged tsconfig, so a `design sketch check` stops redrawing every frame in Studio.
83
+ - A rule in a sketch's `shared/` stylesheet counts as used when any variant of the sketch draws it, so `sketch ready` no longer fails a variant on `UNUSED` for a rule only another variant uses.
84
+ - A design frame asked for a `fill` token its product lacks keeps the product's page colour, so an sdk grid cell in the light column draws on the light surface instead of on Studio's dark canvas.
85
+ - `et design shot <part> --variants` finds a sketch by an extra target, `sketch list` shows extra targets, and new `sketch target add <sketch> <part>` adds a target to an open sketch.
86
+ - `et design sketch report` on a ready sketch moves it back to in progress. It still refuses a decided sketch.
87
+ - STALE-REPLACE no longer flags a replace whose regex is anchored with `^` or `$`; the rule searches the source, not the markup string.
88
+ - A static shot draws in the colour scheme Studio prefers, like the live frames next to it, so a dark Studio no longer mixes light shots into a grid.
89
+ - Studio draws every part grid twice, under a Light and a Dark head, and `frame.html?scheme=` pins a live frame to one scheme.
90
+ - Swap a canvas frame between its shot and its live page without a blank frame: the frame page reports when it painted, and Studio keeps the old view until the new one is ready.
91
+
3
92
  ## 3.0.0-next.25
4
93
 
5
94
  ### Minor Changes
@@ -248,7 +248,8 @@ export const placeOverlays = (root: HTMLElement) => {
248
248
  /**
249
249
  * Grows `root` to cover every absolutely or fixed positioned element inside it, such as an open
250
250
  * overlay pane, so a frame measured or shot from `root` shows them. Skips a fixed element on all four
251
- * edges of the viewport, such as a backdrop. Grows sideways too with `sideways`. Safe to run again.
251
+ * edges of the viewport, such as a backdrop, and counts an absolute element only as far as the
252
+ * overflow boxes that clip it. Grows sideways too with `sideways`. Safe to run again.
252
253
  */
253
254
  export const fitOutOfFlow = (root: HTMLElement, options: { sideways: boolean }) => {
254
255
  const view = root.ownerDocument.defaultView;
@@ -260,11 +261,36 @@ export const fitOutOfFlow = (root: HTMLElement, options: { sideways: boolean })
260
261
  const fillsViewport = (style: CSSStyleDeclaration) =>
261
262
  style.position === 'fixed' &&
262
263
  [style.top, style.right, style.bottom, style.left].every((edge) => parseFloat(edge) === 0);
264
+ const clips = (style: CSSStyleDeclaration) =>
265
+ [style.overflow, style.overflowX, style.overflowY].some((overflow) => overflow && overflow !== 'visible');
266
+ const holdsAbsolute = (style: CSSStyleDeclaration) =>
267
+ !['static', ''].includes(style.position) || !['none', ''].includes(style.transform);
268
+ const clipped = (element: Element) => {
269
+ let { top, right, bottom, left } = element.getBoundingClientRect();
270
+ let escaping = true;
271
+
272
+ for (let ancestor = element.parentElement; ancestor && ancestor !== root; ancestor = ancestor.parentElement) {
273
+ const style = view.getComputedStyle(ancestor);
274
+
275
+ if (escaping && holdsAbsolute(style)) escaping = false;
276
+ if (!escaping && clips(style)) {
277
+ const box = ancestor.getBoundingClientRect();
278
+
279
+ top = Math.max(top, box.top);
280
+ right = Math.min(right, box.right);
281
+ bottom = Math.min(bottom, box.bottom);
282
+ left = Math.max(left, box.left);
283
+ }
284
+ if (style.position === 'fixed') break;
285
+ if (style.position === 'absolute') escaping = true;
286
+ }
287
+
288
+ return { top, right, bottom, left, width: right - left, height: bottom - top };
289
+ };
263
290
  const outOfFlow = [...root.querySelectorAll('*')]
264
291
  .map((element) => ({ element, style: view.getComputedStyle(element) }))
265
292
  .filter(({ style }) => ['absolute', 'fixed'].includes(style.position) && !fillsViewport(style))
266
- .map(({ element }) => element)
267
- .map((element) => element.getBoundingClientRect())
293
+ .map(({ element, style }) => (style.position === 'fixed' ? element.getBoundingClientRect() : clipped(element)))
268
294
  .filter((rect) => rect.width > 0 && rect.height > 0);
269
295
  const extentOf = ({ side, start, paddings }: { side: 'bottom' | 'right'; start: number; paddings: string[] }) => {
270
296
  const extent = Math.max(0, ...outOfFlow.map((rect) => Math.ceil(rect[side] - start)));
@@ -10,6 +10,8 @@ export { iconSheet } from './icon-sheet';
10
10
  export type { IconSheetIcon, IconSheetOptions } from './icon-sheet';
11
11
  export { interactionRows } from './interaction-rows';
12
12
  export type { InteractionRowsOptions } from './interaction-rows';
13
+ export { logoSheet } from './logo-sheet';
14
+ export type { LogoSheetMark, LogoSheetOptions } from './logo-sheet';
13
15
  export { spring } from './spring';
14
16
  export type { Spring } from './spring';
15
17
  export { svgAsset } from './svg-asset';
@@ -33,10 +35,13 @@ export type Interaction = 'hover' | 'focus-visible' | 'active' | 'disabled';
33
35
  * `controls` names each control the part draws, keyed by the id its markup carries in `data-et-control`; `design
34
36
  * complete` requires the states and surfaces its `kind` and `input` need, see `Control`.
35
37
  * `motions` names each motion between two of its states, see `Motion`; a rule in `rules` can name one.
38
+ * `unreachable` lists the state pairs no user can move between, each with its `reason`; they need no motion.
39
+ * `omits` records mock option values deliberately left off the grid, each with a reason.
36
40
  * `checks.skip` names the render checks of `design check` the part breaks on purpose, such as
37
41
  * `CLIPPED` for a part that cuts its content. To skip a check for one element only, put
38
42
  * `data-et-skip="CLIPPED"` on it; the check then ignores that element and everything inside it.
39
43
  * `contrast` feeds the contrast check of `design check`, see `PartContrast`.
44
+ * `implements` links the part to the code that builds it, see `PartLink`.
40
45
  */
41
46
  export type Part<TProps, TState extends string = string> = {
42
47
  note: string;
@@ -50,10 +55,19 @@ export type Part<TProps, TState extends string = string> = {
50
55
  checks?: { skip?: RenderCheck[] };
51
56
  contrast?: PartContrast;
52
57
  motions?: Record<string, Motion<TState>>;
58
+ unreachable?: { from: TState; to: TState; reason: string }[];
59
+ omits?: { option: keyof TProps & string; value: string; reason: string }[];
53
60
  rules?: PartRule<TState>[];
61
+ implements?: PartLink;
54
62
  render: (props: TProps, options?: RenderOptions) => DrawnPart | Promise<DrawnPart>;
55
63
  };
56
64
 
65
+ /**
66
+ * The code a part is built in: a component or element names the repo-relative path of its `.component.ts`, a page
67
+ * names its route. `design check` fails on a path that does not exist, and lists a part without a link as open.
68
+ */
69
+ export type PartLink = `${string}.component.ts` | { route: `/${string}` };
70
+
57
71
  /** What `play` of a motion may use, so Studio controls time: `at` schedules `fn` at `ms` into the motion on the tool's clock, `random` is seeded, `rect` measures a selector in the `from` state. */
58
72
  export type MotionContext = {
59
73
  at: (ms: number, fn: () => void) => void;
@@ -62,7 +76,7 @@ export type MotionContext = {
62
76
  };
63
77
 
64
78
  /**
65
- * A motion from the state `from` to the state `to`. `at` adds shot times in ms, `interrupt` starts the reverse motion
79
+ * A motion from the state `from` to the state `to`; with `from` equal to `to` it is the looping motion inside that state. `at` adds shot times in ms, `interrupt` starts the reverse motion
66
80
  * at that time, `viewTransition` uses a View Transition, and `play` runs after the morph for what CSS cannot express.
67
81
  */
68
82
  export type Motion<TState extends string = string> = {
@@ -0,0 +1,52 @@
1
+ import { Drawing, html } from './define-call';
2
+ import { svgAsset } from './svg-asset';
3
+
4
+ export type LogoSheetMark = {
5
+ title: string;
6
+ svg: string;
7
+ sizes: number[];
8
+ onLight?: number[];
9
+ };
10
+
11
+ export type LogoSheetOptions = {
12
+ marks: LogoSheetMark[];
13
+ };
14
+
15
+ const SHEET_STYLES = `
16
+ .lgs-root { display: flex; flex-direction: column; gap: var(--space-4, 1.6rem); padding: var(--space-6, 2.4rem); 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); }
17
+ .lgs-root svg { display: block; }
18
+ .lgs-title { margin: 0; font-size: var(--text-xl, 1.7rem); line-height: var(--text-xl-line, 2.2rem); font-weight: var(--weight-strong, 600); }
19
+ .lgs-row { display: flex; align-items: flex-end; gap: var(--space-6, 2.4rem); }
20
+ .lgs-item { display: flex; flex-direction: column; align-items: center; gap: var(--space-2, 0.8rem); }
21
+ .lgs-label { font-family: var(--font-code, ui-monospace, "SF Mono", Menlo, Consolas, monospace); font-size: var(--text-xs, 1rem); line-height: var(--text-xs-line, 1.4rem); color: var(--text-muted, oklch(0.72 0 0)); }
22
+ .lgs-plate { display: flex; align-items: center; gap: var(--space-3, 1.2rem); padding: var(--space-2, 0.8rem) var(--space-3, 1.2rem); border-radius: var(--radius-control-md, 0.6rem); background: var(--text, oklch(0.94 0 0)); }
23
+ `;
24
+
25
+ const item = (content: string, label: string) =>
26
+ html`<div class="lgs-item">${content}<span class="lgs-label">${label}</span></div>`;
27
+
28
+ /**
29
+ * Draws a `foundations/logo` part: one row per mark with its `title`, the mark at each of its `sizes` in px, and, when
30
+ * `onLight` lists sizes, the mark at those sizes on a light bar.
31
+ */
32
+ export const logoSheet = ({ marks }: LogoSheetOptions): Drawing => ({
33
+ body: html`<div class="lgs-root">
34
+ ${marks
35
+ .map(
36
+ ({ title, svg, sizes, onLight }) =>
37
+ html`<h2 class="lgs-title">${title}</h2>
38
+ <div class="lgs-row">
39
+ ${sizes.map((size) => item(svgAsset(svg, { size }), `${size} px`)).join('')}
40
+ ${
41
+ !!onLight?.length &&
42
+ item(
43
+ html`<div class="lgs-plate">${onLight.map((size) => svgAsset(svg, { size })).join('')}</div>`,
44
+ 'on a light bar',
45
+ )
46
+ }
47
+ </div>`,
48
+ )
49
+ .join('')}
50
+ </div>`,
51
+ styles: SHEET_STYLES,
52
+ });
@@ -0,0 +1,215 @@
1
+ import { SteppedMotion } from './motion-runtime';
2
+
3
+ /** What the Studio timeline posts into a live frame to drive a motion of its part. */
4
+ export type MotionDriveMessage =
5
+ | { type: 'design-explore:motion-start'; motion: string; reduced: boolean }
6
+ | { type: 'design-explore:motion-seek'; ms: number; rate: number }
7
+ | { type: 'design-explore:motion-play'; ms: number; rate: number }
8
+ | { type: 'design-explore:motion-stop' };
9
+
10
+ /** What a live frame posts back once a motion started, or failed to. */
11
+ export type MotionDriveReply =
12
+ | { type: 'design-explore:motion-ready'; motion: string }
13
+ | { type: 'design-explore:motion-failed'; motion: string; error: string };
14
+
15
+ const TAIL_MS = 30000;
16
+
17
+ const isTime = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value) && value >= 0;
18
+
19
+ const isRate = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value) && value > 0;
20
+
21
+ /** The drive message `data` holds, or `null` for any other message. */
22
+ export const motionDriveMessageOf = (data: unknown): MotionDriveMessage | null => {
23
+ if (typeof data !== 'object' || data === null) return null;
24
+
25
+ const message = data as Record<string, unknown>;
26
+
27
+ switch (message['type']) {
28
+ case 'design-explore:motion-start':
29
+ return typeof message['motion'] === 'string'
30
+ ? { type: message['type'], motion: message['motion'], reduced: message['reduced'] === true }
31
+ : null;
32
+ case 'design-explore:motion-seek':
33
+ case 'design-explore:motion-play':
34
+ return isTime(message['ms']) && isRate(message['rate'])
35
+ ? { type: message['type'], ms: message['ms'], rate: message['rate'] }
36
+ : null;
37
+ case 'design-explore:motion-stop':
38
+ return { type: message['type'] };
39
+ default:
40
+ return null;
41
+ }
42
+ };
43
+
44
+ const REDUCED_FEATURE = /\(\s*prefers-reduced-motion\s*(?::\s*(reduce|no-preference)\s*)?\)/g;
45
+ const ALWAYS = '(min-width: 0px)';
46
+ const NEVER = '(max-width: -1px)';
47
+
48
+ type MediaHolder = CSSRule & { media: MediaList };
49
+
50
+ const mediaRulesOf = (rules: CSSRuleList): MediaHolder[] =>
51
+ [...rules].flatMap((rule) =>
52
+ 'cssRules' in rule
53
+ ? [...('media' in rule ? [rule as MediaHolder] : []), ...mediaRulesOf((rule as CSSGroupingRule).cssRules)]
54
+ : [],
55
+ );
56
+
57
+ const sheetRulesOf = (sheet: CSSStyleSheet) => {
58
+ try {
59
+ return mediaRulesOf(sheet.cssRules);
60
+ } catch {
61
+ return [];
62
+ }
63
+ };
64
+
65
+ /**
66
+ * Makes every `prefers-reduced-motion` media rule of `document` match as if the user asked for `reduced` motion, and
67
+ * returns a function that puts the rules back. Scripts that call `matchMedia` still see the real preference.
68
+ */
69
+ export const emulateReducedMotion = (document: Document, reduced: boolean) => {
70
+ const changed = [...document.styleSheets].flatMap(sheetRulesOf).flatMap((rule) => {
71
+ const original = rule.media.mediaText;
72
+ const emulated = original.replace(REDUCED_FEATURE, (_, value?: string) =>
73
+ (value === 'no-preference' ? !reduced : reduced) ? ALWAYS : NEVER,
74
+ );
75
+
76
+ if (emulated === original) return [];
77
+
78
+ rule.media.mediaText = emulated;
79
+
80
+ return [{ rule, original }];
81
+ });
82
+
83
+ return () => {
84
+ for (const { rule, original } of changed) rule.media.mediaText = original;
85
+ };
86
+ };
87
+
88
+ /**
89
+ * Drives a motion in a live frame from drive messages: `start` (re)starts the motion from its `from` state, a seek
90
+ * pauses every animation of the document at that time, play runs the motion's clock and animations at `rate`, and
91
+ * stop resumes every animation at 1x. `released` hears each stop.
92
+ */
93
+ export const createMotionDriver = (options: {
94
+ document: Document;
95
+ start: (motion: string, reduced: boolean) => Promise<SteppedMotion>;
96
+ reply: (reply: MotionDriveReply) => void;
97
+ released?: () => void;
98
+ frame?: (step: (time: number) => void) => unknown;
99
+ }) => {
100
+ const { document, reply } = options;
101
+ const frame = options.frame ?? ((step: (time: number) => void) => requestAnimationFrame(step));
102
+ let started: { motion: string; reduced: boolean } | null = null;
103
+ let stepped: SteppedMotion | null = null;
104
+ let loop = 0;
105
+ let queue: Promise<void> = Promise.resolve();
106
+
107
+ const holdAll = (rate: number) => {
108
+ for (const animation of document.getAnimations()) {
109
+ animation.pause();
110
+ animation.playbackRate = rate;
111
+ }
112
+ };
113
+
114
+ const resumeAll = () => {
115
+ for (const animation of document.getAnimations()) {
116
+ const end = animation.effect?.getComputedTiming().endTime;
117
+
118
+ animation.playbackRate = 1;
119
+
120
+ if (typeof end === 'number' && Number.isFinite(end) && Number(animation.currentTime ?? 0) >= end)
121
+ animation.finish();
122
+ else animation.play();
123
+ }
124
+ };
125
+
126
+ const run = (play: { motion: SteppedMotion; from: number; rate: number; step: () => void }) => {
127
+ const { motion, rate, step } = play;
128
+ const id = ++loop;
129
+ let time = play.from;
130
+ let last: number | null = null;
131
+
132
+ const tick = (now: number) => {
133
+ if (id !== loop) return;
134
+
135
+ time += (now - (last ?? now)) * rate;
136
+ last = now;
137
+ motion.seek(Math.min(time, TAIL_MS));
138
+ step();
139
+
140
+ if (time < TAIL_MS) frame(tick);
141
+ };
142
+
143
+ frame(tick);
144
+ };
145
+
146
+ const moveTo = (ms: number): Promise<SteppedMotion | null> => {
147
+ if (!stepped || !started) return Promise.resolve(null);
148
+ if (ms >= stepped.now()) {
149
+ stepped.seek(ms);
150
+
151
+ return Promise.resolve(stepped);
152
+ }
153
+
154
+ return options.start(started.motion, started.reduced).then((restarted) => {
155
+ stepped = restarted;
156
+ restarted.seek(ms);
157
+
158
+ return restarted;
159
+ });
160
+ };
161
+
162
+ const handled = (message: MotionDriveMessage): Promise<void> => {
163
+ loop++;
164
+
165
+ switch (message.type) {
166
+ case 'design-explore:motion-start':
167
+ started = { motion: message.motion, reduced: message.reduced };
168
+ stepped = null;
169
+
170
+ return options.start(message.motion, message.reduced).then(
171
+ (motion) => {
172
+ stepped = motion;
173
+ reply({ type: 'design-explore:motion-ready', motion: message.motion });
174
+ },
175
+ (error: unknown) =>
176
+ reply({
177
+ type: 'design-explore:motion-failed',
178
+ motion: message.motion,
179
+ error: error instanceof Error ? error.message : String(error),
180
+ }),
181
+ );
182
+ case 'design-explore:motion-seek':
183
+ return moveTo(message.ms).then((motion) => {
184
+ if (motion) holdAll(message.rate);
185
+ });
186
+ case 'design-explore:motion-play':
187
+ return moveTo(message.ms).then((motion) => {
188
+ if (!motion) return;
189
+
190
+ holdAll(message.rate);
191
+ run({ motion, from: message.ms, rate: message.rate, step: () => holdAll(message.rate) });
192
+ });
193
+ case 'design-explore:motion-stop': {
194
+ const motion = stepped;
195
+
196
+ if (!motion) return Promise.resolve();
197
+
198
+ motion.release();
199
+ resumeAll();
200
+ options.released?.();
201
+ run({ motion, from: motion.now(), rate: 1, step: () => undefined });
202
+
203
+ return Promise.resolve();
204
+ }
205
+ }
206
+ };
207
+
208
+ return {
209
+ handle: (message: MotionDriveMessage) => {
210
+ queue = queue.then(() => handled(message)).catch(() => undefined);
211
+
212
+ return queue;
213
+ },
214
+ };
215
+ };
@@ -162,9 +162,14 @@ export type MotionTrack = {
162
162
  end: number | null;
163
163
  };
164
164
 
165
- /** A motion paused at its start: `seek` moves it forward to a time in ms, `tracks` lists what ran so far. */
165
+ /**
166
+ * A motion paused at its start: `seek` moves it forward to a time in ms, `tracks` lists what ran so far. `release`
167
+ * stops pausing and posing its animations, so a later `seek` only runs its `ctx.at` steps.
168
+ */
166
169
  export type SteppedMotion = {
167
170
  seek: (ms: number) => void;
171
+ now: () => number;
172
+ release: () => void;
168
173
  tracks: () => MotionTrack[];
169
174
  end: () => number;
170
175
  bounds: () => MotionBox;
@@ -238,6 +243,7 @@ export const stepMotion = (options: {
238
243
  const cut = new Set<Animation>();
239
244
  const interrupt = motion.interrupt;
240
245
  let interrupted = false;
246
+ let released = false;
241
247
  let lastTask = 0;
242
248
  let ready: Promise<unknown> = Promise.resolve();
243
249
 
@@ -257,12 +263,14 @@ export const stepMotion = (options: {
257
263
  for (const animation of document.getAnimations()) {
258
264
  if (started.has(animation)) continue;
259
265
 
260
- animation.pause();
266
+ if (!released) animation.pause();
261
267
  started.set(animation, trackOf(animation, start));
262
268
  }
263
269
  };
264
270
 
265
271
  const pose = (ms: number) => {
272
+ if (released) return;
273
+
266
274
  for (const [animation, track] of started)
267
275
  if (!cut.has(animation)) animation.currentTime = Math.max(0, ms - track.start);
268
276
  };
@@ -318,6 +326,10 @@ export const stepMotion = (options: {
318
326
 
319
327
  pose(ms);
320
328
  },
329
+ now: () => clock.now(),
330
+ release: () => {
331
+ released = true;
332
+ },
321
333
  tracks: () => [...started.values()],
322
334
  end: () => Math.max(lastTask, ...[...started.values()].map((track) => track.end ?? 0)),
323
335
  bounds: () => {