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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/design/define-call.ts +69 -0
  3. package/design/define-part.ts +43 -9
  4. package/design/embed-page.ts +37 -4
  5. package/design/icon-sheet.ts +151 -0
  6. package/design/logo-sheet.ts +52 -0
  7. package/design/mark-part.ts +79 -2
  8. package/design/motion-drive.ts +215 -0
  9. package/design/motion-runtime.ts +224 -5
  10. package/design/motion-tokens.ts +177 -0
  11. package/design/spring.ts +53 -0
  12. package/design/token-sheet.ts +87 -0
  13. package/design/web/frame-fill.ts +4 -0
  14. package/design/web/frame-scheme.ts +44 -0
  15. package/design/web/frame.ts +239 -31
  16. package/package.json +1 -1
  17. package/src/lib/design/calls-tsconfig.d.ts +2 -0
  18. package/src/lib/design/calls-tsconfig.js +11 -5
  19. package/src/lib/design/calls-tsconfig.js.map +1 -1
  20. package/src/lib/design/check.d.ts +1 -0
  21. package/src/lib/design/check.js +62 -6
  22. package/src/lib/design/check.js.map +1 -1
  23. package/src/lib/design/contact-sheet.d.ts +9 -0
  24. package/src/lib/design/contact-sheet.js +38 -4
  25. package/src/lib/design/contact-sheet.js.map +1 -1
  26. package/src/lib/design/design-command.js +56 -7
  27. package/src/lib/design/design-command.js.map +1 -1
  28. package/src/lib/design/draw-ledger.d.ts +36 -0
  29. package/src/lib/design/draw-ledger.js +52 -0
  30. package/src/lib/design/draw-ledger.js.map +1 -0
  31. package/src/lib/design/force-interactions.d.ts +2 -1
  32. package/src/lib/design/force-interactions.js +7 -4
  33. package/src/lib/design/force-interactions.js.map +1 -1
  34. package/src/lib/design/mcp-command.d.ts +81 -0
  35. package/src/lib/design/mcp-command.js +422 -0
  36. package/src/lib/design/mcp-command.js.map +1 -0
  37. package/src/lib/design/motion-command.d.ts +71 -0
  38. package/src/lib/design/motion-command.js +308 -0
  39. package/src/lib/design/motion-command.js.map +1 -0
  40. package/src/lib/design/motion-endpoint.d.ts +61 -0
  41. package/src/lib/design/motion-endpoint.js +133 -0
  42. package/src/lib/design/motion-endpoint.js.map +1 -0
  43. package/src/lib/design/motion-flow.d.ts +5 -0
  44. package/src/lib/design/motion-flow.js +29 -0
  45. package/src/lib/design/motion-flow.js.map +1 -0
  46. package/src/lib/design/motion-shot.d.ts +43 -0
  47. package/src/lib/design/motion-shot.js +97 -0
  48. package/src/lib/design/motion-shot.js.map +1 -0
  49. package/src/lib/design/paths.d.ts +14 -0
  50. package/src/lib/design/paths.js +50 -1
  51. package/src/lib/design/paths.js.map +1 -1
  52. package/src/lib/design/ready-shots.d.ts +7 -0
  53. package/src/lib/design/ready-shots.js +22 -1
  54. package/src/lib/design/ready-shots.js.map +1 -1
  55. package/src/lib/design/render-packages.d.ts +14 -0
  56. package/src/lib/design/render-packages.js +18 -3
  57. package/src/lib/design/render-packages.js.map +1 -1
  58. package/src/lib/design/render-worker.js +1 -0
  59. package/src/lib/design/render-worker.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.d.ts +2 -2
  63. package/src/lib/design/serve.js +191 -21
  64. package/src/lib/design/serve.js.map +1 -1
  65. package/src/lib/design/shot-command.d.ts +13 -4
  66. package/src/lib/design/shot-command.js +30 -24
  67. package/src/lib/design/shot-command.js.map +1 -1
  68. package/src/lib/design/shot-store.d.ts +24 -0
  69. package/src/lib/design/shot-store.js +120 -0
  70. package/src/lib/design/shot-store.js.map +1 -0
  71. package/src/lib/design/shot.d.ts +37 -1
  72. package/src/lib/design/shot.js +185 -34
  73. package/src/lib/design/shot.js.map +1 -1
  74. package/src/lib/design/sketch-command.js +197 -51
  75. package/src/lib/design/sketch-command.js.map +1 -1
  76. package/src/lib/design/ssr-shims.d.ts +2 -0
  77. package/src/lib/design/ssr-shims.js +12 -1
  78. package/src/lib/design/ssr-shims.js.map +1 -1
  79. package/src/lib/design/studio-api.d.ts +31 -0
  80. package/src/lib/design/studio-api.js +118 -1
  81. package/src/lib/design/studio-api.js.map +1 -1
  82. package/src/lib/design/system/check-system.d.ts +6 -2
  83. package/src/lib/design/system/check-system.js +102 -20
  84. package/src/lib/design/system/check-system.js.map +1 -1
  85. package/src/lib/design/system/colour.d.ts +4 -1
  86. package/src/lib/design/system/colour.js +54 -2
  87. package/src/lib/design/system/colour.js.map +1 -1
  88. package/src/lib/design/system/complete-command.d.ts +1 -1
  89. package/src/lib/design/system/complete-command.js +80 -6
  90. package/src/lib/design/system/complete-command.js.map +1 -1
  91. package/src/lib/design/system/complete.d.ts +172 -9
  92. package/src/lib/design/system/complete.js +792 -167
  93. package/src/lib/design/system/complete.js.map +1 -1
  94. package/src/lib/design/system/contrast-check.d.ts +6 -1
  95. package/src/lib/design/system/contrast-check.js +167 -12
  96. package/src/lib/design/system/contrast-check.js.map +1 -1
  97. package/src/lib/design/system/drawn-open-points.d.ts +16 -0
  98. package/src/lib/design/system/drawn-open-points.js +43 -0
  99. package/src/lib/design/system/drawn-open-points.js.map +1 -0
  100. package/src/lib/design/system/exceptions.d.ts +59 -0
  101. package/src/lib/design/system/exceptions.js +132 -0
  102. package/src/lib/design/system/exceptions.js.map +1 -0
  103. package/src/lib/design/system/flows/flow.d.ts +3 -0
  104. package/src/lib/design/system/flows/flow.js +5 -3
  105. package/src/lib/design/system/flows/flow.js.map +1 -1
  106. package/src/lib/design/system/handoff.d.ts +11 -0
  107. package/src/lib/design/system/handoff.js +30 -0
  108. package/src/lib/design/system/handoff.js.map +1 -0
  109. package/src/lib/design/system/inventory-approval.d.ts +41 -0
  110. package/src/lib/design/system/inventory-approval.js +71 -0
  111. package/src/lib/design/system/inventory-approval.js.map +1 -0
  112. package/src/lib/design/system/inventory.d.ts +65 -0
  113. package/src/lib/design/system/inventory.js +168 -0
  114. package/src/lib/design/system/inventory.js.map +1 -0
  115. package/src/lib/design/system/list-system.d.ts +4 -1
  116. package/src/lib/design/system/list-system.js +11 -3
  117. package/src/lib/design/system/list-system.js.map +1 -1
  118. package/src/lib/design/system/open-points.d.ts +101 -0
  119. package/src/lib/design/system/open-points.js +156 -0
  120. package/src/lib/design/system/open-points.js.map +1 -0
  121. package/src/lib/design/system/part-ready.d.ts +52 -0
  122. package/src/lib/design/system/part-ready.js +101 -0
  123. package/src/lib/design/system/part-ready.js.map +1 -0
  124. package/src/lib/design/system/read-open-points.d.ts +27 -0
  125. package/src/lib/design/system/read-open-points.js +81 -0
  126. package/src/lib/design/system/read-open-points.js.map +1 -0
  127. package/src/lib/design/system/ready-command.d.ts +7 -0
  128. package/src/lib/design/system/ready-command.js +65 -0
  129. package/src/lib/design/system/ready-command.js.map +1 -0
  130. package/src/lib/design/system/render-check.d.ts +2 -0
  131. package/src/lib/design/system/render-check.js +216 -32
  132. package/src/lib/design/system/render-check.js.map +1 -1
  133. package/src/lib/design/system/resolve-product.d.ts +4 -4
  134. package/src/lib/design/system/resolve-product.js +22 -7
  135. package/src/lib/design/system/resolve-product.js.map +1 -1
  136. package/src/lib/design/system/rules/implements.d.ts +7 -0
  137. package/src/lib/design/system/rules/implements.js +43 -0
  138. package/src/lib/design/system/rules/implements.js.map +1 -0
  139. package/src/lib/design/system/rules/motion.d.ts +2 -0
  140. package/src/lib/design/system/rules/motion.js +26 -1
  141. package/src/lib/design/system/rules/motion.js.map +1 -1
  142. package/src/lib/design/system/rules/option-values.d.ts +15 -0
  143. package/src/lib/design/system/rules/option-values.js +145 -0
  144. package/src/lib/design/system/rules/option-values.js.map +1 -0
  145. package/src/lib/design/system/rules/stale-replace.js +80 -3
  146. package/src/lib/design/system/rules/stale-replace.js.map +1 -1
  147. package/src/lib/design/system/rules/token-selectors.js +1 -1
  148. package/src/lib/design/system/rules/token-selectors.js.map +1 -1
  149. package/src/lib/design/system/sketch/baseline.d.ts +11 -2
  150. package/src/lib/design/system/sketch/baseline.js +4 -2
  151. package/src/lib/design/system/sketch/baseline.js.map +1 -1
  152. package/src/lib/design/system/sketch/check-sketches.d.ts +1 -0
  153. package/src/lib/design/system/sketch/check-sketches.js +4 -2
  154. package/src/lib/design/system/sketch/check-sketches.js.map +1 -1
  155. package/src/lib/design/system/sketch/host-edit.d.ts +4 -0
  156. package/src/lib/design/system/sketch/host-edit.js +68 -0
  157. package/src/lib/design/system/sketch/host-edit.js.map +1 -0
  158. package/src/lib/design/system/sketch/merge.d.ts +3 -2
  159. package/src/lib/design/system/sketch/merge.js +79 -12
  160. package/src/lib/design/system/sketch/merge.js.map +1 -1
  161. package/src/lib/design/system/sketch/merged-parts.d.ts +11 -1
  162. package/src/lib/design/system/sketch/merged-parts.js +24 -3
  163. package/src/lib/design/system/sketch/merged-parts.js.map +1 -1
  164. package/src/lib/design/system/sketch/no-visible-change.d.ts +6 -0
  165. package/src/lib/design/system/sketch/no-visible-change.js +25 -1
  166. package/src/lib/design/system/sketch/no-visible-change.js.map +1 -1
  167. package/src/lib/design/system/sketch/overlay.js +11 -3
  168. package/src/lib/design/system/sketch/overlay.js.map +1 -1
  169. package/src/lib/design/system/sketch/part-definition.d.ts +36 -0
  170. package/src/lib/design/system/sketch/part-definition.js +56 -2
  171. package/src/lib/design/system/sketch/part-definition.js.map +1 -1
  172. package/src/lib/design/system/sketch/sketch.d.ts +47 -0
  173. package/src/lib/design/system/sketch/sketch.js +137 -44
  174. package/src/lib/design/system/sketch/sketch.js.map +1 -1
  175. package/src/lib/design/system/sketch/unchanged-states.d.ts +4 -1
  176. package/src/lib/design/system/sketch/unchanged-states.js +9 -5
  177. package/src/lib/design/system/sketch/unchanged-states.js.map +1 -1
  178. package/src/lib/design/system/sketch/unused-closure.d.ts +12 -0
  179. package/src/lib/design/system/sketch/unused-closure.js +195 -0
  180. package/src/lib/design/system/sketch/unused-closure.js.map +1 -0
  181. package/src/lib/design/system/sketch/unused-files.d.ts +1 -1
  182. package/src/lib/design/system/sketch/unused-files.js +6 -3
  183. package/src/lib/design/system/sketch/unused-files.js.map +1 -1
  184. package/src/lib/design/system/system-modules.js +6 -9
  185. package/src/lib/design/system/system-modules.js.map +1 -1
  186. package/src/lib/design/system/system-tsconfig.d.ts +2 -1
  187. package/src/lib/design/system/system-tsconfig.js +10 -6
  188. package/src/lib/design/system/system-tsconfig.js.map +1 -1
  189. package/src/lib/design/system/ts-source.d.ts +3 -0
  190. package/src/lib/design/system/ts-source.js +21 -4
  191. package/src/lib/design/system/ts-source.js.map +1 -1
  192. package/src/lib/design/thumb.d.ts +31 -4
  193. package/src/lib/design/thumb.js +105 -40
  194. package/src/lib/design/thumb.js.map +1 -1
@@ -9,7 +9,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.completenessOf = exports.completenessGapLineOf = exports.completenessGapsOf = void 0;
12
+ exports.partsCompletenessOf = exports.completenessOf = exports.inventoryIdsOf = exports.seePart = exports.PLAIN_CONTROL_SELECTOR = exports.SDK_CONTROLS = exports.isMissingStateGap = exports.completenessGapLineOf = exports.completenessGapsOf = exports.findingKeyOf = exports.isFindingOf = exports.smallChangeHintsOf = exports.controlStateGapsOf = exports.isControlPart = exports.isInteractionState = exports.declaredControlsOf = void 0;
13
+ const fs_1 = require("fs");
13
14
  const path_1 = require("path");
14
15
  const browser_1 = require("../browser");
15
16
  const force_interactions_1 = require("../force-interactions");
@@ -17,13 +18,41 @@ const serve_1 = require("../serve");
17
18
  const shot_1 = require("../shot");
18
19
  const thumb_1 = require("../thumb");
19
20
  const resolve_product_1 = require("./resolve-product");
21
+ const contact_sheet_1 = require("../contact-sheet");
20
22
  const baseline_1 = require("./sketch/baseline");
21
23
  const merged_parts_1 = require("./sketch/merged-parts");
22
24
  const system_modules_1 = require("./system-modules");
23
- const CHECKED_KINDS = ['disclosure'];
25
+ const exceptions_1 = require("./exceptions");
26
+ const inventory_approval_1 = require("./inventory-approval");
27
+ const option_values_1 = require("./rules/option-values");
28
+ const KINDS = {
29
+ disclosure: { surface: 'body', closed: 'collapsed' },
30
+ button: { adds: ['active'] },
31
+ select: { surface: 'open', options: { interactions: ['hover', 'focus-visible'], selected: true } },
32
+ menu: { surface: 'open', options: { interactions: ['hover', 'focus-visible', 'active'], selected: false } },
33
+ row: { selects: true, virtualFocus: true },
34
+ field: { noActive: true },
35
+ tooltip: { surface: 'tooltip' },
36
+ };
37
+ const CHECKED_KINDS = Object.keys(KINDS);
24
38
  const INPUT_INTERACTIONS = { pointer: ['hover', 'active'], keyboard: ['focus-visible'] };
39
+ const MARKS = {
40
+ selected: 'aria-selected="true", aria-checked="true", aria-pressed="true" or aria-current',
41
+ disabled: 'disabled or aria-disabled="true"',
42
+ };
43
+ const CONTROL_MARKS = Object.assign(Object.assign({}, MARKS), { disabled: `${MARKS.disabled} on it or a data-et-control-part in it` });
25
44
  const isRecord = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
26
- const requiredInteractionsOf = (control) => control.input.flatMap((input) => { var _a; return (_a = INPUT_INTERACTIONS[input]) !== null && _a !== void 0 ? _a : []; });
45
+ const requiredInteractionsOf = (control) => {
46
+ var _a, _b;
47
+ return [
48
+ ...new Set([
49
+ ...control.input
50
+ .flatMap((input) => { var _a; return (_a = INPUT_INTERACTIONS[input]) !== null && _a !== void 0 ? _a : []; })
51
+ .filter((interaction) => { var _a; return !(((_a = KINDS[control.kind]) === null || _a === void 0 ? void 0 : _a.noActive) && interaction === 'active'); }),
52
+ ...((_b = (_a = KINDS[control.kind]) === null || _a === void 0 ? void 0 : _a.adds) !== null && _b !== void 0 ? _b : []),
53
+ ]),
54
+ ];
55
+ };
27
56
  const gridGapOf = (options) => {
28
57
  const { grid, states, interactions } = options;
29
58
  const fix = `draw its states with interactionRows, which gives the grid a column per interaction (${interactions.join(', ')})`;
@@ -55,21 +84,102 @@ const declaredControlsOf = (value) => isRecord(value)
55
84
  surfaces: isRecord(declared['surfaces'])
56
85
  ? Object.fromEntries(Object.entries(declared['surfaces']).filter((entry) => typeof entry[1] === 'string'))
57
86
  : {},
87
+ disabled: declared['disabled'] === true,
58
88
  },
59
89
  ];
60
90
  }))
61
91
  : null;
92
+ exports.declaredControlsOf = declaredControlsOf;
93
+ const INTERACTION_WORDS = new Set(['hover', 'hovered', 'focus', 'focused', 'pressed', 'active', 'selected']);
94
+ const NAME_WORDS = new Set([...INTERACTION_WORDS, 'visible', 'within', 'invalid', 'disabled', 'checked', 'open']);
95
+ const wordsOf = (state) => state
96
+ .replace(/([a-z0-9])([A-Z])/gu, '$1-$2')
97
+ .toLowerCase()
98
+ .split(/[^a-z0-9]+/u)
99
+ .filter((word) => word !== '');
100
+ /**
101
+ * Whether a state name shows an interaction: its last word, split at `-`, `_`, spaces and camelCase, or the whole name
102
+ * is hover, hovered, focus, focused, pressed, active or selected. `row-hover`, `collapseFocus` and `focus-visible`
103
+ * are; `active-only` and `inactive` are not.
104
+ */
105
+ const isInteractionState = (state) => {
106
+ var _a, _b;
107
+ const words = wordsOf(state);
108
+ while (words.length > 1 && ['visible', 'within'].includes((_a = words[words.length - 1]) !== null && _a !== void 0 ? _a : ''))
109
+ words.pop();
110
+ return INTERACTION_WORDS.has((_b = words[words.length - 1]) !== null && _b !== void 0 ? _b : '');
111
+ };
112
+ exports.isInteractionState = isInteractionState;
113
+ /** Whether a part may draw interaction states: it is no page, and its `controls` declare a control of a checked kind. */
114
+ const isControlPart = (options) => {
115
+ var _a;
116
+ return !options.part.startsWith('pages/') &&
117
+ Object.values((_a = (0, exports.declaredControlsOf)(options.controls)) !== null && _a !== void 0 ? _a : {}).some((control) => CHECKED_KINDS.includes(control.kind));
118
+ };
119
+ exports.isControlPart = isControlPart;
120
+ /**
121
+ * One `CONTROL-STATE` gap per element whose interaction states a part draws that is no control part. The element is
122
+ * the state name without its interaction words (`row-hover` names the row), or `control` when nothing is left.
123
+ */
124
+ const controlStateGapsOf = (options) => {
125
+ var _a, _b;
126
+ const { part, states } = options;
127
+ if ((0, exports.isControlPart)(options))
128
+ return [];
129
+ const elements = new Map();
130
+ for (const state of states.filter(exports.isInteractionState)) {
131
+ const element = wordsOf(state)
132
+ .filter((word) => !NAME_WORDS.has(word))
133
+ .join('-') || 'control';
134
+ elements.set(element, [...((_a = elements.get(element)) !== null && _a !== void 0 ? _a : []), state]);
135
+ }
136
+ const why = part.startsWith('pages/')
137
+ ? 'a page is no control part'
138
+ : `${part} declares no control of a kind ${CHECKED_KINDS.join(', ')}`;
139
+ const declaresNone = Object.keys((_b = (0, exports.declaredControlsOf)(options.controls)) !== null && _b !== void 0 ? _b : {}).length === 0;
140
+ const fixOf = (element) => part.startsWith('components/') && declaresNone
141
+ ? `declare its controls (Q164), or extract the ${element} as its own control part`
142
+ : `extract the ${element} as its own control part`;
143
+ return [...elements].map(([element, shown]) => ({
144
+ code: 'CONTROL-STATE',
145
+ target: part,
146
+ detail: `${shown.length === 1 ? 'state' : 'states'} ${statesShownOf(shown)} draw${shown.length === 1 ? 's' : ''} an interaction (hover, focus, pressed, active or selected), which only a control part draws, and ${why} - ${fixOf(element)} and draw ${shown.length === 1 ? 'this state' : 'these states'} there`,
147
+ }));
148
+ };
149
+ exports.controlStateGapsOf = controlStateGapsOf;
150
+ const SMALL_CHANGE = 0.1;
151
+ /**
152
+ * One hint per state of an element or component that is no control part whose pixels differ from its first state in
153
+ * under a tenth of the frame. Pages compose parts, so they get none; interaction states are left to `CONTROL-STATE`.
154
+ */
155
+ const smallChangeHintsOf = (options) => options.part.startsWith('pages/') || (0, exports.isControlPart)(options)
156
+ ? []
157
+ : options.changes
158
+ .filter(({ state, share }) => share > 0 && share < SMALL_CHANGE && !(0, exports.isInteractionState)(state))
159
+ .map(({ state, base, share }) => `HINT ${options.part} ${state} - changes ${(share * 100).toFixed(1)} % of the frame against ${base} - extract the changed element as its own part`);
160
+ exports.smallChangeHintsOf = smallChangeHintsOf;
161
+ /** Whether a discovered element is an undeclared or unclassified finding of the part, not owned or declared. */
162
+ const isFindingOf = (declared) => (found) => {
163
+ var _a;
164
+ return ((_a = found.owner) !== null && _a !== void 0 ? _a : null) === null &&
165
+ !found.within.some((id) => { var _a, _b; return ((_b = (_a = declared[id]) === null || _a === void 0 ? void 0 : _a.part) !== null && _b !== void 0 ? _b : null) !== null; }) &&
166
+ !(found.kind === 'control' && found.marked !== null);
167
+ };
168
+ exports.isFindingOf = isFindingOf;
169
+ const findingKeyOf = (found) => `${found.kind} ${found.what} ${found.path}`;
170
+ exports.findingKeyOf = findingKeyOf;
171
+ const NATIVE_CONTROLS = ['button', 'a[href]', 'input:not([type="hidden"])', 'select', 'textarea', 'summary'];
62
172
  const discoveryGapsOf = (options) => {
63
173
  var _a, _b, _c, _d;
64
174
  const { part, declared, frames } = options;
65
- const ownedByPart = (found) => found.within.some((id) => { var _a, _b; return ((_b = (_a = declared[id]) === null || _a === void 0 ? void 0 : _a.part) !== null && _b !== void 0 ? _b : null) !== null; });
175
+ const isFinding = (0, exports.isFindingOf)(declared);
66
176
  const groups = new Map();
67
177
  for (const frame of frames.filter((seen) => seen.interaction === null)) {
68
178
  const counts = new Map();
69
179
  for (const found of (_a = frame.found) !== null && _a !== void 0 ? _a : []) {
70
- if (ownedByPart(found) || (found.kind === 'control' && found.marked !== null))
180
+ if (!isFinding(found))
71
181
  continue;
72
- const key = `${found.kind} ${found.what} ${found.path}`;
182
+ const key = (0, exports.findingKeyOf)(found);
73
183
  const group = (_b = groups.get(key)) !== null && _b !== void 0 ? _b : { found, states: [], count: 0 };
74
184
  counts.set(key, ((_c = counts.get(key)) !== null && _c !== void 0 ? _c : 0) + 1);
75
185
  if (!group.states.includes(frame.state))
@@ -81,13 +191,14 @@ const discoveryGapsOf = (options) => {
81
191
  return [...groups.values()]
82
192
  .sort((a, b) => a.found.path.localeCompare(b.found.path) || a.found.what.localeCompare(b.found.what))
83
193
  .map(({ found, states, count }) => {
84
- const shown = states.length > 4 ? `${states.slice(0, 3).join(', ')} and ${states.length - 3} more` : states.join(', ');
85
- const where = `${found.what} at ${found.path} in ${shown}${count > 1 ? ` (${count} of them)` : ''}`;
194
+ const where = `${found.what} at ${found.path} in ${statesShownOf(states)}${count > 1 ? ` (${count} of them)` : ''}`;
86
195
  return found.kind === 'control'
87
196
  ? {
88
197
  code: 'CONTROL-UNDECLARED',
89
198
  target: part,
90
- detail: `${where} - mark it with data-et-control and declare it in controls`,
199
+ detail: `${where} - ${NATIVE_CONTROLS.includes(found.what)
200
+ ? 'mark it with control(markup, name) from @ethlete/components/design/control and declare the name in controls'
201
+ : 'mark it with data-et-control and declare it in controls'}`,
91
202
  }
92
203
  : {
93
204
  code: 'CONTROL-UNCLASSIFIED',
@@ -96,18 +207,96 @@ const discoveryGapsOf = (options) => {
96
207
  };
97
208
  });
98
209
  };
99
- const interactionGapOf = (options) => {
210
+ const isUndeclaredControlGap = (gap) => gap.code === 'CONTROL-UNDECLARED';
211
+ const statesShownOf = (states) => states.length > 4 ? `${states.slice(0, 3).join(', ')} and ${states.length - 3} more` : states.join(', ');
212
+ const overrideGapsOf = (options) => {
213
+ var _a, _b;
214
+ const groups = new Map();
215
+ for (const frame of options.frames.filter((seen) => seen.interaction === null))
216
+ for (const override of (_a = frame.overrides) !== null && _a !== void 0 ? _a : []) {
217
+ const key = `${override.owner} ${override.selector} ${override.path}`;
218
+ const group = (_b = groups.get(key)) !== null && _b !== void 0 ? _b : { override, states: [] };
219
+ if (!group.states.includes(frame.state))
220
+ group.states.push(frame.state);
221
+ groups.set(key, group);
222
+ }
223
+ return [...groups.values()]
224
+ .sort((a, b) => a.override.owner.localeCompare(b.override.owner) || a.override.path.localeCompare(b.override.path))
225
+ .map(({ override, states }) => ({
226
+ code: 'CONTROL-OVERRIDDEN',
227
+ target: options.part,
228
+ detail: `"${override.selector}" sets ${override.properties.join(', ')} on ${override.path}, drawn by ${override.owner}, in ${statesShownOf(states)} - change ${override.owner} instead, or mark the control with data-et-control and declare it in controls`,
229
+ }));
230
+ };
231
+ const ownerGapsOf = (options) => {
232
+ var _a, _b, _c;
233
+ const { part, declared, frames, referenced } = options;
234
+ const named = new Set(Object.values(declared).map((control) => control.part));
235
+ const owned = new Map();
236
+ for (const frame of frames.filter((seen) => seen.interaction === null))
237
+ for (const found of (_a = frame.found) !== null && _a !== void 0 ? _a : []) {
238
+ const owner = (_b = found.owner) !== null && _b !== void 0 ? _b : null;
239
+ if (owner === null || named.has(owner))
240
+ continue;
241
+ owned.set(owner, ((_c = owned.get(owner)) !== null && _c !== void 0 ? _c : new Set()).add(`${found.what} ${found.path}`));
242
+ }
243
+ return [...owned.entries()]
244
+ .sort(([a], [b]) => a.localeCompare(b))
245
+ .flatMap(([owner, controls]) => {
246
+ const used = referenced[owner];
247
+ const drawn = `${owner} draws ${controls.size} control(s) here`;
248
+ if (typeof used === 'string')
249
+ return [{ code: 'CONTROL-PART-INCOMPLETE', target: part, detail: `${drawn}: ${used}` }];
250
+ if (!used || used.length === 0)
251
+ return [];
252
+ return [
253
+ {
254
+ code: 'CONTROL-PART-INCOMPLETE',
255
+ target: part,
256
+ detail: `${drawn} and is not design complete: ${used.length} gap(s) - run design complete ${owner}`,
257
+ },
258
+ ];
259
+ });
260
+ };
261
+ const ownersOf = (frames) => [
262
+ ...new Set(frames
263
+ .filter((seen) => seen.interaction === null)
264
+ .flatMap((seen) => { var _a; return ((_a = seen.found) !== null && _a !== void 0 ? _a : []).map((found) => { var _a; return (_a = found.owner) !== null && _a !== void 0 ? _a : null; }); })
265
+ .filter((owner) => owner !== null)),
266
+ ];
267
+ const undefinedClassesGapOf = (options) => {
100
268
  var _a;
269
+ const { forced, what, interaction } = options;
270
+ const seen = forced.find((force) => { var _a; return (_a = force.undefinedClasses) === null || _a === void 0 ? void 0 : _a.length; });
271
+ if (!seen)
272
+ return null;
273
+ const classes = (_a = seen.undefinedClasses) !== null && _a !== void 0 ? _a : [];
274
+ return `state ${seen.state} forces ${interaction} on ${what}, but no stylesheet defines its ${classes.length === 1 ? 'class' : 'classes'} ${classes.join(', ')} - Tailwind generates no utility of that name; use a colour or value the product's theme defines`;
275
+ };
276
+ const interactionGapOf = (options) => {
277
+ var _a, _b;
101
278
  const { frames, id, interaction } = options;
102
279
  const forced = frames.flatMap((frame) => frame.controls
103
280
  .filter((control) => control.id === id)
104
281
  .flatMap((control) => control.forced.filter((force) => force.interaction === interaction))
105
- .map((force) => ({ state: frame.state, differs: force.differs })));
282
+ .map((force) => ({ state: frame.state, differs: force.differs, undefinedClasses: force.undefinedClasses })));
106
283
  if (forced.some((force) => force.differs))
107
284
  return null;
108
285
  if (forced.length > 0)
109
- return `state ${(_a = forced[0]) === null || _a === void 0 ? void 0 : _a.state} forces ${interaction} on it, but its box draws no different pixel than at rest`;
110
- return `no state draws it ${interaction} - draw a state that forces ${interaction} on the data-et-control element, e.g. with forceInteraction`;
286
+ return ((_a = undefinedClassesGapOf({ forced, what: 'it', interaction })) !== null && _a !== void 0 ? _a : `state ${(_b = forced[0]) === null || _b === void 0 ? void 0 : _b.state} forces ${interaction} on it, but its box draws no different pixel than at rest - if a descendant draws ${interaction}, mark it with data-et-control-part`);
287
+ return `no state draws it ${interaction} - draw a state that forces ${interaction} on the data-et-control element, e.g. with forceInteraction - a data-et-control-part in it takes the force too`;
288
+ };
289
+ const keyboardActiveRowGapOf = (options) => {
290
+ const { instances, forcedGap } = options;
291
+ const active = instances.filter((seen) => seen.keyboard);
292
+ const plain = instances.filter((seen) => !seen.keyboard && !seen.selected && !seen.disabled);
293
+ if (active.length === 0)
294
+ return `${forcedGap}; no state marks it data-active-source="keyboard" either`;
295
+ if (plain.length === 0)
296
+ return `a state marks it data-active-source="keyboard", but no state draws it plain (not selected, active or disabled) to compare it with`;
297
+ if (active.some((seen) => plain.some((other) => other.look !== seen.look)))
298
+ return null;
299
+ return `it looks the same with data-active-source="keyboard" as plain - give the active look its own colours`;
111
300
  };
112
301
  const stillGapOf = (options) => {
113
302
  var _a;
@@ -118,30 +307,139 @@ const stillGapOf = (options) => {
118
307
  return `state ${frame.state} changes ${(_a = force.still) === null || _a === void 0 ? void 0 : _a.join(', ')} in one frame - give them a transition with the product's motion tokens`;
119
308
  return null;
120
309
  };
121
- const disclosureGapsOf = (options) => {
122
- const { frames, id, control, states } = options;
310
+ const lookGapOf = (options) => {
311
+ const { seen, flag, what, marks = MARKS } = options;
312
+ const on = seen.filter((one) => one.flag);
313
+ const off = seen.filter((one) => !one.flag);
314
+ if (on.length === 0)
315
+ return `no state draws ${what} ${flag} - mark it with ${marks[flag]} in a state`;
316
+ if (off.length === 0)
317
+ return `every state draws ${what} ${flag} - draw it without ${flag} too, to compare`;
318
+ if (!on.some((one) => off.some((other) => one.look !== other.look)))
319
+ return `${what} looks the same ${flag} as without - give the ${flag} look its own colours`;
320
+ return null;
321
+ };
322
+ const forcedGapOf = (options) => {
323
+ var _a, _b;
324
+ const { forced, what, interaction } = options;
325
+ if (forced.some((force) => force.differs))
326
+ return null;
327
+ if (forced.length > 0)
328
+ return ((_a = undefinedClassesGapOf(options)) !== null && _a !== void 0 ? _a : `state ${(_b = forced[0]) === null || _b === void 0 ? void 0 : _b.state} forces ${interaction} on ${what}, but its box draws no different pixel than at rest`);
329
+ return `no state draws ${what} ${interaction} - force ${interaction} on it, e.g. with forceInteraction`;
330
+ };
331
+ const surfaceGapsOf = (options) => {
332
+ const { frames, id, control, states, surface, closed } = options;
123
333
  const rest = frames.filter((frame) => frame.interaction === null);
124
- const body = control.surfaces['body'];
125
- const bodyId = `${id}.body`;
126
- const showsBody = (frame) => frame.surfaces.some((surface) => surface.id === bodyId && surface.shown);
334
+ const state = control.surfaces[surface];
335
+ const surfaceId = `${id}.${surface}`;
336
+ const shows = (frame) => frame.surfaces.some((seen) => seen.id === surfaceId && seen.shown);
127
337
  const drawsControl = (frame) => frame.controls.some((seen) => seen.id === id);
128
338
  const gaps = [];
129
- for (const interaction of control.part === null ? requiredInteractionsOf(control) : []) {
130
- const gap = interactionGapOf({ frames, id, interaction });
339
+ if (closed && !rest.some((frame) => drawsControl(frame) && !shows(frame)))
340
+ gaps.push([closed, `every state that draws it also shows its ${surface} - draw a state with ${surfaceId} closed`]);
341
+ if (state === undefined)
342
+ gaps.push([surface, `declares no surfaces.${surface} - name the state that draws it open`]);
343
+ else if (!states.includes(state))
344
+ gaps.push([surface, `surfaces.${surface} names state ${state}, which the part does not declare`]);
345
+ else if (!rest.some((frame) => frame.state === state && drawsControl(frame) && shows(frame)))
346
+ gaps.push([
347
+ surface,
348
+ `state ${state} draws no visible, non-empty data-et-surface="${surfaceId}" next to data-et-control="${id}"`,
349
+ ]);
350
+ return gaps;
351
+ };
352
+ const KEYBOARD_ACTIVE = 'aria-activedescendant or data-active-source="keyboard"';
353
+ const isKeyboardActive = (option) => !!option.named || !!option.keyboard;
354
+ const isPlain = (option) => !option.selected && !isKeyboardActive(option) && option.forced.length === 0;
355
+ const keyboardActiveGapOf = (options) => {
356
+ var _a, _b;
357
+ const { frames, shownIn, forcedGap } = options;
358
+ const active = frames
359
+ .map((frame) => ({ state: frame.state, options: shownIn(frame) }))
360
+ .filter((frame) => frame.options.some(isKeyboardActive));
361
+ const compared = active.filter((frame) => frame.options.some(isPlain));
362
+ if (active.length === 0)
363
+ return `${forcedGap}; no option is marked active by ${KEYBOARD_ACTIVE} either`;
364
+ if (compared.length === 0)
365
+ return `state ${(_a = active[0]) === null || _a === void 0 ? void 0 : _a.state} marks an option active by ${KEYBOARD_ACTIVE}, but draws no plain option (not selected, active or forced) to compare it with`;
366
+ if (compared.some((frame) => frame.options.some((option) => isKeyboardActive(option) && frame.options.some((plain) => isPlain(plain) && plain.look !== option.look))))
367
+ return null;
368
+ return `state ${(_b = compared[0]) === null || _b === void 0 ? void 0 : _b.state} marks an option active by ${KEYBOARD_ACTIVE}, but it looks the same as a plain option - give the active option its own look`;
369
+ };
370
+ const optionGapsOf = (options) => {
371
+ const { frames, surfaceId, interactions } = options;
372
+ const shownIn = (frame) => frame.surfaces.filter((seen) => seen.id === surfaceId && seen.shown).flatMap((seen) => { var _a; return (_a = seen.options) !== null && _a !== void 0 ? _a : []; });
373
+ const what = `an option in ${surfaceId}`;
374
+ const gaps = [];
375
+ for (const interaction of interactions) {
376
+ const forced = frames.flatMap((frame) => shownIn(frame).flatMap((option) => option.forced
377
+ .filter((force) => force.interaction === interaction)
378
+ .map((force) => ({ state: frame.state, differs: force.differs, undefinedClasses: force.undefinedClasses }))));
379
+ const gap = forcedGapOf({ forced, what, interaction });
380
+ const missing = gap && interaction === 'focus-visible' ? keyboardActiveGapOf({ frames, shownIn, forcedGap: gap }) : gap;
381
+ if (missing)
382
+ gaps.push([`option-${interaction}`, missing]);
383
+ }
384
+ if (!options.selected)
385
+ return gaps;
386
+ const seen = frames
387
+ .filter((frame) => frame.interaction === null)
388
+ .flatMap((frame) => shownIn(frame)
389
+ .filter((option) => option.forced.length === 0 && !isKeyboardActive(option))
390
+ .map((option) => ({ look: option.look, flag: option.selected })));
391
+ const selected = lookGapOf({ seen, flag: 'selected', what });
392
+ if (selected)
393
+ gaps.push(['option-selected', selected]);
394
+ return gaps;
395
+ };
396
+ const requirementGapsOf = (options) => {
397
+ var _a;
398
+ const { frames, id, control } = options;
399
+ const kind = (_a = KINDS[control.kind]) !== null && _a !== void 0 ? _a : {};
400
+ const own = control.part === null;
401
+ const instances = frames
402
+ .filter((frame) => frame.interaction === null)
403
+ .flatMap((frame) => frame.controls.filter((seen) => seen.id === id && seen.forced.length === 0));
404
+ const gaps = [];
405
+ for (const interaction of own ? requiredInteractionsOf(control) : []) {
406
+ const forcedGap = interactionGapOf({ frames, id, interaction });
407
+ const gap = forcedGap && interaction === 'focus-visible' && kind.virtualFocus
408
+ ? keyboardActiveRowGapOf({ instances, forcedGap })
409
+ : forcedGap;
131
410
  if (gap)
132
411
  gaps.push([interaction, gap]);
133
412
  }
134
- if (!rest.some((frame) => drawsControl(frame) && !showsBody(frame)))
135
- gaps.push(['collapsed', `every state that draws it also shows its body - draw a state with ${bodyId} closed`]);
136
- if (body === undefined)
137
- gaps.push(['body', 'declares no surfaces.body - name the state that draws it open']);
138
- else if (!states.includes(body))
139
- gaps.push(['body', `surfaces.body names state ${body}, which the part does not declare`]);
140
- else if (!rest.some((frame) => frame.state === body && drawsControl(frame) && showsBody(frame)))
141
- gaps.push([
142
- 'body',
143
- `state ${body} draws no visible, non-empty data-et-surface="${bodyId}" next to data-et-control="${id}"`,
144
- ]);
413
+ if (own && kind.selects) {
414
+ const selected = lookGapOf({
415
+ seen: instances.map((seen) => { var _a; return ({ look: seen.look, flag: (_a = seen.selected) !== null && _a !== void 0 ? _a : false }); }),
416
+ flag: 'selected',
417
+ what: 'it',
418
+ });
419
+ const forced = frames.flatMap((frame) => frame.controls
420
+ .filter((seen) => seen.id === id && seen.selected)
421
+ .flatMap((seen) => seen.forced.filter((force) => force.interaction === 'hover'))
422
+ .map((force) => ({ state: frame.state, differs: force.differs, undefinedClasses: force.undefinedClasses })));
423
+ const hovered = forcedGapOf({ forced, what: 'it selected', interaction: 'hover' });
424
+ if (selected)
425
+ gaps.push(['selected', selected]);
426
+ if (hovered)
427
+ gaps.push(['selected-hover', hovered]);
428
+ }
429
+ if (own && control.disabled) {
430
+ const disabled = lookGapOf({
431
+ seen: instances.map((seen) => { var _a; return ({ look: seen.look, flag: (_a = seen.disabled) !== null && _a !== void 0 ? _a : false }); }),
432
+ flag: 'disabled',
433
+ what: 'it',
434
+ marks: CONTROL_MARKS,
435
+ });
436
+ if (disabled)
437
+ gaps.push(['disabled', disabled]);
438
+ }
439
+ if (kind.surface)
440
+ gaps.push(...surfaceGapsOf(Object.assign(Object.assign({}, options), { surface: kind.surface, closed: kind.closed })));
441
+ if (own && kind.options && kind.surface)
442
+ gaps.push(...optionGapsOf(Object.assign({ frames, surfaceId: `${id}.${kind.surface}` }, kind.options)));
145
443
  return gaps;
146
444
  };
147
445
  /**
@@ -150,8 +448,8 @@ const disclosureGapsOf = (options) => {
150
448
  */
151
449
  const completenessGapsOf = (options) => {
152
450
  var _a;
153
- const { part, states, frames, reviewed, grid, referenced = {} } = options;
154
- const declared = (_a = declaredControlsOf(options.controls)) !== null && _a !== void 0 ? _a : {};
451
+ const { part, states, frames, reviewed, grid, referenced = {}, inventory } = options;
452
+ const declared = (_a = (0, exports.declaredControlsOf)(options.controls)) !== null && _a !== void 0 ? _a : {};
155
453
  const gaps = [];
156
454
  if (!reviewed)
157
455
  gaps.push({
@@ -208,7 +506,7 @@ const completenessGapsOf = (options) => {
208
506
  gaps.push({ code: 'CONTROL-UNDRAWN', target: `${part}.${id}`, detail: `no state draws data-et-control="${id}"` });
209
507
  continue;
210
508
  }
211
- for (const [requirement, detail] of disclosureGapsOf({ frames, id, control, states }))
509
+ for (const [requirement, detail] of requirementGapsOf({ frames, id, control, states }))
212
510
  gaps.push({ code: 'REQUIREMENT-MISSING', target: `${part}.${id}.${requirement}`, detail });
213
511
  for (const interaction of interactions) {
214
512
  const gap = stillGapOf({ frames, id, interaction });
@@ -216,11 +514,27 @@ const completenessGapsOf = (options) => {
216
514
  gaps.push({ code: 'CONTROL-TRANSITION-MISSING', target: `${part}.${id}.${interaction}`, detail: gap });
217
515
  }
218
516
  }
219
- return [...gaps, ...discoveryGapsOf({ part, declared, frames })];
517
+ return [
518
+ ...gaps,
519
+ ...discoveryGapsOf({ part, declared, frames }).filter((gap) => !isUndeclaredControlGap(gap)),
520
+ ...ownerGapsOf({ part, declared, frames, referenced }),
521
+ ...overrideGapsOf({ part, frames }),
522
+ ...inventoryGapsOf({ part, inventory }),
523
+ ];
220
524
  };
221
525
  exports.completenessGapsOf = completenessGapsOf;
526
+ const inventoryGapsOf = ({ part, inventory }) => {
527
+ const reason = inventory ? (0, inventory_approval_1.inventoryReasonOf)(inventory) : null;
528
+ return reason === null
529
+ ? []
530
+ : [{ code: 'INVENTORY-UNAPPROVED', target: part, detail: `${reason} - check and approve it in Studio` }];
531
+ };
222
532
  const completenessGapLineOf = (gap) => `${gap.code} ${gap.target} - ${gap.detail}`;
223
533
  exports.completenessGapLineOf = completenessGapLineOf;
534
+ const MISSING_STATE_CODES = ['CONTROL-UNDRAWN', 'CONTROL-GRID-MISSING', 'REQUIREMENT-MISSING'];
535
+ /** Whether a gap is a declared control that no state draws, or that lacks a drawn interaction, look or surface. */
536
+ const isMissingStateGap = (gap) => MISSING_STATE_CODES.includes(gap.code);
537
+ exports.isMissingStateGap = isMissingStateGap;
224
538
  const NO_MOTION = '*, *::before, *::after { transition: none !important; animation: none !important; }';
225
539
  const PAD = 8;
226
540
  const COLOUR_PROPERTIES = [
@@ -264,6 +578,28 @@ const coversOf = (property) => {
264
578
  return ['all', 'border', 'border-color', property.replace(/-color$/, ''), property];
265
579
  return ['all', property];
266
580
  };
581
+ const undefinedVariantClasses = (at) => `((at) => {
582
+ const control = document.querySelector('[data-et-complete="' + at + '"]');
583
+ const selectors = [];
584
+ const collect = (rules) => {
585
+ for (const rule of rules) {
586
+ if (rule.selectorText) selectors.push(rule.selectorText);
587
+ if (rule.cssRules) collect(rule.cssRules);
588
+ }
589
+ };
590
+ for (const sheet of document.styleSheets) {
591
+ try {
592
+ collect(sheet.cssRules);
593
+ } catch {}
594
+ }
595
+ const tokens = new Set([control, ...control.querySelectorAll('*')].flatMap((element) => [...element.classList]));
596
+ return [...tokens].filter((token) => token.includes(':') && !selectors.some((selector) => selector.includes('.' + CSS.escape(token))));
597
+ })(${JSON.stringify(at)})`;
598
+ const VARIANT_WORDS = { hover: 'hover', active: 'active', 'focus-visible': 'focus' };
599
+ const isVariantOf = (token, interaction) => token
600
+ .split(':')
601
+ .slice(0, -1)
602
+ .some((variant) => { var _a; return variant.includes((_a = VARIANT_WORDS[interaction]) !== null && _a !== void 0 ? _a : interaction); });
267
603
  const stillChangesOf = (rest, forced) => forced.names.flatMap((name, element) => {
268
604
  var _a;
269
605
  const { properties, durations } = (_a = forced.transitions[element]) !== null && _a !== void 0 ? _a : { properties: [], durations: [] };
@@ -301,7 +637,7 @@ const INTERACTIVE_ROLES = [
301
637
  'textbox',
302
638
  'treeitem',
303
639
  ];
304
- const SDK_CONTROLS = [
640
+ exports.SDK_CONTROLS = [
305
641
  '[et-button]',
306
642
  '[et-icon-button]',
307
643
  '[et-text-button]',
@@ -341,48 +677,23 @@ const SDK_CONTROLS = [
341
677
  'et-time-input',
342
678
  'et-toggletip',
343
679
  ];
344
- const LIKELY_CONTROLS = [
345
- ...SDK_CONTROLS,
346
- 'button',
347
- 'a[href]',
348
- 'input:not([type="hidden"])',
349
- 'select',
350
- 'textarea',
351
- 'summary',
352
- '[tabindex]',
353
- ...INTERACTIVE_ROLES.map((role) => `[role="${role}"]`),
354
- ];
680
+ const PLAIN_CONTROLS = [...NATIVE_CONTROLS, '[tabindex]', ...INTERACTIVE_ROLES.map((role) => `[role="${role}"]`)];
681
+ const LIKELY_CONTROLS = [...exports.SDK_CONTROLS, ...PLAIN_CONTROLS];
682
+ const LOOK_PROPERTIES = [...COLOUR_PROPERTIES, 'opacity', 'box-shadow', 'font-weight', 'text-decoration-line'];
683
+ const OPTION_SELECTOR = '[role="option"], [role^="menuitem"], [data-et-option]';
684
+ const COMPOSITE_ROLES = ['grid', 'listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'];
685
+ const FOCUS_CONTAINER = COMPOSITE_ROLES.flatMap((role) => [
686
+ `[role="${role}"][tabindex="-1"]`,
687
+ `[role="${role}"][aria-activedescendant]`,
688
+ ]).join(', ');
689
+ exports.PLAIN_CONTROL_SELECTOR = `:is(${PLAIN_CONTROLS.join(', ')}):not(${FOCUS_CONTAINER})`;
355
690
  const discoverControls = `((root) => {
356
691
  const LIKELY = ${JSON.stringify(LIKELY_CONTROLS)};
357
692
  const likely = LIKELY.join(', ');
358
- const sdk = ${JSON.stringify(SDK_CONTROLS.join(', '))};
693
+ const container = ${JSON.stringify(FOCUS_CONTAINER)};
694
+ const isLikely = (element) => element.matches(likely) && !element.matches(container);
695
+ const sdk = ${JSON.stringify(exports.SDK_CONTROLS.join(', '))};
359
696
  const rendered = (element) => element.getClientRects().length > 0;
360
- const nameOf = (element) => {
361
- const cls = [...element.classList].find((name) => /^[a-z][\\w-]*$/i.test(name));
362
- return element.tagName.toLowerCase() + (cls ? '.' + cls : '');
363
- };
364
- const pathOf = (element) => {
365
- const names = [];
366
- for (let at = element; at && at !== root; at = at.parentElement) names.unshift(nameOf(at));
367
- return (names.length > 4 ? '... > ' : '') + names.slice(-4).join(' > ');
368
- };
369
- const splitTop = (selector) => {
370
- const parts = [];
371
- let depth = 0;
372
- let from = 0;
373
- for (let at = 0; at < selector.length; at++) {
374
- const char = selector[at];
375
- if (char === '\\\\') at++;
376
- else if (char === '(' || char === '[') depth++;
377
- else if (char === ')' || char === ']') depth--;
378
- else if (char === ',' && depth === 0) {
379
- parts.push(selector.slice(from, at).trim());
380
- from = at + 1;
381
- }
382
- }
383
- parts.push(selector.slice(from).trim());
384
- return parts.filter(Boolean);
385
- };
386
697
  const HOVER = /(?<!\\\\):hover(?![\\w-])/g;
387
698
  const FORCED_HOVER = /:is\\(:hover,\\s*\\[data-et-force~="?hover"?\\]\\)/g;
388
699
  const hoveredOf = (written) => {
@@ -408,7 +719,7 @@ const discoverControls = `((root) => {
408
719
  for (const rule of rules) {
409
720
  if (rule instanceof CSSStyleRule) {
410
721
  const own = splitTop(rule.selectorText).map((selector) =>
411
- parent === null ? selector : selector.includes('&') ? selector.replaceAll('&', ':is(' + parent + ')') : ':is(' + parent + ') ' + selector,
722
+ nested(selector, parent),
412
723
  );
413
724
  for (const selector of own) {
414
725
  const target = hoveredOf(selector);
@@ -426,43 +737,166 @@ const discoverControls = `((root) => {
426
737
  walk(sheet.cssRules, null);
427
738
  } catch {}
428
739
  }
740
+ const markerOf = (at, element) =>
741
+ at.getAttribute('data-et-control') ?? (element.matches(${JSON.stringify(OPTION_SELECTOR)}) ? at.getAttribute('data-et-surface')?.split('.')[0] : null) ?? null;
429
742
  const withinOf = (element) => {
430
743
  const ids = [];
431
- for (let at = element; at && at !== root; at = at.parentElement)
432
- if (at.hasAttribute('data-et-control')) ids.push(at.getAttribute('data-et-control'));
744
+ for (let at = element; at && at !== root; at = at.parentElement) {
745
+ const id = markerOf(at, element);
746
+ if (id !== null) ids.push(id);
747
+ }
433
748
  return ids;
434
749
  };
435
750
  const markedOf = (element) => {
436
751
  if (element.hasAttribute('data-et-control')) return element.getAttribute('data-et-control');
437
752
  for (let at = element.parentElement; at && at !== root; at = at.parentElement) {
438
- if (at.matches(likely)) return null;
439
- if (at.hasAttribute('data-et-control')) return at.getAttribute('data-et-control');
753
+ if (isLikely(at)) return null;
754
+ const id = markerOf(at, element);
755
+ if (id !== null) return id;
440
756
  }
441
757
  return null;
442
758
  };
443
759
  const found = [];
444
760
  for (const element of root.querySelectorAll('*')) {
445
761
  if (!rendered(element)) continue;
446
- const what = LIKELY.find((selector) => element.matches(selector));
762
+ const what = !element.matches(container) && LIKELY.find((selector) => element.matches(selector));
447
763
  if (what) {
448
764
  const inSdk = element.parentElement.closest(sdk);
449
765
  if (inSdk !== null && root.contains(inSdk)) continue;
450
- found.push({ kind: 'control', what: what.startsWith('[') ? element.tagName.toLowerCase() + what : what, path: pathOf(element), marked: markedOf(element), within: withinOf(element) });
766
+ found.push({ kind: 'control', what: what.startsWith('[') ? element.tagName.toLowerCase() + what : what, path: pathOf(element), marked: markedOf(element), within: withinOf(element), owner: ownerOf(element)?.key ?? null, box: frameBoxOf(element) });
451
767
  continue;
452
768
  }
453
769
  const cursor = getComputedStyle(element).cursor;
454
770
  const pointer = cursor === 'pointer' && getComputedStyle(element.parentElement).cursor !== 'pointer';
455
771
  if (!pointer && !hovered.has(element)) continue;
456
- if (element.closest('[data-et-control], ' + likely) !== null) continue;
772
+ let inControl = false;
773
+ for (let at = element; at && !inControl; at = at.parentElement) inControl = at.hasAttribute('data-et-control') || isLikely(at);
774
+ if (inControl) continue;
457
775
  const reason = [pointer ? 'cursor: pointer' : '', hovered.has(element) ? ':hover rule' : ''].filter(Boolean).join(' and ');
458
- found.push({ kind: 'unclassified', what: nameOf(element) + ' with ' + reason, path: pathOf(element), marked: null, within: [] });
776
+ found.push({ kind: 'unclassified', what: nameOf(element) + ' with ' + reason, path: pathOf(element), marked: null, within: [], owner: ownerOf(element)?.key ?? null, box: frameBoxOf(element) });
459
777
  }
460
778
  return found;
461
779
  })`;
462
- const seeFrame = (discover) => `(() => {
780
+ const ROOT_LAYOUT = [
781
+ '^margin',
782
+ '^inset',
783
+ '^(top|right|bottom|left)$',
784
+ '^(min-|max-)?(width|height|inline-size|block-size)$',
785
+ '^position$',
786
+ '^z-index$',
787
+ '^flex(-grow|-shrink|-basis)?$',
788
+ '^grid-(area|row|column)',
789
+ '^(align|justify|place)-self$',
790
+ '^order$',
791
+ '^(transform|translate|rotate|scale)$',
792
+ ];
793
+ const findOverrides = `((ownCss) => {
794
+ const LAYOUT = new RegExp(${JSON.stringify(ROOT_LAYOUT.join('|'))});
795
+ const USER_ACTION = /(?<!\\\\):(hover|focus-visible|focus-within|focus|active)(?![\\w-])/g;
796
+ const PSEUDO_ELEMENT = /::?(before|after|marker|placeholder|selection|backdrop|file-selector-button|first-line|first-letter)(?![\\w-])/g;
797
+ const declaredByHost = (element) => {
798
+ for (let at = element; at && at !== root.parentElement; at = at.parentElement)
799
+ if (at.hasAttribute('data-et-control') && ownerOf(at) === null) return true;
800
+ return false;
801
+ };
802
+ const overrides = [];
803
+ const seen = new Set();
804
+ const check = (selector, style) => {
805
+ const properties = [...new Set(style.cssText.split(';').map((declaration) => declaration.split(':')[0].trim()).filter(Boolean))];
806
+ if (properties.length === 0) return;
807
+ let matched = [];
808
+ try {
809
+ matched = [...root.querySelectorAll(selector.replace(PSEUDO_ELEMENT, '').replace(USER_ACTION, ':where(*)'))];
810
+ } catch {
811
+ return;
812
+ }
813
+ for (const element of matched) {
814
+ const owner = ownerOf(element);
815
+ if (owner === null || declaredByHost(element)) continue;
816
+ const look = owner.at === element ? properties.filter((property) => !LAYOUT.test(property)) : properties;
817
+ const key = selector + ' ' + pathOf(element);
818
+ if (look.length === 0 || seen.has(key)) continue;
819
+ seen.add(key);
820
+ overrides.push({ owner: owner.key, selector, path: pathOf(element), properties: look });
821
+ }
822
+ };
823
+ const walk = (rules, parent) => {
824
+ for (const rule of rules) {
825
+ if (rule instanceof CSSStyleRule) {
826
+ const own = splitTop(rule.selectorText).map((selector) => nested(selector, parent));
827
+ for (const selector of own) check(selector, rule.style);
828
+ if (rule.cssRules && rule.cssRules.length > 0) walk(rule.cssRules, own.join(', '));
829
+ } else if (parent !== null && rule.constructor.name === 'CSSNestedDeclarations') check(parent, rule.style);
830
+ else if (rule.cssRules) walk(rule.cssRules, parent);
831
+ }
832
+ };
833
+ for (const text of ownCss) {
834
+ const sheet = new CSSStyleSheet();
835
+ try {
836
+ sheet.replaceSync(text);
837
+ } catch {
838
+ continue;
839
+ }
840
+ walk(sheet.cssRules, null);
841
+ }
842
+ return overrides;
843
+ })`;
844
+ /** A control root and every `data-et-control-part` whose nearest `data-et-control` is that root, in the page. */
845
+ const controlPartsOf = `((control) => [
846
+ control,
847
+ ...[...control.querySelectorAll('[data-et-control-part]')].filter((part) => part.parentElement.closest('[data-et-control]') === control),
848
+ ])`;
849
+ const seeFrame = (options) => `(() => {
463
850
  const root = document.getElementById('root');
464
851
  const refused = root.innerText.trim();
465
852
  if (refused.startsWith('design-explore:')) return { refused: refused.split('\\n')[0] };
853
+ const ORIGIN = ${options.clip} ? root.getBoundingClientRect() : { left: -scrollX, top: -scrollY };
854
+ const frameBoxOf = (element) => {
855
+ const box = element.getBoundingClientRect();
856
+ return box.width > 0 && box.height > 0
857
+ ? { x: Math.round(box.left - ORIGIN.left), y: Math.round(box.top - ORIGIN.top), width: Math.round(box.width), height: Math.round(box.height) }
858
+ : null;
859
+ };
860
+ const OWN = ${JSON.stringify(options.part)};
861
+ const tokensOf = (element, name) => (element.getAttribute(name) ?? '').split(' ').filter(Boolean);
862
+ const ownerOf = (element) => {
863
+ const slotted = tokensOf(element, 'data-et-slot');
864
+ for (let at = element; at; at = at.parentElement)
865
+ for (const key of tokensOf(at, 'data-et-part')) {
866
+ const index = slotted.indexOf(key);
867
+ if (index >= 0) slotted.splice(index, 1);
868
+ else return key === OWN ? null : { key, at };
869
+ }
870
+ return null;
871
+ };
872
+ const nameOf = (element) => {
873
+ const cls = [...element.classList].find((name) => /^[a-z][\\w-]*$/i.test(name));
874
+ return element.tagName.toLowerCase() + (cls ? '.' + cls : '');
875
+ };
876
+ const pathOf = (element) => {
877
+ const names = [];
878
+ for (let at = element; at && at !== root; at = at.parentElement) names.unshift(nameOf(at));
879
+ return (names.length > 4 ? '... > ' : '') + names.slice(-4).join(' > ');
880
+ };
881
+ const splitTop = (selector) => {
882
+ const parts = [];
883
+ let depth = 0;
884
+ let from = 0;
885
+ for (let at = 0; at < selector.length; at++) {
886
+ const char = selector[at];
887
+ if (char === '\\\\') at++;
888
+ else if (char === '(' || char === '[') depth++;
889
+ else if (char === ')' || char === ']') depth--;
890
+ else if (char === ',' && depth === 0) {
891
+ parts.push(selector.slice(from, at).trim());
892
+ from = at + 1;
893
+ }
894
+ }
895
+ parts.push(selector.slice(from).trim());
896
+ return parts.filter(Boolean);
897
+ };
898
+ const nested = (selector, parent) =>
899
+ parent === null ? selector : selector.includes('&') ? selector.replaceAll('&', ':is(' + parent + ')') : ':is(' + parent + ') ' + selector;
466
900
  const shown = (element) => {
467
901
  const box = element.getBoundingClientRect();
468
902
  let { left, top, right, bottom } = box;
@@ -479,38 +913,79 @@ const seeFrame = (discover) => `(() => {
479
913
  }
480
914
  return element.innerText.trim() !== '' || !!element.querySelector('img, svg, canvas, video, iframe, input, select, textarea');
481
915
  };
482
- const controls = [...root.querySelectorAll('[data-et-control]')].map((element, at) => {
916
+ const LOOK = ${JSON.stringify(LOOK_PROPERTIES)};
917
+ const lookOf = (element) => {
918
+ const own = LOOK.map((name) => getComputedStyle(element).getPropertyValue(name));
919
+ const inside = [...element.querySelectorAll('*')].map((child) => {
920
+ const style = getComputedStyle(child);
921
+ return LOOK.map((name) => style.getPropertyValue(name)).join(' ');
922
+ });
923
+ return JSON.stringify([own, [...new Set(inside)].sort()]);
924
+ };
925
+ const named = new Set([...root.querySelectorAll('[aria-activedescendant]')].filter((element) => ownerOf(element) === null).map((element) => element.getAttribute('aria-activedescendant')));
926
+ const isSelected = (element) =>
927
+ ['aria-selected', 'aria-checked', 'aria-pressed'].some((name) => element.getAttribute(name) === 'true') ||
928
+ (element.hasAttribute('aria-current') && element.getAttribute('aria-current') !== 'false');
929
+ const isDisabled = (element) =>
930
+ element.matches(':disabled') || element.getAttribute('aria-disabled') === 'true' || tokensOf(element, 'data-et-force').includes('disabled');
931
+ const isKeyboard = (element) => element.getAttribute('data-active-source') === 'keyboard';
932
+ let next = 0;
933
+ const tagged = (element) => {
934
+ const at = element.hasAttribute('data-et-complete') ? Number(element.getAttribute('data-et-complete')) : next++;
483
935
  element.setAttribute('data-et-complete', String(at));
484
936
  const box = element.getBoundingClientRect();
485
937
  const x = Math.max(0, box.x + scrollX - ${PAD});
486
938
  const y = Math.max(0, box.y + scrollY - ${PAD});
487
939
  return {
488
- id: element.getAttribute('data-et-control'),
489
940
  at,
490
- forced: (element.getAttribute('data-et-force') ?? '').split(' ').filter(Boolean),
941
+ forced: tokensOf(element, 'data-et-force'),
491
942
  box: box.width > 0 && box.height > 0 ? { x, y, width: box.width + ${PAD * 2}, height: box.height + ${PAD * 2} } : null,
943
+ selected: isSelected(element),
944
+ look: lookOf(element),
945
+ };
946
+ };
947
+ const controls = [...root.querySelectorAll('[data-et-control]')].filter((element) => ownerOf(element) === null).map((element) => {
948
+ const parts = ${controlPartsOf}(element);
949
+ const forced = new Set(parts.flatMap((part) => tokensOf(part, 'data-et-force')));
950
+ for (const part of parts) if (forced.size > 0) part.setAttribute('data-et-force', [...forced].join(' '));
951
+ return {
952
+ id: element.getAttribute('data-et-control'),
953
+ disabled: parts.some(isDisabled),
954
+ keyboard: parts.some(isKeyboard),
955
+ frameBox: frameBoxOf(element),
956
+ ...tagged(element),
492
957
  };
493
958
  });
494
- const surfaces = [...root.querySelectorAll('[data-et-surface]')].map((element) => ({
959
+ const surfaces = [...root.querySelectorAll('[data-et-surface]')].filter((element) => ownerOf(element) === null).map((element) => ({
495
960
  id: element.getAttribute('data-et-surface'),
496
961
  shown: shown(element),
962
+ options: [...element.querySelectorAll(${JSON.stringify(OPTION_SELECTOR)})].filter((option) => ownerOf(option) === null && shown(option)).map((option) => ({ ...tagged(option), named: option.id !== '' && named.has(option.id), keyboard: isKeyboard(option) })),
497
963
  }));
498
- return { controls, surfaces, found: ${discover} ? ${discoverControls}(root) : [] };
964
+ const discover = ${options.discover};
965
+ return {
966
+ controls,
967
+ surfaces,
968
+ found: discover ? ${discoverControls}(root) : [],
969
+ overrides: discover ? ${findOverrides}(${JSON.stringify(options.ownCss)}) : [],
970
+ };
499
971
  })()`;
500
972
  const forceOn = ({ at, interaction, on, }) => `((at, interaction, on) => {
501
- const element = document.querySelector('[data-et-complete="' + at + '"]');
502
- const tokens = new Set((element.getAttribute('data-et-force') ?? '').split(' ').filter(Boolean));
503
- if (on) tokens.add(interaction);
504
- else tokens.delete(interaction);
505
- if (tokens.size > 0) element.setAttribute('data-et-force', [...tokens].join(' '));
506
- else element.removeAttribute('data-et-force');
973
+ const found = document.querySelector('[data-et-complete="' + at + '"]');
974
+ for (const element of found.hasAttribute('data-et-control') ? ${controlPartsOf}(found) : [found]) {
975
+ const tokens = new Set((element.getAttribute('data-et-force') ?? '').split(' ').filter(Boolean));
976
+ if (on) tokens.add(interaction);
977
+ else tokens.delete(interaction);
978
+ if (tokens.size > 0) element.setAttribute('data-et-force', [...tokens].join(' '));
979
+ else element.removeAttribute('data-et-force');
980
+ }
507
981
  })(${JSON.stringify(at)}, ${JSON.stringify(interaction)}, ${JSON.stringify(on)})`;
508
982
  const FORCEABLE = new Set(['hover', 'focus-visible', 'active']);
509
983
  const seeFrames = (options) => __awaiter(void 0, void 0, void 0, function* () {
510
- const { browser, base, frames } = options;
984
+ const { browser, base, frames, part, ownCss } = options;
511
985
  const context = yield browser.newContext();
512
986
  const seen = [];
513
987
  const refused = [];
988
+ const shots = [];
514
989
  try {
515
990
  const page = yield context.newPage();
516
991
  for (const frame of frames) {
@@ -518,117 +993,256 @@ const seeFrames = (options) => __awaiter(void 0, void 0, void 0, function* () {
518
993
  yield (0, thumb_1.openFrame)(page, `${base}/frame.html?${frame.search}`);
519
994
  yield page.addStyleTag({ content: NO_MOTION });
520
995
  yield page.evaluate(thumb_1.settled);
521
- const found = (yield page.evaluate(seeFrame(frame.interaction === null)));
996
+ const found = (yield page.evaluate(seeFrame({ discover: frame.interaction === null, part, ownCss, clip: frame.clip })));
522
997
  if ('refused' in found) {
523
998
  refused.push(`${frame.state}: ${found.refused}`);
524
999
  continue;
525
1000
  }
526
- const controls = [];
527
- for (const control of found.controls) {
1001
+ if (frame.interaction === null) {
1002
+ const box = frame.clip ? yield page.locator('#root').boundingBox() : null;
1003
+ shots.push({
1004
+ state: frame.state,
1005
+ png: yield page.screenshot({ type: 'png', fullPage: true, animations: 'disabled', clip: box !== null && box !== void 0 ? box : undefined }),
1006
+ });
1007
+ }
1008
+ const forcedOf = (element) => __awaiter(void 0, void 0, void 0, function* () {
528
1009
  const forced = [];
529
- for (const interaction of control.forced.filter((name) => FORCEABLE.has(name))) {
530
- if (!control.box) {
1010
+ for (const interaction of element.forced.filter((name) => FORCEABLE.has(name))) {
1011
+ if (!element.box) {
531
1012
  forced.push({ interaction, differs: false });
532
1013
  continue;
533
1014
  }
534
- const shot = () => { var _a; return page.screenshot({ type: 'png', fullPage: true, clip: (_a = control.box) !== null && _a !== void 0 ? _a : undefined, animations: 'disabled' }); };
1015
+ const shot = () => { var _a; return page.screenshot({ type: 'png', fullPage: true, clip: (_a = element.box) !== null && _a !== void 0 ? _a : undefined, animations: 'disabled' }); };
535
1016
  const drawn = yield shot();
536
- const drawnColours = (yield page.evaluate(colourSnapshot(control.at)));
537
- yield page.evaluate(forceOn({ at: control.at, interaction, on: false }));
1017
+ const drawnColours = (yield page.evaluate(colourSnapshot(element.at)));
1018
+ yield page.evaluate(forceOn({ at: element.at, interaction, on: false }));
538
1019
  yield page.evaluate(thumb_1.settled);
539
1020
  const rest = yield shot();
540
- const restColours = (yield page.evaluate(colourSnapshot(control.at)));
541
- yield page.evaluate(forceOn({ at: control.at, interaction, on: true }));
542
- forced.push({
543
- interaction,
544
- differs: !drawn.equals(rest),
545
- still: stillChangesOf(restColours, drawnColours),
546
- });
1021
+ const restColours = (yield page.evaluate(colourSnapshot(element.at)));
1022
+ yield page.evaluate(forceOn({ at: element.at, interaction, on: true }));
1023
+ const differs = !drawn.equals(rest);
1024
+ const undefinedClasses = differs
1025
+ ? []
1026
+ : (yield page.evaluate(undefinedVariantClasses(element.at))).filter((token) => isVariantOf(token, interaction));
1027
+ forced.push(Object.assign({ interaction,
1028
+ differs, still: stillChangesOf(restColours, drawnColours) }, (undefinedClasses.length > 0 ? { undefinedClasses } : {})));
547
1029
  }
548
- controls.push({ id: control.id, forced });
1030
+ return forced;
1031
+ });
1032
+ const controls = [];
1033
+ const surfaces = [];
1034
+ for (const control of found.controls)
1035
+ controls.push(Object.assign(Object.assign({ id: control.id, forced: yield forcedOf(control), selected: control.selected, disabled: control.disabled, look: control.look }, (control.keyboard ? { keyboard: true } : {})), { box: control.frameBox }));
1036
+ for (const surface of found.surfaces) {
1037
+ const options = [];
1038
+ for (const option of surface.options)
1039
+ options.push(Object.assign(Object.assign({ selected: option.selected, look: option.look, forced: yield forcedOf(option) }, (option.named ? { named: true } : {})), (option.keyboard ? { keyboard: true } : {})));
1040
+ surfaces.push({ id: surface.id, shown: surface.shown, options });
549
1041
  }
550
1042
  seen.push({
551
1043
  state: frame.state,
552
1044
  interaction: frame.interaction,
553
1045
  controls,
554
- surfaces: found.surfaces,
1046
+ surfaces,
555
1047
  found: found.found,
1048
+ overrides: found.overrides,
556
1049
  });
557
1050
  }
558
1051
  }
559
1052
  finally {
560
1053
  yield context.close();
561
1054
  }
562
- return { seen, refused };
1055
+ const [first, ...others] = shots;
1056
+ const shares = first ? yield (0, contact_sheet_1.changedSharesOf)({ browser, base: first.png, others: others.map(({ png }) => png) }) : [];
1057
+ return {
1058
+ seen,
1059
+ refused,
1060
+ changes: others.map(({ state }, index) => {
1061
+ var _a, _b;
1062
+ return ({
1063
+ state,
1064
+ base: (_a = first === null || first === void 0 ? void 0 : first.state) !== null && _a !== void 0 ? _a : '',
1065
+ share: (_b = shares[index]) !== null && _b !== void 0 ? _b : 0,
1066
+ });
1067
+ }),
1068
+ };
1069
+ });
1070
+ /**
1071
+ * Draws every state of one part of the product, at rest and, unless `restOnly`, in each interaction it declares,
1072
+ * once at its first preset or width, and answers what each frame showed, or why its module did not load as a string.
1073
+ */
1074
+ const seePart = (options) => __awaiter(void 0, void 0, void 0, function* () {
1075
+ var _a, _b, _c, _d, _e;
1076
+ const { product, part, system, base, load, draw, restOnly = false, sketch = null } = options;
1077
+ const key = (0, resolve_product_1.partKeyOf)(part);
1078
+ const loaded = yield load((0, system_modules_1.productModuleIdOf)({ file: part.file, product: product.name, sketch }))
1079
+ .then((module) => ({ module: module }))
1080
+ .catch((error) => ({ error: error instanceof Error ? error.message : String(error) }));
1081
+ if ('error' in loaded)
1082
+ return loaded.error;
1083
+ const definition = loaded.module.default;
1084
+ const states = isRecord(definition === null || definition === void 0 ? void 0 : definition.states) ? Object.keys(definition.states) : [];
1085
+ const interactions = Array.isArray(definition === null || definition === void 0 ? void 0 : definition.interactions)
1086
+ ? definition.interactions.filter((name) => force_interactions_1.INTERACTIONS.some((known) => known === name))
1087
+ : [];
1088
+ const widths = Array.isArray(definition === null || definition === void 0 ? void 0 : definition.widths)
1089
+ ? definition.widths.filter((width) => typeof width === 'number')
1090
+ : [];
1091
+ const presets = Object.keys((_b = (_a = system.drawnOf(product.name, null)) === null || _a === void 0 ? void 0 : _a.presets) !== null && _b !== void 0 ? _b : {});
1092
+ const preset = part.layer === 'pages' ? ((_c = presets[0]) !== null && _c !== void 0 ? _c : null) : null;
1093
+ const width = preset === null ? ((_d = widths[0]) !== null && _d !== void 0 ? _d : null) : null;
1094
+ const frames = [];
1095
+ for (const state of states)
1096
+ for (const interaction of [null, ...(restOnly ? [] : interactions)]) {
1097
+ const frame = (0, shot_1.partFrameOf)({
1098
+ system,
1099
+ product: product.name,
1100
+ part: key,
1101
+ state,
1102
+ preset,
1103
+ width,
1104
+ sketch,
1105
+ interaction,
1106
+ });
1107
+ if (!(frame instanceof Error))
1108
+ frames.push({
1109
+ state,
1110
+ interaction,
1111
+ search: frame.search.toString(),
1112
+ size: (_e = frame.size) !== null && _e !== void 0 ? _e : null,
1113
+ clip: preset === null,
1114
+ });
1115
+ }
1116
+ const dir = (0, path_1.dirname)(part.file);
1117
+ const ownCss = (0, fs_1.readdirSync)(dir)
1118
+ .filter((file) => file.endsWith('.css'))
1119
+ .sort()
1120
+ .map((file) => (0, fs_1.readFileSync)((0, path_1.join)(dir, file), 'utf8'));
1121
+ const { seen, refused, changes } = yield draw((browser) => seeFrames({ browser, base, frames, part: key, ownCss }));
1122
+ const owner = (0, inventory_approval_1.partOwnerOf)({ product, part });
1123
+ return {
1124
+ definition,
1125
+ states,
1126
+ seen,
1127
+ refused,
1128
+ changes,
1129
+ addresses: states.map((state) => ({ state, preset, width })),
1130
+ owner,
1131
+ hash: (0, baseline_1.partHashOf)({ product: owner, dir }),
1132
+ };
563
1133
  });
1134
+ exports.seePart = seePart;
1135
+ /** The control ids a page draws at rest, the set its inventory approval holds. */
1136
+ const inventoryIdsOf = (frames) => [
1137
+ ...new Set(frames.filter((frame) => frame.interaction === null).flatMap((frame) => frame.controls.map(({ id }) => id))),
1138
+ ].sort();
1139
+ exports.inventoryIdsOf = inventoryIdsOf;
564
1140
  /**
565
1141
  * Draws every state of one part, at rest and in each interaction it declares, once at its first preset or width,
566
- * and answers the gaps of its `controls` declaration, or why it could not draw. A part a control names in `part`
567
- * is checked the same way, and its gaps fail the control.
1142
+ * and answers the gaps of its `controls` declaration, or why it could not draw. A part a control names in `part`,
1143
+ * and an embedded part whose `data-et-part` root draws a control, is checked the same way, and its gaps fail it -
1144
+ * unless it is a part of the product's base, whose gaps stay its own.
1145
+ * A page fails until the user approves its control inventory in Studio. An exception the user approved in Studio
1146
+ * drops the gap it names until the part changes; a pending one still fails. With `sketch`, every part is drawn as
1147
+ * that variant draws it, and a part the variant changes is not merged yet, so it never fails as unreviewed.
1148
+ * A drawn control that no `controls` entry declares is a hint; the sketch check fails on one a sketch adds.
568
1149
  */
569
1150
  const completenessOf = (options) => __awaiter(void 0, void 0, void 0, function* () {
570
1151
  var _a, _b, _c, _d;
571
- const { target, config, product, part } = options;
1152
+ const { target, config, product, part, sketch = null } = options;
1153
+ const system = (0, system_modules_1.createSystemModules)({ target, config });
1154
+ const parts = (_b = (_a = (sketch ? system.drawnOf(product.name, sketch) : product)) === null || _a === void 0 ? void 0 : _a.parts) !== null && _b !== void 0 ? _b : [];
1155
+ if (!parts.some((candidate) => (0, resolve_product_1.partKeyOf)(candidate) === part))
1156
+ return {
1157
+ problem: sketch
1158
+ ? `variant "${sketch.variant}" of the sketch on ${sketch.target} in product "${product.name}" has no part "${part}"`
1159
+ : `product "${product.name}" has no part "${part}"`,
1160
+ };
1161
+ const checked = yield (0, exports.partsCompletenessOf)(Object.assign(Object.assign({}, options), { parts: [part] }));
1162
+ if ('unavailable' in checked)
1163
+ return checked;
1164
+ const gaps = (_c = checked.gaps.get(part)) !== null && _c !== void 0 ? _c : [];
1165
+ if (typeof gaps === 'string')
1166
+ return { problem: gaps };
1167
+ const hints = (_d = checked.hints.get(part)) !== null && _d !== void 0 ? _d : [];
1168
+ return Object.assign(Object.assign({ gaps }, (checked.exceptions.length > 0 ? { exceptions: checked.exceptions } : {})), (hints.length > 0 ? { hints } : {}));
1169
+ });
1170
+ exports.completenessOf = completenessOf;
1171
+ /**
1172
+ * Checks each named part as `completenessOf` does, sharing one server, browser and cache, and answers the gaps of
1173
+ * each part, or why it could not be checked as a string.
1174
+ */
1175
+ const partsCompletenessOf = (options) => __awaiter(void 0, void 0, void 0, function* () {
1176
+ var _a, _b, _c, _d;
1177
+ const { target, config, product, sketch = null } = options;
572
1178
  const resolveChromium = (_a = options.chromiumOf) !== null && _a !== void 0 ? _a : browser_1.chromiumOf;
573
- if (!product.parts.some((candidate) => (0, resolve_product_1.partKeyOf)(candidate) === part))
574
- return { problem: `product "${product.name}" has no part "${part}"` };
1179
+ const system = (0, system_modules_1.createSystemModules)({ target, config });
1180
+ const parts = (_c = (_b = (sketch ? system.drawnOf(product.name, sketch) : product)) === null || _b === void 0 ? void 0 : _b.parts) !== null && _c !== void 0 ? _c : [];
1181
+ const optionValuesOf = (0, option_values_1.optionValuesChecker)({ target, config, parts });
575
1182
  if (!(yield resolveChromium(target)))
576
1183
  return { unavailable: 'neither the checkout nor this package resolves playwright or playwright-core' };
577
1184
  const queue = (0, thumb_1.createDrawQueue)({ target, chromiumOf: resolveChromium });
578
1185
  const server = yield (0, serve_1.createDesignServer)({ target, config, listen: true, port: 0 });
579
1186
  try {
580
1187
  yield server.listen();
581
- const { port } = (_b = server.httpServer) === null || _b === void 0 ? void 0 : _b.address();
582
- const system = (0, system_modules_1.createSystemModules)({ target, config });
583
- const presets = Object.keys((_d = (_c = system.drawnOf(product.name, null)) === null || _c === void 0 ? void 0 : _c.presets) !== null && _d !== void 0 ? _d : {});
1188
+ const { port } = (_d = server.httpServer) === null || _d === void 0 ? void 0 : _d.address();
1189
+ const checked = new Map();
1190
+ const exceptions = [];
1191
+ const hints = new Map();
584
1192
  const gapsOfPart = (key, chain) => __awaiter(void 0, void 0, void 0, function* () {
585
- var _a, _b, _c, _d, _e;
586
- const resolved = product.parts.find((candidate) => (0, resolve_product_1.partKeyOf)(candidate) === key);
1193
+ const known = checked.get(key);
1194
+ if (known !== undefined && !chain.includes(key))
1195
+ return known;
1196
+ const gaps = yield freshGapsOfPart(key, chain);
1197
+ checked.set(key, gaps);
1198
+ return gaps;
1199
+ });
1200
+ const freshGapsOfPart = (key, chain) => __awaiter(void 0, void 0, void 0, function* () {
1201
+ var _a, _b, _c, _d;
1202
+ const resolved = parts.find((candidate) => (0, resolve_product_1.partKeyOf)(candidate) === key);
587
1203
  if (!resolved)
588
1204
  return `product "${product.name}" has no part "${key}"`;
589
1205
  if (chain.includes(key))
590
1206
  return `its controls name each other in a cycle: ${[...chain, key].join(' > ')}`;
591
- const loaded = yield server
592
- .ssrLoadModule((0, system_modules_1.productModuleIdOf)({ file: resolved.file, product: product.name, sketch: null }))
593
- .then((module) => ({ module: module }))
594
- .catch((error) => ({ error: error instanceof Error ? error.message : String(error) }));
595
- if ('error' in loaded)
596
- return [{ code: 'RENDER', target: key, detail: (_a = loaded.error.split('\n')[0]) !== null && _a !== void 0 ? _a : '' }];
597
- const definition = loaded.module.default;
598
- const states = isRecord(definition === null || definition === void 0 ? void 0 : definition.states) ? Object.keys(definition.states) : [];
599
- const interactions = Array.isArray(definition === null || definition === void 0 ? void 0 : definition.interactions)
600
- ? definition.interactions.filter((name) => force_interactions_1.INTERACTIONS.some((known) => known === name))
601
- : [];
602
- const widths = Array.isArray(definition === null || definition === void 0 ? void 0 : definition.widths)
603
- ? definition.widths.filter((width) => typeof width === 'number')
604
- : [];
605
- const preset = resolved.layer === 'pages' ? ((_b = presets[0]) !== null && _b !== void 0 ? _b : null) : null;
606
- const width = preset === null ? ((_c = widths[0]) !== null && _c !== void 0 ? _c : null) : null;
607
- const frames = [];
608
- for (const state of states)
609
- for (const interaction of [null, ...interactions]) {
610
- const frame = (0, shot_1.partFrameOf)({
611
- system,
612
- product: product.name,
613
- part: key,
614
- state,
615
- preset,
616
- width,
617
- sketch: null,
618
- interaction,
619
- });
620
- if (!(frame instanceof Error))
621
- frames.push({ state, interaction, search: frame.search.toString(), size: (_d = frame.size) !== null && _d !== void 0 ? _d : null });
622
- }
623
- const { seen, refused } = yield queue.run((browser) => seeFrames({ browser, base: `http://localhost:${port}`, frames }));
624
- const reviewed = (0, merged_parts_1.readMerged)(product.dir)[key] === (0, baseline_1.partHashOf)({ product, dir: (0, path_1.dirname)(resolved.file) });
1207
+ const sight = yield (0, exports.seePart)({
1208
+ product,
1209
+ part: resolved,
1210
+ system,
1211
+ base: `http://localhost:${port}`,
1212
+ load: (id) => server.ssrLoadModule(id),
1213
+ draw: (job) => queue.run(job),
1214
+ sketch,
1215
+ });
1216
+ if (typeof sight === 'string')
1217
+ return [{ code: 'RENDER', target: key, detail: (_a = sight.split('\n')[0]) !== null && _a !== void 0 ? _a : '' }];
1218
+ const { definition, states, seen, refused, changes, owner, hash } = sight;
1219
+ const optionProblems = optionValuesOf(resolved, definition !== null && definition !== void 0 ? definition : {});
1220
+ hints.set(key, [
1221
+ ...(0, exports.smallChangeHintsOf)({ part: key, controls: definition === null || definition === void 0 ? void 0 : definition.controls, changes }),
1222
+ ...(0, exports.controlStateGapsOf)({ part: key, controls: definition === null || definition === void 0 ? void 0 : definition.controls, states }).map(exports.completenessGapLineOf),
1223
+ ...discoveryGapsOf({ part: key, declared: (_b = (0, exports.declaredControlsOf)(definition === null || definition === void 0 ? void 0 : definition.controls)) !== null && _b !== void 0 ? _b : {}, frames: seen })
1224
+ .filter(isUndeclaredControlGap)
1225
+ .map(exports.completenessGapLineOf),
1226
+ ...optionProblems
1227
+ .filter((gap) => gap.code === 'OPTION-VALUE-MISSING')
1228
+ .map((gap) => (0, exports.completenessGapLineOf)(Object.assign(Object.assign({}, gap), { target: key }))),
1229
+ ]);
1230
+ const sketched = ((_c = product.parts.find((candidate) => (0, resolve_product_1.partKeyOf)(candidate) === key)) === null || _c === void 0 ? void 0 : _c.file) !== resolved.file;
1231
+ const reviewed = sketched || (0, merged_parts_1.readMerged)(owner.dir)[key] === hash;
625
1232
  const referenced = {};
626
- for (const control of Object.values((_e = declaredControlsOf(definition === null || definition === void 0 ? void 0 : definition.controls)) !== null && _e !== void 0 ? _e : {}))
627
- if (control.part !== null && !(control.part in referenced))
628
- referenced[control.part] = yield gapsOfPart(control.part, [...chain, key]);
629
- return [
630
- ...refused.map((detail) => ({ code: 'RENDER', target: key, detail })),
631
- ...(0, exports.completenessGapsOf)({
1233
+ const used = [
1234
+ ...Object.values((_d = (0, exports.declaredControlsOf)(definition === null || definition === void 0 ? void 0 : definition.controls)) !== null && _d !== void 0 ? _d : {}).map((control) => control.part),
1235
+ ...ownersOf(seen),
1236
+ ];
1237
+ const isKitPart = (other) => { var _a; return resolved.origin !== 'base' && ((_a = parts.find((candidate) => (0, resolve_product_1.partKeyOf)(candidate) === other)) === null || _a === void 0 ? void 0 : _a.origin) === 'base'; };
1238
+ for (const other of used)
1239
+ if (other !== null && !(other in referenced))
1240
+ referenced[other] = isKitPart(other) ? [] : yield gapsOfPart(other, [...chain, key]);
1241
+ const excepted = (0, exceptions_1.withExceptions)({
1242
+ part: key,
1243
+ exceptions: (0, exceptions_1.readExceptions)(owner.dir),
1244
+ hash,
1245
+ gaps: (0, exports.completenessGapsOf)({
632
1246
  part: key,
633
1247
  controls: definition === null || definition === void 0 ? void 0 : definition.controls,
634
1248
  states,
@@ -636,11 +1250,22 @@ const completenessOf = (options) => __awaiter(void 0, void 0, void 0, function*
636
1250
  reviewed,
637
1251
  grid: definition === null || definition === void 0 ? void 0 : definition.grid,
638
1252
  referenced,
1253
+ inventory: resolved.layer === 'pages'
1254
+ ? (0, inventory_approval_1.inventoryStateOf)({ approval: (0, inventory_approval_1.readInventory)(owner.dir)[key], ids: (0, exports.inventoryIdsOf)(seen), hash })
1255
+ : undefined,
639
1256
  }),
1257
+ });
1258
+ exceptions.push(...excepted.listed);
1259
+ return [
1260
+ ...refused.map((detail) => ({ code: 'RENDER', target: key, detail })),
1261
+ ...optionProblems.filter((gap) => gap.code !== 'OPTION-VALUE-MISSING').map((gap) => (Object.assign(Object.assign({}, gap), { target: key }))),
1262
+ ...excepted.gaps,
640
1263
  ];
641
1264
  });
642
- const gaps = yield gapsOfPart(part, []);
643
- return typeof gaps === 'string' ? { problem: gaps } : { gaps };
1265
+ const gaps = new Map();
1266
+ for (const part of options.parts)
1267
+ gaps.set(part, yield gapsOfPart(part, []));
1268
+ return { gaps, exceptions, hints };
644
1269
  }
645
1270
  catch (error) {
646
1271
  if (error instanceof thumb_1.ThumbUnavailableError)
@@ -651,5 +1276,5 @@ const completenessOf = (options) => __awaiter(void 0, void 0, void 0, function*
651
1276
  yield Promise.all([queue.close(), server.close()]);
652
1277
  }
653
1278
  });
654
- exports.completenessOf = completenessOf;
1279
+ exports.partsCompletenessOf = partsCompletenessOf;
655
1280
  //# sourceMappingURL=complete.js.map