@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.
- package/CHANGELOG.md +89 -0
- package/design/define-call.ts +29 -3
- package/design/define-part.ts +15 -1
- package/design/logo-sheet.ts +52 -0
- package/design/motion-drive.ts +215 -0
- package/design/motion-runtime.ts +14 -2
- package/design/motion-tokens.ts +177 -0
- package/design/web/frame-fill.ts +4 -0
- package/design/web/frame-scheme.ts +44 -0
- package/design/web/frame.ts +122 -29
- package/package.json +1 -1
- package/src/lib/design/calls-tsconfig.d.ts +2 -0
- package/src/lib/design/calls-tsconfig.js +10 -3
- package/src/lib/design/calls-tsconfig.js.map +1 -1
- package/src/lib/design/check.d.ts +1 -0
- package/src/lib/design/check.js +62 -6
- package/src/lib/design/check.js.map +1 -1
- package/src/lib/design/contact-sheet.d.ts +9 -0
- package/src/lib/design/contact-sheet.js +38 -4
- package/src/lib/design/contact-sheet.js.map +1 -1
- package/src/lib/design/design-command.js +27 -8
- package/src/lib/design/design-command.js.map +1 -1
- package/src/lib/design/draw-ledger.d.ts +36 -0
- package/src/lib/design/draw-ledger.js +52 -0
- package/src/lib/design/draw-ledger.js.map +1 -0
- package/src/lib/design/mcp-command.d.ts +23 -2
- package/src/lib/design/mcp-command.js +105 -10
- package/src/lib/design/mcp-command.js.map +1 -1
- package/src/lib/design/motion-command.js +9 -15
- package/src/lib/design/motion-command.js.map +1 -1
- package/src/lib/design/paths.d.ts +14 -0
- package/src/lib/design/paths.js +50 -1
- package/src/lib/design/paths.js.map +1 -1
- package/src/lib/design/render-packages.d.ts +5 -0
- package/src/lib/design/render-packages.js +6 -1
- package/src/lib/design/render-packages.js.map +1 -1
- package/src/lib/design/render-worker.js +1 -0
- package/src/lib/design/render-worker.js.map +1 -1
- package/src/lib/design/serve.d.ts +2 -2
- package/src/lib/design/serve.js +158 -19
- package/src/lib/design/serve.js.map +1 -1
- package/src/lib/design/shot-command.d.ts +5 -2
- package/src/lib/design/shot-command.js +18 -16
- package/src/lib/design/shot-command.js.map +1 -1
- package/src/lib/design/shot-store.d.ts +24 -0
- package/src/lib/design/shot-store.js +120 -0
- package/src/lib/design/shot-store.js.map +1 -0
- package/src/lib/design/shot.d.ts +21 -1
- package/src/lib/design/shot.js +118 -31
- package/src/lib/design/shot.js.map +1 -1
- package/src/lib/design/sketch-command.js +121 -37
- package/src/lib/design/sketch-command.js.map +1 -1
- package/src/lib/design/ssr-shims.d.ts +2 -0
- package/src/lib/design/ssr-shims.js +12 -1
- package/src/lib/design/ssr-shims.js.map +1 -1
- package/src/lib/design/studio-api.d.ts +12 -0
- package/src/lib/design/studio-api.js +68 -2
- package/src/lib/design/studio-api.js.map +1 -1
- package/src/lib/design/system/check-system.d.ts +6 -2
- package/src/lib/design/system/check-system.js +102 -20
- package/src/lib/design/system/check-system.js.map +1 -1
- package/src/lib/design/system/colour.d.ts +4 -1
- package/src/lib/design/system/colour.js +54 -2
- package/src/lib/design/system/colour.js.map +1 -1
- package/src/lib/design/system/complete-command.js +6 -2
- package/src/lib/design/system/complete-command.js.map +1 -1
- package/src/lib/design/system/complete.d.ts +67 -2
- package/src/lib/design/system/complete.js +227 -53
- package/src/lib/design/system/complete.js.map +1 -1
- package/src/lib/design/system/contrast-check.d.ts +6 -1
- package/src/lib/design/system/contrast-check.js +167 -12
- package/src/lib/design/system/contrast-check.js.map +1 -1
- package/src/lib/design/system/drawn-open-points.d.ts +16 -0
- package/src/lib/design/system/drawn-open-points.js +43 -0
- package/src/lib/design/system/drawn-open-points.js.map +1 -0
- package/src/lib/design/system/list-system.d.ts +4 -1
- package/src/lib/design/system/list-system.js +2 -1
- package/src/lib/design/system/list-system.js.map +1 -1
- package/src/lib/design/system/open-points.d.ts +101 -0
- package/src/lib/design/system/open-points.js +156 -0
- package/src/lib/design/system/open-points.js.map +1 -0
- package/src/lib/design/system/part-ready.d.ts +52 -0
- package/src/lib/design/system/part-ready.js +101 -0
- package/src/lib/design/system/part-ready.js.map +1 -0
- package/src/lib/design/system/read-open-points.d.ts +27 -0
- package/src/lib/design/system/read-open-points.js +81 -0
- package/src/lib/design/system/read-open-points.js.map +1 -0
- package/src/lib/design/system/ready-command.d.ts +7 -0
- package/src/lib/design/system/ready-command.js +65 -0
- package/src/lib/design/system/ready-command.js.map +1 -0
- package/src/lib/design/system/render-check.d.ts +2 -0
- package/src/lib/design/system/render-check.js +197 -31
- package/src/lib/design/system/render-check.js.map +1 -1
- package/src/lib/design/system/resolve-product.d.ts +2 -1
- package/src/lib/design/system/resolve-product.js +20 -3
- package/src/lib/design/system/resolve-product.js.map +1 -1
- package/src/lib/design/system/rules/implements.d.ts +7 -0
- package/src/lib/design/system/rules/implements.js +43 -0
- package/src/lib/design/system/rules/implements.js.map +1 -0
- package/src/lib/design/system/rules/motion.d.ts +2 -0
- package/src/lib/design/system/rules/motion.js +26 -1
- package/src/lib/design/system/rules/motion.js.map +1 -1
- package/src/lib/design/system/rules/option-values.d.ts +15 -0
- package/src/lib/design/system/rules/option-values.js +145 -0
- package/src/lib/design/system/rules/option-values.js.map +1 -0
- package/src/lib/design/system/rules/stale-replace.js +41 -3
- package/src/lib/design/system/rules/stale-replace.js.map +1 -1
- package/src/lib/design/system/sketch/baseline.d.ts +11 -2
- package/src/lib/design/system/sketch/baseline.js +4 -2
- package/src/lib/design/system/sketch/baseline.js.map +1 -1
- package/src/lib/design/system/sketch/check-sketches.d.ts +1 -0
- package/src/lib/design/system/sketch/check-sketches.js +4 -2
- package/src/lib/design/system/sketch/check-sketches.js.map +1 -1
- package/src/lib/design/system/sketch/merge.js +32 -0
- package/src/lib/design/system/sketch/merge.js.map +1 -1
- package/src/lib/design/system/sketch/merged-parts.d.ts +1 -1
- package/src/lib/design/system/sketch/merged-parts.js +1 -1
- package/src/lib/design/system/sketch/merged-parts.js.map +1 -1
- package/src/lib/design/system/sketch/part-definition.d.ts +36 -0
- package/src/lib/design/system/sketch/part-definition.js +56 -2
- package/src/lib/design/system/sketch/part-definition.js.map +1 -1
- package/src/lib/design/system/sketch/sketch.d.ts +15 -0
- package/src/lib/design/system/sketch/sketch.js +65 -27
- package/src/lib/design/system/sketch/sketch.js.map +1 -1
- package/src/lib/design/system/sketch/unchanged-states.d.ts +4 -1
- package/src/lib/design/system/sketch/unchanged-states.js +9 -5
- package/src/lib/design/system/sketch/unchanged-states.js.map +1 -1
- package/src/lib/design/system/sketch/unused-closure.js +12 -2
- package/src/lib/design/system/sketch/unused-closure.js.map +1 -1
- package/src/lib/design/system/sketch/unused-files.d.ts +1 -1
- package/src/lib/design/system/sketch/unused-files.js +6 -3
- package/src/lib/design/system/sketch/unused-files.js.map +1 -1
- package/src/lib/design/system/system-tsconfig.js +2 -2
- package/src/lib/design/system/system-tsconfig.js.map +1 -1
- package/src/lib/design/system/ts-source.d.ts +3 -0
- package/src/lib/design/system/ts-source.js +21 -4
- package/src/lib/design/system/ts-source.js.map +1 -1
- package/src/lib/design/thumb.d.ts +31 -4
- package/src/lib/design/thumb.js +105 -40
- 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
|
package/design/define-call.ts
CHANGED
|
@@ -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
|
|
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)));
|
package/design/define-part.ts
CHANGED
|
@@ -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
|
|
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
|
+
};
|
package/design/motion-runtime.ts
CHANGED
|
@@ -162,9 +162,14 @@ export type MotionTrack = {
|
|
|
162
162
|
end: number | null;
|
|
163
163
|
};
|
|
164
164
|
|
|
165
|
-
/**
|
|
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: () => {
|