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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/design/contrast.ts +49 -0
  3. package/design/define-call.ts +48 -8
  4. package/design/define-part.ts +83 -1
  5. package/design/embed-page.ts +37 -4
  6. package/design/icon-sheet.ts +151 -0
  7. package/design/mark-part.ts +79 -2
  8. package/design/morph.ts +67 -0
  9. package/design/motion-runtime.ts +407 -0
  10. package/design/spring.ts +53 -0
  11. package/design/token-sheet.ts +87 -0
  12. package/design/web/env.d.ts +6 -2
  13. package/design/web/frame.ts +159 -6
  14. package/package.json +2 -1
  15. package/src/lib/design/calls-tsconfig.js +1 -2
  16. package/src/lib/design/calls-tsconfig.js.map +1 -1
  17. package/src/lib/design/changed-parts.d.ts +4 -4
  18. package/src/lib/design/changed-parts.js +10 -6
  19. package/src/lib/design/changed-parts.js.map +1 -1
  20. package/src/lib/design/contact-sheet.d.ts +26 -13
  21. package/src/lib/design/contact-sheet.js +122 -40
  22. package/src/lib/design/contact-sheet.js.map +1 -1
  23. package/src/lib/design/design-command.js +50 -2
  24. package/src/lib/design/design-command.js.map +1 -1
  25. package/src/lib/design/force-interactions.d.ts +2 -1
  26. package/src/lib/design/force-interactions.js +7 -4
  27. package/src/lib/design/force-interactions.js.map +1 -1
  28. package/src/lib/design/generated-stylesheets.d.ts +15 -0
  29. package/src/lib/design/generated-stylesheets.js +52 -0
  30. package/src/lib/design/generated-stylesheets.js.map +1 -0
  31. package/src/lib/design/inbox-command.d.ts +1 -0
  32. package/src/lib/design/inbox-command.js +15 -0
  33. package/src/lib/design/inbox-command.js.map +1 -1
  34. package/src/lib/design/mcp-command.d.ts +60 -0
  35. package/src/lib/design/mcp-command.js +327 -0
  36. package/src/lib/design/mcp-command.js.map +1 -0
  37. package/src/lib/design/merge-compare.d.ts +10 -0
  38. package/src/lib/design/merge-compare.js +17 -4
  39. package/src/lib/design/merge-compare.js.map +1 -1
  40. package/src/lib/design/motion-command.d.ts +71 -0
  41. package/src/lib/design/motion-command.js +314 -0
  42. package/src/lib/design/motion-command.js.map +1 -0
  43. package/src/lib/design/motion-endpoint.d.ts +61 -0
  44. package/src/lib/design/motion-endpoint.js +133 -0
  45. package/src/lib/design/motion-endpoint.js.map +1 -0
  46. package/src/lib/design/motion-flow.d.ts +5 -0
  47. package/src/lib/design/motion-flow.js +29 -0
  48. package/src/lib/design/motion-flow.js.map +1 -0
  49. package/src/lib/design/motion-shot.d.ts +43 -0
  50. package/src/lib/design/motion-shot.js +97 -0
  51. package/src/lib/design/motion-shot.js.map +1 -0
  52. package/src/lib/design/paths.d.ts +11 -0
  53. package/src/lib/design/paths.js.map +1 -1
  54. package/src/lib/design/ready-shots.d.ts +7 -0
  55. package/src/lib/design/ready-shots.js +22 -1
  56. package/src/lib/design/ready-shots.js.map +1 -1
  57. package/src/lib/design/render-packages.d.ts +9 -0
  58. package/src/lib/design/render-packages.js +13 -3
  59. package/src/lib/design/render-packages.js.map +1 -1
  60. package/src/lib/design/render.js +3 -3
  61. package/src/lib/design/render.js.map +1 -1
  62. package/src/lib/design/serve.js +108 -21
  63. package/src/lib/design/serve.js.map +1 -1
  64. package/src/lib/design/shot-command.d.ts +12 -6
  65. package/src/lib/design/shot-command.js +60 -48
  66. package/src/lib/design/shot-command.js.map +1 -1
  67. package/src/lib/design/shot.d.ts +30 -4
  68. package/src/lib/design/shot.js +138 -53
  69. package/src/lib/design/shot.js.map +1 -1
  70. package/src/lib/design/sketch-command.js +132 -39
  71. package/src/lib/design/sketch-command.js.map +1 -1
  72. package/src/lib/design/sketch-stylesheets.d.ts +7 -0
  73. package/src/lib/design/sketch-stylesheets.js +63 -0
  74. package/src/lib/design/sketch-stylesheets.js.map +1 -0
  75. package/src/lib/design/studio-api.d.ts +28 -6
  76. package/src/lib/design/studio-api.js +102 -17
  77. package/src/lib/design/studio-api.js.map +1 -1
  78. package/src/lib/design/system/check-system.js +8 -0
  79. package/src/lib/design/system/check-system.js.map +1 -1
  80. package/src/lib/design/system/comments/comment.d.ts +17 -0
  81. package/src/lib/design/system/comments/comment.js +35 -1
  82. package/src/lib/design/system/comments/comment.js.map +1 -1
  83. package/src/lib/design/system/comments/complete.js +4 -2
  84. package/src/lib/design/system/comments/complete.js.map +1 -1
  85. package/src/lib/design/system/complete-command.d.ts +9 -0
  86. package/src/lib/design/system/complete-command.js +134 -0
  87. package/src/lib/design/system/complete-command.js.map +1 -0
  88. package/src/lib/design/system/complete.d.ts +171 -0
  89. package/src/lib/design/system/complete.js +1106 -0
  90. package/src/lib/design/system/complete.js.map +1 -0
  91. package/src/lib/design/system/contrast-check.d.ts +26 -0
  92. package/src/lib/design/system/contrast-check.js +153 -0
  93. package/src/lib/design/system/contrast-check.js.map +1 -0
  94. package/src/lib/design/system/exceptions.d.ts +59 -0
  95. package/src/lib/design/system/exceptions.js +132 -0
  96. package/src/lib/design/system/exceptions.js.map +1 -0
  97. package/src/lib/design/system/flows/flow.d.ts +3 -0
  98. package/src/lib/design/system/flows/flow.js +5 -3
  99. package/src/lib/design/system/flows/flow.js.map +1 -1
  100. package/src/lib/design/system/handoff.d.ts +11 -0
  101. package/src/lib/design/system/handoff.js +30 -0
  102. package/src/lib/design/system/handoff.js.map +1 -0
  103. package/src/lib/design/system/imports.d.ts +28 -3
  104. package/src/lib/design/system/imports.js +84 -7
  105. package/src/lib/design/system/imports.js.map +1 -1
  106. package/src/lib/design/system/inventory-approval.d.ts +41 -0
  107. package/src/lib/design/system/inventory-approval.js +71 -0
  108. package/src/lib/design/system/inventory-approval.js.map +1 -0
  109. package/src/lib/design/system/inventory.d.ts +65 -0
  110. package/src/lib/design/system/inventory.js +168 -0
  111. package/src/lib/design/system/inventory.js.map +1 -0
  112. package/src/lib/design/system/list-system.d.ts +11 -0
  113. package/src/lib/design/system/list-system.js +26 -3
  114. package/src/lib/design/system/list-system.js.map +1 -1
  115. package/src/lib/design/system/product-stylesheets.d.ts +3 -0
  116. package/src/lib/design/system/product-stylesheets.js +40 -0
  117. package/src/lib/design/system/product-stylesheets.js.map +1 -0
  118. package/src/lib/design/system/render-check.js +30 -4
  119. package/src/lib/design/system/render-check.js.map +1 -1
  120. package/src/lib/design/system/resolve-product.d.ts +2 -3
  121. package/src/lib/design/system/resolve-product.js +4 -6
  122. package/src/lib/design/system/resolve-product.js.map +1 -1
  123. package/src/lib/design/system/rules/canonical-class.d.ts +9 -0
  124. package/src/lib/design/system/rules/canonical-class.js +132 -0
  125. package/src/lib/design/system/rules/canonical-class.js.map +1 -0
  126. package/src/lib/design/system/rules/inline-svg.d.ts +7 -0
  127. package/src/lib/design/system/rules/inline-svg.js +28 -0
  128. package/src/lib/design/system/rules/inline-svg.js.map +1 -0
  129. package/src/lib/design/system/rules/motion.d.ts +4 -0
  130. package/src/lib/design/system/rules/motion.js +46 -2
  131. package/src/lib/design/system/rules/motion.js.map +1 -1
  132. package/src/lib/design/system/rules/stale-replace.js +41 -2
  133. package/src/lib/design/system/rules/stale-replace.js.map +1 -1
  134. package/src/lib/design/system/rules/token-selectors.js +1 -1
  135. package/src/lib/design/system/rules/token-selectors.js.map +1 -1
  136. package/src/lib/design/system/rules/utility-css.d.ts +11 -0
  137. package/src/lib/design/system/rules/utility-css.js +290 -0
  138. package/src/lib/design/system/rules/utility-css.js.map +1 -0
  139. package/src/lib/design/system/sketch/baseline.d.ts +3 -2
  140. package/src/lib/design/system/sketch/baseline.js +10 -4
  141. package/src/lib/design/system/sketch/baseline.js.map +1 -1
  142. package/src/lib/design/system/sketch/host-edit.d.ts +4 -0
  143. package/src/lib/design/system/sketch/host-edit.js +68 -0
  144. package/src/lib/design/system/sketch/host-edit.js.map +1 -0
  145. package/src/lib/design/system/sketch/merge-preview.d.ts +4 -2
  146. package/src/lib/design/system/sketch/merge-preview.js +20 -3
  147. package/src/lib/design/system/sketch/merge-preview.js.map +1 -1
  148. package/src/lib/design/system/sketch/merge-shared.d.ts +42 -6
  149. package/src/lib/design/system/sketch/merge-shared.js +122 -24
  150. package/src/lib/design/system/sketch/merge-shared.js.map +1 -1
  151. package/src/lib/design/system/sketch/merge.d.ts +6 -5
  152. package/src/lib/design/system/sketch/merge.js +95 -35
  153. package/src/lib/design/system/sketch/merge.js.map +1 -1
  154. package/src/lib/design/system/sketch/merged-parts.d.ts +17 -0
  155. package/src/lib/design/system/sketch/merged-parts.js +35 -2
  156. package/src/lib/design/system/sketch/merged-parts.js.map +1 -1
  157. package/src/lib/design/system/sketch/no-visible-change.d.ts +10 -3
  158. package/src/lib/design/system/sketch/no-visible-change.js +75 -20
  159. package/src/lib/design/system/sketch/no-visible-change.js.map +1 -1
  160. package/src/lib/design/system/sketch/overlay.js +16 -3
  161. package/src/lib/design/system/sketch/overlay.js.map +1 -1
  162. package/src/lib/design/system/sketch/part-definition.d.ts +12 -0
  163. package/src/lib/design/system/sketch/part-definition.js +40 -1
  164. package/src/lib/design/system/sketch/part-definition.js.map +1 -1
  165. package/src/lib/design/system/sketch/sketch.d.ts +40 -3
  166. package/src/lib/design/system/sketch/sketch.js +147 -50
  167. package/src/lib/design/system/sketch/sketch.js.map +1 -1
  168. package/src/lib/design/system/sketch/unchanged-states.d.ts +10 -4
  169. package/src/lib/design/system/sketch/unchanged-states.js +37 -23
  170. package/src/lib/design/system/sketch/unchanged-states.js.map +1 -1
  171. package/src/lib/design/system/sketch/unused-closure.d.ts +12 -0
  172. package/src/lib/design/system/sketch/unused-closure.js +185 -0
  173. package/src/lib/design/system/sketch/unused-closure.js.map +1 -0
  174. package/src/lib/design/system/system-modules.d.ts +5 -1
  175. package/src/lib/design/system/system-modules.js +32 -17
  176. package/src/lib/design/system/system-modules.js.map +1 -1
  177. package/src/lib/design/system/system-tsconfig.d.ts +2 -1
  178. package/src/lib/design/system/system-tsconfig.js +11 -6
  179. package/src/lib/design/system/system-tsconfig.js.map +1 -1
  180. package/src/lib/design/thumb.d.ts +6 -2
  181. package/src/lib/design/thumb.js +36 -15
  182. package/src/lib/design/thumb.js.map +1 -1
  183. package/src/lib/design/variant-diff.d.ts +12 -10
  184. package/src/lib/design/variant-diff.js +14 -13
  185. package/src/lib/design/variant-diff.js.map +1 -1
  186. package/src/lib/design/warm-pages.d.ts +20 -0
  187. package/src/lib/design/warm-pages.js +78 -0
  188. package/src/lib/design/warm-pages.js.map +1 -0
  189. package/src/lib/update/commits.d.ts +9 -0
  190. package/src/lib/update/commits.js +15 -1
  191. package/src/lib/update/commits.js.map +1 -1
  192. package/src/lib/update/migration-manifest.d.ts +6 -0
  193. package/src/lib/update/migration-manifest.js +7 -0
  194. package/src/lib/update/migration-manifest.js.map +1 -1
  195. package/src/lib/update/migrations-command.js +1 -0
  196. package/src/lib/update/migrations-command.js.map +1 -1
  197. package/src/lib/update/run-migrations.d.ts +22 -0
  198. package/src/lib/update/run-migrations.js +60 -1
  199. package/src/lib/update/run-migrations.js.map +1 -1
  200. package/src/lib/update/update-command.d.ts +10 -0
  201. package/src/lib/update/update-command.js +28 -2
  202. package/src/lib/update/update-command.js.map +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,90 @@
1
1
  # @ethlete/cli
2
2
 
3
+ ## 3.0.0-next.25
4
+
5
+ ### Minor Changes
6
+
7
+ - `et design mcp` serves the design tools to an MCP client over stdio: parts, check, shot, motion_shots, the sketch commands and the inbox. `shot` and `motion_shots` return the sheet image; `motion_shots` also returns its JSON.
8
+ - The design server answers `GET /__design/motion` with the keyframes and tracks of one motion mode, and each keyframe as a PNG with `frame=<ms>`. The draw is kept until the part changes.
9
+ - `embedPage` takes `mark` and `css`, both applied inside the embedded page: every element matching `mark` gets an accent outline, and `css` is added as a stylesheet there.
10
+ - `et design check` warns with `EMBED-SCALE` when an element around an `embedPage` frame with a `scale` scales it again, so the page shrinks twice.
11
+ - `et design complete` accepts a `field` control kind for text fields: it needs a drawn hover and focus-visible, but no active.
12
+ - The shot sheet draws an empty "not drawn" cell for each state combo a grid row leaves out, and `sketch check` lists those cells on the grid line.
13
+ - `et design motion flow <flow> <line>` draws a flow line with `"transition": "view"` as a stepped View Transition from the `from` page to the `to` page, shot in the preset viewport. Lines are named by `id` or `from→to`.
14
+ - `et design motion shots` clips each shot to the part and every animated element over all keyframes, so an overshoot is not cut off, and names a track's element by `data-et-key` where it has one.
15
+ - Add `et design motion shots <part> <motion>`: it draws a motion at its keyframes in full and reduced motion into one sheet and writes its tracks as JSON.
16
+ - `spring({ damping, frequency })` from `@design-system` returns a `linear()` easing and its duration for a motion token. A motion's `interrupt` now switches back to its `from` state at that time in `motion shots`.
17
+ - `design sketch` merges a sketch of a base part into the base. `sketch new --override` opens a product override; `--into-base` moves an existing override into the base. `sketch ready` refuses a variant that does not load.
18
+ - Add `design sketch hold <sketch> --until <text>` and `design sketch release <sketch>`: a held sketch goes back to in progress, leaves the merge queue, and `sketch ready` refuses with the `--until` text until it is released.
19
+ - A sketch may carry an edit to a host of its target in a higher layer when the edit only changes the host's imports, so a rename across layers merges in one sketch.
20
+ - `sketch ready` refuses a live variant whose files, or the `shared/` files they import, keep an unused import, parameter or CSS class.
21
+ - Add `tokenSheet` to `@design-system`: it draws a foundations part as a sheet of its CSS custom properties, labelled with the values the drawing resolves, so every product draws its foundations the same way.
22
+ - `design complete` checks the `button`, `select`, `menu`, `row` and `tooltip` control kinds and a declared `disabled` look, and reads a base part's review from its base's `merged.json`.
23
+ - `design complete` fails a page as `INVENTORY-UNAPPROVED` until its control inventory is approved; the design server answers `/__design/inventory` and `/__design/inventory-approve`, which writes `inventory.json` and refuses pages with undeclared or unclassified controls.
24
+ - `design complete` gives each control inside an embedded part's `data-et-part` root to that part, fails the host while that part is incomplete, and fails a host CSS rule restyling it as `CONTROL-OVERRIDDEN`.
25
+ - `et design mcp` gains the tools `sketch_variant`, `sketch_round`, `sketch_list`, `sketch_check`, `sketch_preview`, `sketch_rebase` and `comment_reply`.
26
+
27
+ ### Patch Changes
28
+
29
+ - `design complete` accepts an option named by `aria-activedescendant` as focus-visible evidence when it looks different from a plain option in its surface, and its gap says which evidence is missing.
30
+ - `design complete --request <part>.<control>.<requirement> --reason` writes a pending exception that still fails. Studio's approve endpoint records by, at and the part hash; then the gap passes until the part changes.
31
+ - `design complete <layer>/<name> --variant <key>` checks a variant of the part's open sketch, so a sketch proves it closes its gaps before it is merged.
32
+ - `design complete`: a `data-et-control-part` descendant of a `data-et-control` takes the forced interactions of its control, and its `disabled` or `aria-disabled="true"` counts as the control's disabled mark.
33
+ - `et design complete <part> --handoff` gives a design complete part the `handoff` status; `et design complete --handoff` re-checks every `handoff` part and exits 1 when one regressed, for CI.
34
+ - `design` renders no longer fail with "exports is not defined": the CLI build again ships the frame runtime as source only, with no CommonJS copies next to it.
35
+ - `STALE-REPLACE` no longer warns on a `.replace` of markup a package draws, such as `button(options)` from `@ethlete/components`, which the rule cannot read.
36
+ - `tokenSheet` styles now carry static fallbacks for every custom property, so design products without the Studio tokens draw a sane sheet.
37
+ - `tokenSheet` takes `scope`, classes on the sheet root where values are read, so theme-scoped tokens resolve, and `note`, markup drawn under the title.
38
+ - A design frame retries a failed part import twice after a short back-off, so a part file that changes while its grid loads no longer leaves "Failed to fetch dynamically imported module" in the frame.
39
+
40
+ ## 3.0.0-next.24
41
+
42
+ ### Minor Changes
43
+
44
+ - A sketch's changed area gives one box per cluster of changed pixels; `et design shot --crop changed` writes one sheet per cluster, and Studio shows one enlarged crop per cluster.
45
+ - `et design check` now fails with `CANONICAL-CLASS` when a Tailwind class in a design part has a shorter canonical form, for example `w-[420px]` that can be written as `w-2xl`.
46
+ - `et design sketch merge` takes `--variant <key> --allowed-by <plan ref>`: it merges a sketch with no pick when that variant is its only live one, and logs the plan in `accepted.json`.
47
+ - `et update` adds the packages a codemod requires, such as `ts-morph`, as dev dependencies before it runs the codemod, in a commit of its own. A migration declares them in its new `requires` field.
48
+ - A design product can list stylesheets a command generates from a part (`generated`). A frame of a sketch variant that changes that part runs the command against the variant's part and loads its output.
49
+ - `et design check` now fails a literal duration, delay or easing in a transition or animation outside `foundations/`, with `HARD-CODED-MOTION`.
50
+ - `et design check` now fails an inline `<svg>` outside `foundations/`, with `INLINE-SVG`. A computed drawing passes with `data-et-drawing`.
51
+ - `et design sketch merge` and `sketch discard` now resolve the addressed comments of the sketch, and a merge those of its parts without a sketch. New `et design comment resolve-ended` resolves the addressed comments of ended sketches.
52
+ - `et design complete <layer>/<name>` fails while a part's declared `controls` lack drawn, reviewed evidence; it checks the `disclosure` kind first.
53
+ - `et design complete` now discovers drawn controls: an unmarked native, ARIA or SDK control fails as `CONTROL-UNDECLARED`, a pointer or `:hover` element outside a control as `CONTROL-UNCLASSIFIED`, and `CONTROLS-MISSING` is gone.
54
+ - `et design complete` now fails a control whose part draws its interactions as flat states instead of an `interactionRows` grid.
55
+ - A page control in `definePart({ controls })` names the part that draws it in `part`; `et design complete` takes its interactions from that part and fails while that part is not design complete.
56
+ - `et design complete` now requires a state that forces `active` on a control with `pointer` input, next to the state that forces `hover`.
57
+ - `et design complete` now fails a control whose forced interaction changes a colour, background, border or shadow without a transition.
58
+ - `et design check` fails with `CONTRAST` when drawn text is below WCAG 2 AA against its composited background. Parts declare token `pairs` to compute and `exceptions` with a reason; `contrastRatioOf` draws a ratio table.
59
+ - A part's rules now run in its design frame. A switch morphs the existing DOM, so CSS transitions run, then plays the motion the rule names, with a controlled clock, seeded random and `from`-state measurements.
60
+ - A design part can declare `motions` between its states, and a rule can name one. `et design check` fails on a motion between unknown states, a rule naming an undeclared motion, and `setTimeout` in motion code.
61
+ - `et design check` now fails with `UTILITY-CSS` when a design part's CSS holds a declaration that a Tailwind utility of the product draws, for example `display: flex`.
62
+ - `et design`: a product's `shared/` folder holds helper modules that are no part; any part imports them as `@shared/<path>`, and editing one marks each importing part changed.
63
+ - The design server lists per sketch variant the states it adds and drops and its changed part note. Studio says on the canvas when a variant changes no pixel, and a click shows the old and new note.
64
+ - `et design sketch new` takes several parts: one sketch, one variant folder holding them all, one `ready`, one merge; `ready` and `preview` name every part a `shared/` edit reaches; Studio shows every target part.
65
+
66
+ ### Patch Changes
67
+
68
+ - `et update --continue` no longer commits what you changed after the stopped run, such as a package you installed by hand, into its own commits.
69
+ - `et design check` no longer reports contrast for the label of a disabled control: a `label[for]`, a wrapping label, or an `aria-labelledby` target.
70
+ - The design server's Studio event stream now also sends `calls` and `sketches` when a call or sketch file changes, and a `parts` event for a design system change that redraws no frame.
71
+ - The design server draws up to four pictures at once instead of one, so cold shots, sketch lists and merge views render about three times faster. High-priority drawings still start before waiting low ones.
72
+ - `et design` draws with partial raster off, so two draws of a page with embedded frames give the same picture under load, and `sketch ready` gives the same answer on a re-run.
73
+ - The design auto-merge check now skips rejected variants, so only live variants can block a merge that is safe.
74
+ - `et design` now fails with the frame URL when a frame or an embedded frame never draws, instead of saving a blank picture. `sketch ready` draws a changed state once more before it counts as a change.
75
+ - Drawn design frames no longer show a real hover on a control at the frame's top-left corner, so `design shot` and `design complete` see only the interactions a state forces.
76
+ - `et design` `CANONICAL-CLASS` and `UTILITY-CSS` now check the files of a `shared/` folder that a part imports, also in a sketch, so `sketch check` and `sketch ready` catch them before a merge.
77
+ - Design screenshots now show the end of every finite CSS animation and transition, so an element that fades in with `@starting-style` is drawn fully visible.
78
+ - The design server draws each shot on a warm browser page per viewport and scale instead of opening a new browser context, so a warm shot is about 12 % faster and a batch uses about 20 % less CPU.
79
+ - The `et design` server now keeps the pixel compare of each sketch's unchanged states until a product file changes, so the sketch list loads in a fraction of the time after the first call.
80
+ - `et design` redraws only the affected parts, not every part, when a file that a product stylesheet scans as a Tailwind `@source` changes.
81
+ - `et design inbox done` now writes the note into the sketch variant for a comment on a part that exists only in the sketch, instead of failing with "is no part of".
82
+ - A multi-target sketch merge gives each target only the `shared/` files it imports, directly or transitively; an unimported file returns to the target that held it. `sketch new` refuses targets whose `shared/` files clash.
83
+ - `et design sketch ready` now also draws the parts that show the sketched part before it merges on its own. When one of them changes, the sketch stays ready and the output names the part and state.
84
+ - A sketch with several targets shows the diff marks and the pages that use any target part. Studio names the parts a `shared/` edit reaches. The sketch-diff endpoint takes `part=`.
85
+ - `et design` draws a sketch variant with its own part stylesheet where the product stylesheet `@import`s the merged one, so `sketch ready` no longer auto-merges a token value change as "no visible change".
86
+ - A cold sketch shows its first frame sooner: Studio lists the sketches without the slow unchanged-state comparison first (`?unchanged=no`), asks for shots and diffs at once, then lists them in full.
87
+
3
88
  ## 3.0.0-next.23
4
89
 
5
90
  ### Minor Changes
@@ -0,0 +1,49 @@
1
+ type Rgba = [number, number, number, number];
2
+
3
+ const rgbaIn = (probe: HTMLElement, value: string): Rgba => {
4
+ probe.style.color = `color-mix(in srgb, ${value} 100%, transparent)`;
5
+
6
+ const computed = getComputedStyle(probe).color;
7
+ const numbers = [...computed.matchAll(/-?[\d.]+(?:e-?\d+)?/g)].map((match) => parseFloat(match[0]));
8
+ const scale = computed.startsWith('color(') ? 1 : 255;
9
+ const [r = 0, g = 0, b = 0] = numbers.map((channel) => Math.min(1, Math.max(0, channel / scale)));
10
+
11
+ return [r, g, b, numbers[3] ?? 1];
12
+ };
13
+
14
+ const over = (top: Rgba, under: Rgba): Rgba => [
15
+ top[0] * top[3] + under[0] * (1 - top[3]),
16
+ top[1] * top[3] + under[1] * (1 - top[3]),
17
+ top[2] * top[3] + under[2] * (1 - top[3]),
18
+ 1,
19
+ ];
20
+
21
+ const luminanceOf = ([r, g, b]: Rgba) => {
22
+ const linear = (channel: number) => (channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4);
23
+
24
+ return 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b);
25
+ };
26
+
27
+ /**
28
+ * The WCAG 2 contrast ratio of the colour `text` on the colour `on`, both CSS colours resolved inside `within`
29
+ * (`document.body` by default), so `var(--token)` works. A translucent `on` is laid over the background of
30
+ * `document.body`. For a ratio table in a drawing's `setup`: `contrastRatioOf({ text: 'var(--text)', on: 'var(--surface)', within: root })`.
31
+ */
32
+ export const contrastRatioOf = (options: { text: string; on: string; within?: Element }) => {
33
+ const probe = document.createElement('i');
34
+
35
+ probe.style.display = 'none';
36
+ (options.within ?? document.body).append(probe);
37
+
38
+ try {
39
+ const page = over(rgbaIn(probe, getComputedStyle(document.body).backgroundColor), [1, 1, 1, 1]);
40
+ const behind = over(rgbaIn(probe, options.on), page);
41
+ const [light, dark] = [luminanceOf(over(rgbaIn(probe, options.text), behind)), luminanceOf(behind)].sort(
42
+ (a, b) => b - a,
43
+ );
44
+
45
+ return ((light ?? 0) + 0.05) / ((dark ?? 0) + 0.05);
46
+ } finally {
47
+ probe.remove();
48
+ }
49
+ };
@@ -1,3 +1,5 @@
1
+ import { morph } from './morph';
2
+
1
3
  /**
2
4
  * A value inside a template. An array joins with nothing between, and `null`, `undefined` and
3
5
  * `false` write nothing, so `${rows.map(row)}` and `${isOn && html`…`}` both read as they look.
@@ -81,7 +83,7 @@ export type StateRule<TState extends string> = { click: string; go: TState };
81
83
 
82
84
  /**
83
85
  * Swaps the content of the drawing's state host for `states[rule.go]` when a click matches a rule.
84
- * The first matching rule wins. The swap runs in a View Transition where the browser has one.
86
+ * The first matching rule wins. The swap morphs the existing DOM, so CSS transitions run.
85
87
  */
86
88
  export const switchStates =
87
89
  <TState extends string>(states: Record<TState, string>, rules: StateRule<TState>[]) =>
@@ -100,13 +102,8 @@ export const switchStates =
100
102
 
101
103
  if (!rule) return;
102
104
 
103
- const swap = () => {
104
- host.innerHTML = states[rule.go];
105
- host.dataset['deStates'] = rule.go;
106
- };
107
-
108
- if (document.startViewTransition) document.startViewTransition(swap);
109
- else swap();
105
+ morph(host, states[rule.go]);
106
+ host.dataset['deStates'] = rule.go;
110
107
  });
111
108
  };
112
109
 
@@ -248,6 +245,49 @@ export const placeOverlays = (root: HTMLElement) => {
248
245
  }
249
246
  };
250
247
 
248
+ /**
249
+ * Grows `root` to cover every absolutely or fixed positioned element inside it, such as an open
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.
252
+ */
253
+ export const fitOutOfFlow = (root: HTMLElement, options: { sideways: boolean }) => {
254
+ const view = root.ownerDocument.defaultView;
255
+
256
+ if (!view) return;
257
+
258
+ const box = root.getBoundingClientRect();
259
+ const style = view.getComputedStyle(root);
260
+ const fillsViewport = (style: CSSStyleDeclaration) =>
261
+ style.position === 'fixed' &&
262
+ [style.top, style.right, style.bottom, style.left].every((edge) => parseFloat(edge) === 0);
263
+ const outOfFlow = [...root.querySelectorAll('*')]
264
+ .map((element) => ({ element, style: view.getComputedStyle(element) }))
265
+ .filter(({ style }) => ['absolute', 'fixed'].includes(style.position) && !fillsViewport(style))
266
+ .map(({ element }) => element)
267
+ .map((element) => element.getBoundingClientRect())
268
+ .filter((rect) => rect.width > 0 && rect.height > 0);
269
+ const extentOf = ({ side, start, paddings }: { side: 'bottom' | 'right'; start: number; paddings: string[] }) => {
270
+ const extent = Math.max(0, ...outOfFlow.map((rect) => Math.ceil(rect[side] - start)));
271
+ const [before, after] = paddings.map((padding) => parseFloat(padding) || 0) as [number, number];
272
+
273
+ if (extent === 0) return '';
274
+
275
+ return `${style.boxSizing === 'border-box' ? extent + after : extent - before}px`;
276
+ };
277
+
278
+ root.style.minHeight = extentOf({
279
+ side: 'bottom',
280
+ start: box.top,
281
+ paddings: [style.paddingTop, style.paddingBottom],
282
+ });
283
+ if (options.sideways)
284
+ root.style.minWidth = extentOf({
285
+ side: 'right',
286
+ start: box.left,
287
+ paddings: [style.paddingLeft, style.paddingRight],
288
+ });
289
+ };
290
+
251
291
  /** Types a drawing where it is written, so a mistake reads in its own file and not in `call.ts`. */
252
292
  export const drawing = (drawn: Drawing): Drawing => drawn;
253
293
 
@@ -1,14 +1,21 @@
1
1
  import { Drawing } from './define-call';
2
2
  import { forceBody } from './mark-part';
3
3
 
4
+ export { contrastRatioOf } from './contrast';
4
5
  export { css, drawing, html, ng } from './define-call';
5
6
  export type { Drawing } from './define-call';
6
7
  export { embedPage } from './embed-page';
7
8
  export type { EmbedPageOptions } from './embed-page';
9
+ export { iconSheet } from './icon-sheet';
10
+ export type { IconSheetIcon, IconSheetOptions } from './icon-sheet';
8
11
  export { interactionRows } from './interaction-rows';
9
12
  export type { InteractionRowsOptions } from './interaction-rows';
13
+ export { spring } from './spring';
14
+ export type { Spring } from './spring';
10
15
  export { svgAsset } from './svg-asset';
11
16
  export type { SvgAssetOptions } from './svg-asset';
17
+ export { tokenSheet } from './token-sheet';
18
+ export type { TokenSheetOptions } from './token-sheet';
12
19
 
13
20
  /** An interaction Studio can force on a part's root element. Rest is always drawn and never listed. */
14
21
  export type Interaction = 'hover' | 'focus-visible' | 'active' | 'disabled';
@@ -23,9 +30,13 @@ export type Interaction = 'hover' | 'focus-visible' | 'active' | 'disabled';
23
30
  * component or element is drawn at the size of its content, and a page at every viewport preset.
24
31
  * `interactions` lists the interactions each state is also drawn in; without it only rest is drawn.
25
32
  * `grid` draws the states it names as one table in rest, see `StateGrid`.
33
+ * `controls` names each control the part draws, keyed by the id its markup carries in `data-et-control`; `design
34
+ * complete` requires the states and surfaces its `kind` and `input` need, see `Control`.
35
+ * `motions` names each motion between two of its states, see `Motion`; a rule in `rules` can name one.
26
36
  * `checks.skip` names the render checks of `design check` the part breaks on purpose, such as
27
37
  * `CLIPPED` for a part that cuts its content. To skip a check for one element only, put
28
38
  * `data-et-skip="CLIPPED"` on it; the check then ignores that element and everything inside it.
39
+ * `contrast` feeds the contrast check of `design check`, see `PartContrast`.
29
40
  */
30
41
  export type Part<TProps, TState extends string = string> = {
31
42
  note: string;
@@ -35,10 +46,37 @@ export type Part<TProps, TState extends string = string> = {
35
46
  widths?: number[];
36
47
  interactions?: Interaction[];
37
48
  grid?: StateGrid<TState>;
49
+ controls?: Record<string, Control<TState>>;
38
50
  checks?: { skip?: RenderCheck[] };
51
+ contrast?: PartContrast;
52
+ motions?: Record<string, Motion<TState>>;
53
+ rules?: PartRule<TState>[];
39
54
  render: (props: TProps, options?: RenderOptions) => DrawnPart | Promise<DrawnPart>;
40
55
  };
41
56
 
57
+ /** 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
+ export type MotionContext = {
59
+ at: (ms: number, fn: () => void) => void;
60
+ random: () => number;
61
+ rect: (selector: string) => DOMRect;
62
+ };
63
+
64
+ /**
65
+ * A motion from the state `from` to the state `to`. `at` adds shot times in ms, `interrupt` starts the reverse motion
66
+ * at that time, `viewTransition` uses a View Transition, and `play` runs after the morph for what CSS cannot express.
67
+ */
68
+ export type Motion<TState extends string = string> = {
69
+ from: TState;
70
+ to: TState;
71
+ at?: number[];
72
+ interrupt?: number;
73
+ viewTransition?: boolean;
74
+ play?: (ctx: MotionContext) => void;
75
+ };
76
+
77
+ /** A rule of a part: `on` an event on a selector, the part switches `to` a state, with the `motion` it names. */
78
+ export type PartRule<TState extends string = string> = { on: string; to: TState; motion?: string };
79
+
42
80
  /** A drawing of a part: its `styles` hold the part's stylesheet and the ones `render` returned, so a host can add them up. */
43
81
  export type DrawnPart = Drawing & { styles: string };
44
82
 
@@ -51,17 +89,61 @@ export type StateGrid<TState extends string = string, TColumn extends string = s
51
89
  rows: Record<string, Partial<Record<TColumn, TState>>>;
52
90
  };
53
91
 
92
+ /** How a control is operated: `pointer` requires a drawn hover and active (a `field` needs no active), `keyboard` a drawn focus-visible. */
93
+ export type ControlInput = 'pointer' | 'keyboard';
94
+
95
+ /** A component or element part that draws a control and its interactions. */
96
+ export type ControlPart = `components/${string}` | `elements/${string}`;
97
+
98
+ /**
99
+ * What a control of each `kind` needs besides the interactions of its `input`. A surface is the element marked
100
+ * `data-et-surface="<id>.<surface>"`, drawn visible and filled in the state its `surfaces` entry names.
101
+ * - `disclosure`: a state that draws it collapsed, and `surfaces.body`.
102
+ * - `button`: `active`.
103
+ * - `field`: a text field (`<input>` or `<textarea>`); nothing beyond its inputs.
104
+ * - `select`, `menu`: `surfaces.open`, and options in it (`role="option"`, `role="menuitem"` or `data-et-option`)
105
+ * drawn hovered, focus-visible, and selected with a look of their own.
106
+ * - `row`: drawn selected (`aria-selected`, `aria-checked`, `aria-pressed` or `aria-current`) with a look of its own,
107
+ * and hovered while selected.
108
+ * - `tooltip`: `surfaces.tooltip`, the visible tooltip next to the control in this part.
109
+ *
110
+ * `disabled: true` also requires a state that draws it disabled with a look of its own. With `part`, the
111
+ * interactions, selected and disabled looks and option states come from that part, which must be design complete
112
+ * itself; the surfaces stay with this part. A page control needs `part`. With `input`, a state of this part draws
113
+ * each requirement, e.g. with `forceInteraction`.
114
+ */
115
+ export type Control<TState extends string = string> = (
116
+ | { kind: 'disclosure'; surfaces: { body: TState } }
117
+ | { kind: 'button' | 'row' | 'field'; surfaces?: never }
118
+ | { kind: 'select' | 'menu'; surfaces: { open: TState } }
119
+ | { kind: 'tooltip'; surfaces: { tooltip: TState } }
120
+ ) & { disabled?: boolean } & ({ part: ControlPart; input?: never } | { input: ControlInput[]; part?: never });
121
+
54
122
  /** A rule of the render check: content larger than its frame, cut off, wrapped onto a second line, or drawn at zero size. */
55
123
  export type RenderCheck = 'OVERFLOW' | 'CLIPPED' | 'WRAP' | 'ZERO-SIZE';
56
124
 
125
+ /**
126
+ * The contrast check measures every text of every state against its drawn background, to WCAG 2 AA: 4.5:1, or
127
+ * 3:1 for large text; disabled text is exempt. `pairs` names token pairs it also computes, each `text` token on
128
+ * each `on` token of an entry, for example `{ text: ['--text', '--text-muted'], on: ['--surface'] }`.
129
+ * `exceptions` exempts the text inside each `selector`, with the `reason` it may stay below the limit.
130
+ */
131
+ export type PartContrast = {
132
+ pairs?: { text: string[]; on: string[] }[];
133
+ exceptions?: { selector: string; reason: string }[];
134
+ };
135
+
57
136
  /** How a composing part draws another one: `interaction` forces it on the root, as a frame does. */
58
137
  export type RenderOptions = { interaction?: Interaction };
59
138
 
60
139
  type PartSource<TProps, TState extends string, TColumn extends string> = Omit<
61
140
  Part<TProps, TState>,
62
- 'states' | 'grid' | 'render'
141
+ 'states' | 'grid' | 'controls' | 'motions' | 'rules' | 'render'
63
142
  > & {
143
+ motions?: Record<string, Motion<NoInfer<TState>>>;
144
+ rules?: PartRule<NoInfer<TState>>[];
64
145
  states: Record<TState, NoInfer<TProps>>;
146
+ controls?: Record<string, Control<NoInfer<TState>>>;
65
147
  grid?: { columns: readonly TColumn[]; rows: Record<string, Partial<Record<NoInfer<TColumn>, NoInfer<TState>>>> };
66
148
  render: (props: TProps) => Drawing | Promise<Drawing>;
67
149
  };
@@ -2,9 +2,20 @@ import { Drawing } from './define-call';
2
2
 
3
3
  /**
4
4
  * How a page embeds another: the `state` to draw, the viewport `width` and `height` it is drawn at (`'auto'` grows
5
- * the frame to the page's content), and the `scale` the frame is shown at.
5
+ * the frame to the page's content), and the `scale` the frame is shown at. Inside the embedded page, `mark` rings
6
+ * every element matching its selector (each gets `data-et-embed-marked`), and `css` is added as a stylesheet.
6
7
  */
7
- export type EmbedPageOptions = { state?: string; width?: number; height?: number | 'auto'; scale?: number };
8
+ export type EmbedPageOptions = {
9
+ state?: string;
10
+ width?: number;
11
+ height?: number | 'auto';
12
+ scale?: number;
13
+ mark?: string;
14
+ css?: string;
15
+ };
16
+
17
+ const MARK_CSS =
18
+ '[data-et-embed-marked] { outline: 0.2rem solid var(--et-theme-color-primary-solid, Highlight); outline-offset: 0.2rem; }';
8
19
 
9
20
  const KEPT = ['product', 'sketch', 'variant', 'surface', 'color'];
10
21
 
@@ -33,11 +44,12 @@ const attribute = (text: string) => text.replace(/&/g, '&amp;').replace(/"/g, '&
33
44
  * Put the answer's `body` into the page's markup; it needs no styles or setup.
34
45
  */
35
46
  export const embedPage = (page: `pages/${string}`, options: EmbedPageOptions = {}): Drawing => {
36
- const { state = 'default', width = 1280, height = 800, scale = 1 } = options;
47
+ const { state = 'default', width = 1280, height = 800, scale = 1, mark, css } = options;
37
48
  const search = embedSearchOf({ page, state, current: globalThis.location?.search ?? '' });
38
49
  const auto = height === 'auto';
39
50
  const frameHeight = auto ? 0 : height;
40
- const frame = `<iframe data-et-embed="${attribute(page)}" data-et-embed-scale="${scale}"${auto ? ' data-et-embed-height="auto"' : ''} title="${attribute(page)}" src="/frame.html?${attribute(search)}" style="display:block;border:0;width:${width}px;height:${frameHeight}px;transform:scale(${scale});transform-origin:0 0"></iframe>`;
51
+ const inside = `${mark ? ` data-et-embed-mark="${attribute(mark)}"` : ''}${css ? ` data-et-embed-css="${attribute(css)}"` : ''}`;
52
+ const frame = `<iframe data-et-embed="${attribute(page)}" data-et-embed-scale="${scale}"${auto ? ' data-et-embed-height="auto"' : ''}${inside} title="${attribute(page)}" src="/frame.html?${attribute(search)}" style="display:block;border:0;width:${width}px;height:${frameHeight}px;transform:scale(${scale});transform-origin:0 0"></iframe>`;
41
53
 
42
54
  return {
43
55
  body:
@@ -68,3 +80,24 @@ export const fitEmbed = (embed: Element | null, height: number) => {
68
80
 
69
81
  frame.dataset['etEmbedDrawn'] = '';
70
82
  };
83
+
84
+ /**
85
+ * Called by an embedded frame after it draws and on each change of its content: adds the `css` of its `embedPage`
86
+ * once and marks every element in `root` that matches its `mark`. Does nothing in a frame no page embeds.
87
+ */
88
+ export const applyEmbed = (embed: Element | null, root: HTMLElement) => {
89
+ if (!(embed as HTMLElement | null)?.hasAttribute('data-et-embed')) return;
90
+
91
+ const { etEmbedMark: mark, etEmbedCss: css } = (embed as HTMLElement).dataset;
92
+ const document = root.ownerDocument;
93
+
94
+ if ((mark || css) && !document.querySelector('style[data-et-embed-css]')) {
95
+ const style = document.createElement('style');
96
+
97
+ style.dataset['etEmbedCss'] = '';
98
+ style.textContent = `${mark ? MARK_CSS : ''}\n${css ?? ''}`;
99
+ document.head.append(style);
100
+ }
101
+
102
+ if (mark) for (const element of root.querySelectorAll(mark)) element.setAttribute('data-et-embed-marked', '');
103
+ };
@@ -0,0 +1,151 @@
1
+ import { Drawing, html } from './define-call';
2
+ import { svgAsset } from './svg-asset';
3
+
4
+ export type IconSheetIcon = {
5
+ name: string;
6
+ svg: string;
7
+ grid?: number;
8
+ padding?: number;
9
+ };
10
+
11
+ export type IconSheetOptions = {
12
+ view: 'catalogue' | 'construction';
13
+ title: string;
14
+ note?: string;
15
+ icons: IconSheetIcon[];
16
+ sizes?: number[];
17
+ size?: number;
18
+ columns?: number;
19
+ };
20
+
21
+ const SHEET_STYLES = `
22
+ .ics-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); }
23
+ .ics-root svg { display: block; }
24
+ .ics-title { margin: 0; font-size: var(--text-xl, 1.7rem); line-height: var(--text-xl-line, 2.2rem); font-weight: var(--weight-strong, 600); }
25
+ .ics-note { margin: 0; max-width: 64rem; color: var(--text-muted, oklch(0.72 0 0)); }
26
+ .ics-group { margin: 0; font-size: var(--text-lg, 1.5rem); line-height: var(--text-lg-line, 2rem); font-weight: var(--weight-strong, 600); }
27
+ .ics-muted { color: var(--text-muted, oklch(0.72 0 0)); }
28
+ .ics-name { justify-self: start; font-family: var(--font-code, ui-monospace, "SF Mono", Menlo, Consolas, monospace); color: var(--text-muted, oklch(0.72 0 0)); }
29
+ .ics-catalogue { display: grid; align-items: center; justify-items: center; column-gap: var(--space-4, 1.6rem); row-gap: var(--space-2, 0.8rem); }
30
+ .ics-size { font-size: var(--text-sm, 1.15rem); }
31
+ .ics-cell { display: flex; }
32
+ .ics-construction { display: grid; gap: var(--space-5, 2rem); }
33
+ .ics-item { display: flex; flex-direction: column; align-items: center; gap: var(--space-2, 0.8rem); }
34
+ .ics-frame { position: relative; display: flex; border: 1px solid var(--surface-border, oklch(0.34 0 0)); background: var(--surface, oklch(0.17 0 0)); }
35
+ .ics-icon { position: relative; }
36
+ .ics-guides { position: absolute; inset: 0; width: 100%; height: 100%; color: var(--accent, oklch(0.7 0.15 250)); pointer-events: none; }
37
+ .ics-guides path, .ics-guides rect { vector-effect: non-scaling-stroke; stroke-width: 1px; }
38
+ .ics-pixels { opacity: 0.18; }
39
+ .ics-key { opacity: 0.7; stroke-dasharray: 4 3; }
40
+ `;
41
+
42
+ const VIEW_BOX = /\bviewBox\s*=\s*["']\s*[-\d.]+[\s,]+[-\d.]+[\s,]+([\d.]+)[\s,]+([\d.]+)/u;
43
+
44
+ const gridOf = (icon: IconSheetIcon) => {
45
+ if (icon.grid) return icon.grid;
46
+ const [, width, height] = VIEW_BOX.exec(icon.svg) ?? [];
47
+ if (!width || !height) throw new Error(`iconSheet: "${icon.name}" has no viewBox, so it needs a grid`);
48
+
49
+ return Math.max(Number(width), Number(height));
50
+ };
51
+
52
+ const groupsOf = (icons: IconSheetIcon[]) => {
53
+ const groups = new Map<number, IconSheetIcon[]>();
54
+ for (const icon of icons) groups.set(gridOf(icon), [...(groups.get(gridOf(icon)) ?? []), icon]);
55
+
56
+ return [...groups];
57
+ };
58
+
59
+ const gridLabel = (grid: number) => (grid <= 48 ? `${grid} px grid` : `${grid}-unit grid`);
60
+
61
+ const guides = (grid: number, { cls, paths }: { cls: string; paths: string }) =>
62
+ `<svg class="ics-guides ${cls}" viewBox="0 0 ${grid} ${grid}" fill="none" stroke="currentColor" aria-hidden="true" data-grid="${grid}">${paths}</svg>`;
63
+
64
+ const pixelLines = (grid: number) => {
65
+ const step = grid > 48 ? grid / 16 : 1;
66
+ const lines = Array.from({ length: Math.round(grid / step) - 1 }, (_, index) => (index + 1) * step);
67
+
68
+ return guides(grid, {
69
+ cls: 'ics-pixels',
70
+ paths: lines.map((at) => `<path d="M${at} 0V${grid}M0 ${at}H${grid}"/>`).join(''),
71
+ });
72
+ };
73
+
74
+ const keyLines = (grid: number, padding: number) =>
75
+ guides(grid, {
76
+ cls: 'ics-key',
77
+ paths: `<path d="M${grid / 2} 0V${grid}M0 ${grid / 2}H${grid}"/><rect x="${padding}" y="${padding}" width="${grid - 2 * padding}" height="${grid - 2 * padding}"/>`,
78
+ });
79
+
80
+ const drawIcon = (icon: IconSheetIcon, { size, cls }: { size: number; cls?: string }) =>
81
+ svgAsset(icon.svg, { size, class: cls, attrs: { 'aria-hidden': 'true' } });
82
+
83
+ const catalogue = (icons: IconSheetIcon[], sizes: number[]) =>
84
+ html`<div
85
+ class="ics-catalogue"
86
+ style="grid-template-columns: max-content repeat(${sizes.length}, minmax(var(--space-7, 3.2rem), max-content))"
87
+ >
88
+ <span></span>
89
+ ${sizes.map((size) => html`<span class="ics-size ics-muted">${size} px</span>`).join('')}
90
+ ${icons
91
+ .map(
92
+ (icon) =>
93
+ html`<span class="ics-name">${icon.name}</span>${sizes
94
+ .map((size) => html`<span class="ics-cell">${drawIcon(icon, { size })}</span>`)
95
+ .join('')}`,
96
+ )
97
+ .join('')}
98
+ </div>`;
99
+
100
+ const construction = (
101
+ icons: IconSheetIcon[],
102
+ { grid, size, columns }: { grid: number; size: number; columns: number },
103
+ ) =>
104
+ html`<div class="ics-construction" style="grid-template-columns: repeat(${columns}, max-content)">
105
+ ${icons
106
+ .map(
107
+ (icon) =>
108
+ html`<div class="ics-item">
109
+ <span class="ics-frame"
110
+ >${pixelLines(grid)}${drawIcon(icon, { size, cls: 'ics-icon' })}${keyLines(grid, icon.padding ?? grid / 8)}</span
111
+ >
112
+ <span class="ics-name">${icon.name}</span>
113
+ </div>`,
114
+ )
115
+ .join('')}
116
+ </div>`;
117
+
118
+ /**
119
+ * Draws a foundation sheet of icons. `catalogue` shows every icon at each of `sizes` with its name; `construction` shows
120
+ * each icon `size` px large on its grid, with the grid lines, the centre lines and the padding box. An icon's grid is its
121
+ * `grid`, else the larger side of its viewBox; its padding is `padding` grid units, `grid / 8` by default.
122
+ * Icons on different grids are drawn in one group per grid.
123
+ */
124
+ export const iconSheet = ({
125
+ view,
126
+ title,
127
+ note,
128
+ icons,
129
+ sizes = [16, 24, 32],
130
+ size = 96,
131
+ columns = 5,
132
+ }: IconSheetOptions): Drawing => {
133
+ const groups = groupsOf(icons);
134
+ const labelled = groups.length > 1;
135
+
136
+ return {
137
+ body: html`<div class="ics-root">
138
+ <h2 class="ics-title">${title}</h2>
139
+ ${note && html`<p class="ics-note">${note}</p>`}
140
+ ${groups
141
+ .map(
142
+ ([grid, members]) =>
143
+ html`${labelled && html`<h3 class="ics-group">${gridLabel(grid)}</h3>`}${
144
+ view === 'catalogue' ? catalogue(members, sizes) : construction(members, { grid, size, columns })
145
+ }`,
146
+ )
147
+ .join('')}
148
+ </div>`,
149
+ styles: SHEET_STYLES,
150
+ };
151
+ };