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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/design/define-call.ts +29 -3
  3. package/design/define-part.ts +15 -1
  4. package/design/logo-sheet.ts +52 -0
  5. package/design/motion-drive.ts +215 -0
  6. package/design/motion-runtime.ts +14 -2
  7. package/design/motion-tokens.ts +177 -0
  8. package/design/web/frame-fill.ts +4 -0
  9. package/design/web/frame-scheme.ts +44 -0
  10. package/design/web/frame.ts +122 -29
  11. package/package.json +1 -1
  12. package/src/lib/design/calls-tsconfig.d.ts +2 -0
  13. package/src/lib/design/calls-tsconfig.js +10 -3
  14. package/src/lib/design/calls-tsconfig.js.map +1 -1
  15. package/src/lib/design/check.d.ts +1 -0
  16. package/src/lib/design/check.js +62 -6
  17. package/src/lib/design/check.js.map +1 -1
  18. package/src/lib/design/contact-sheet.d.ts +9 -0
  19. package/src/lib/design/contact-sheet.js +38 -4
  20. package/src/lib/design/contact-sheet.js.map +1 -1
  21. package/src/lib/design/design-command.js +27 -8
  22. package/src/lib/design/design-command.js.map +1 -1
  23. package/src/lib/design/draw-ledger.d.ts +36 -0
  24. package/src/lib/design/draw-ledger.js +52 -0
  25. package/src/lib/design/draw-ledger.js.map +1 -0
  26. package/src/lib/design/mcp-command.d.ts +23 -2
  27. package/src/lib/design/mcp-command.js +105 -10
  28. package/src/lib/design/mcp-command.js.map +1 -1
  29. package/src/lib/design/motion-command.js +9 -15
  30. package/src/lib/design/motion-command.js.map +1 -1
  31. package/src/lib/design/paths.d.ts +14 -0
  32. package/src/lib/design/paths.js +50 -1
  33. package/src/lib/design/paths.js.map +1 -1
  34. package/src/lib/design/render-packages.d.ts +5 -0
  35. package/src/lib/design/render-packages.js +6 -1
  36. package/src/lib/design/render-packages.js.map +1 -1
  37. package/src/lib/design/render-worker.js +1 -0
  38. package/src/lib/design/render-worker.js.map +1 -1
  39. package/src/lib/design/serve.d.ts +2 -2
  40. package/src/lib/design/serve.js +158 -19
  41. package/src/lib/design/serve.js.map +1 -1
  42. package/src/lib/design/shot-command.d.ts +5 -2
  43. package/src/lib/design/shot-command.js +18 -16
  44. package/src/lib/design/shot-command.js.map +1 -1
  45. package/src/lib/design/shot-store.d.ts +24 -0
  46. package/src/lib/design/shot-store.js +120 -0
  47. package/src/lib/design/shot-store.js.map +1 -0
  48. package/src/lib/design/shot.d.ts +21 -1
  49. package/src/lib/design/shot.js +118 -31
  50. package/src/lib/design/shot.js.map +1 -1
  51. package/src/lib/design/sketch-command.js +121 -37
  52. package/src/lib/design/sketch-command.js.map +1 -1
  53. package/src/lib/design/ssr-shims.d.ts +2 -0
  54. package/src/lib/design/ssr-shims.js +12 -1
  55. package/src/lib/design/ssr-shims.js.map +1 -1
  56. package/src/lib/design/studio-api.d.ts +12 -0
  57. package/src/lib/design/studio-api.js +68 -2
  58. package/src/lib/design/studio-api.js.map +1 -1
  59. package/src/lib/design/system/check-system.d.ts +6 -2
  60. package/src/lib/design/system/check-system.js +102 -20
  61. package/src/lib/design/system/check-system.js.map +1 -1
  62. package/src/lib/design/system/colour.d.ts +4 -1
  63. package/src/lib/design/system/colour.js +54 -2
  64. package/src/lib/design/system/colour.js.map +1 -1
  65. package/src/lib/design/system/complete-command.js +6 -2
  66. package/src/lib/design/system/complete-command.js.map +1 -1
  67. package/src/lib/design/system/complete.d.ts +67 -2
  68. package/src/lib/design/system/complete.js +227 -53
  69. package/src/lib/design/system/complete.js.map +1 -1
  70. package/src/lib/design/system/contrast-check.d.ts +6 -1
  71. package/src/lib/design/system/contrast-check.js +167 -12
  72. package/src/lib/design/system/contrast-check.js.map +1 -1
  73. package/src/lib/design/system/drawn-open-points.d.ts +16 -0
  74. package/src/lib/design/system/drawn-open-points.js +43 -0
  75. package/src/lib/design/system/drawn-open-points.js.map +1 -0
  76. package/src/lib/design/system/list-system.d.ts +4 -1
  77. package/src/lib/design/system/list-system.js +2 -1
  78. package/src/lib/design/system/list-system.js.map +1 -1
  79. package/src/lib/design/system/open-points.d.ts +101 -0
  80. package/src/lib/design/system/open-points.js +156 -0
  81. package/src/lib/design/system/open-points.js.map +1 -0
  82. package/src/lib/design/system/part-ready.d.ts +52 -0
  83. package/src/lib/design/system/part-ready.js +101 -0
  84. package/src/lib/design/system/part-ready.js.map +1 -0
  85. package/src/lib/design/system/read-open-points.d.ts +27 -0
  86. package/src/lib/design/system/read-open-points.js +81 -0
  87. package/src/lib/design/system/read-open-points.js.map +1 -0
  88. package/src/lib/design/system/ready-command.d.ts +7 -0
  89. package/src/lib/design/system/ready-command.js +65 -0
  90. package/src/lib/design/system/ready-command.js.map +1 -0
  91. package/src/lib/design/system/render-check.d.ts +2 -0
  92. package/src/lib/design/system/render-check.js +197 -31
  93. package/src/lib/design/system/render-check.js.map +1 -1
  94. package/src/lib/design/system/resolve-product.d.ts +2 -1
  95. package/src/lib/design/system/resolve-product.js +20 -3
  96. package/src/lib/design/system/resolve-product.js.map +1 -1
  97. package/src/lib/design/system/rules/implements.d.ts +7 -0
  98. package/src/lib/design/system/rules/implements.js +43 -0
  99. package/src/lib/design/system/rules/implements.js.map +1 -0
  100. package/src/lib/design/system/rules/motion.d.ts +2 -0
  101. package/src/lib/design/system/rules/motion.js +26 -1
  102. package/src/lib/design/system/rules/motion.js.map +1 -1
  103. package/src/lib/design/system/rules/option-values.d.ts +15 -0
  104. package/src/lib/design/system/rules/option-values.js +145 -0
  105. package/src/lib/design/system/rules/option-values.js.map +1 -0
  106. package/src/lib/design/system/rules/stale-replace.js +41 -3
  107. package/src/lib/design/system/rules/stale-replace.js.map +1 -1
  108. package/src/lib/design/system/sketch/baseline.d.ts +11 -2
  109. package/src/lib/design/system/sketch/baseline.js +4 -2
  110. package/src/lib/design/system/sketch/baseline.js.map +1 -1
  111. package/src/lib/design/system/sketch/check-sketches.d.ts +1 -0
  112. package/src/lib/design/system/sketch/check-sketches.js +4 -2
  113. package/src/lib/design/system/sketch/check-sketches.js.map +1 -1
  114. package/src/lib/design/system/sketch/merge.js +32 -0
  115. package/src/lib/design/system/sketch/merge.js.map +1 -1
  116. package/src/lib/design/system/sketch/merged-parts.d.ts +1 -1
  117. package/src/lib/design/system/sketch/merged-parts.js +1 -1
  118. package/src/lib/design/system/sketch/merged-parts.js.map +1 -1
  119. package/src/lib/design/system/sketch/part-definition.d.ts +36 -0
  120. package/src/lib/design/system/sketch/part-definition.js +56 -2
  121. package/src/lib/design/system/sketch/part-definition.js.map +1 -1
  122. package/src/lib/design/system/sketch/sketch.d.ts +15 -0
  123. package/src/lib/design/system/sketch/sketch.js +65 -27
  124. package/src/lib/design/system/sketch/sketch.js.map +1 -1
  125. package/src/lib/design/system/sketch/unchanged-states.d.ts +4 -1
  126. package/src/lib/design/system/sketch/unchanged-states.js +9 -5
  127. package/src/lib/design/system/sketch/unchanged-states.js.map +1 -1
  128. package/src/lib/design/system/sketch/unused-closure.js +12 -2
  129. package/src/lib/design/system/sketch/unused-closure.js.map +1 -1
  130. package/src/lib/design/system/sketch/unused-files.d.ts +1 -1
  131. package/src/lib/design/system/sketch/unused-files.js +6 -3
  132. package/src/lib/design/system/sketch/unused-files.js.map +1 -1
  133. package/src/lib/design/system/system-tsconfig.js +2 -2
  134. package/src/lib/design/system/system-tsconfig.js.map +1 -1
  135. package/src/lib/design/system/ts-source.d.ts +3 -0
  136. package/src/lib/design/system/ts-source.js +21 -4
  137. package/src/lib/design/system/ts-source.js.map +1 -1
  138. package/src/lib/design/thumb.d.ts +31 -4
  139. package/src/lib/design/thumb.js +105 -40
  140. package/src/lib/design/thumb.js.map +1 -1
@@ -9,7 +9,7 @@ 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.inventoryIdsOf = exports.seePart = exports.completenessGapLineOf = exports.completenessGapsOf = exports.findingKeyOf = exports.isFindingOf = exports.declaredControlsOf = 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
13
  const fs_1 = require("fs");
14
14
  const path_1 = require("path");
15
15
  const browser_1 = require("../browser");
@@ -18,16 +18,18 @@ const serve_1 = require("../serve");
18
18
  const shot_1 = require("../shot");
19
19
  const thumb_1 = require("../thumb");
20
20
  const resolve_product_1 = require("./resolve-product");
21
+ const contact_sheet_1 = require("../contact-sheet");
21
22
  const baseline_1 = require("./sketch/baseline");
22
23
  const merged_parts_1 = require("./sketch/merged-parts");
23
24
  const system_modules_1 = require("./system-modules");
24
25
  const exceptions_1 = require("./exceptions");
25
26
  const inventory_approval_1 = require("./inventory-approval");
27
+ const option_values_1 = require("./rules/option-values");
26
28
  const KINDS = {
27
29
  disclosure: { surface: 'body', closed: 'collapsed' },
28
30
  button: { adds: ['active'] },
29
- select: { surface: 'open', options: true },
30
- menu: { surface: 'open', options: true },
31
+ select: { surface: 'open', options: { interactions: ['hover', 'focus-visible'], selected: true } },
32
+ menu: { surface: 'open', options: { interactions: ['hover', 'focus-visible', 'active'], selected: false } },
31
33
  row: { selects: true, virtualFocus: true },
32
34
  field: { noActive: true },
33
35
  tooltip: { surface: 'tooltip' },
@@ -88,6 +90,74 @@ const declaredControlsOf = (value) => isRecord(value)
88
90
  }))
89
91
  : null;
90
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;
91
161
  /** Whether a discovered element is an undeclared or unclassified finding of the part, not owned or declared. */
92
162
  const isFindingOf = (declared) => (found) => {
93
163
  var _a;
@@ -98,6 +168,7 @@ const isFindingOf = (declared) => (found) => {
98
168
  exports.isFindingOf = isFindingOf;
99
169
  const findingKeyOf = (found) => `${found.kind} ${found.what} ${found.path}`;
100
170
  exports.findingKeyOf = findingKeyOf;
171
+ const NATIVE_CONTROLS = ['button', 'a[href]', 'input:not([type="hidden"])', 'select', 'textarea', 'summary'];
101
172
  const discoveryGapsOf = (options) => {
102
173
  var _a, _b, _c, _d;
103
174
  const { part, declared, frames } = options;
@@ -125,7 +196,9 @@ const discoveryGapsOf = (options) => {
125
196
  ? {
126
197
  code: 'CONTROL-UNDECLARED',
127
198
  target: part,
128
- 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'}`,
129
202
  }
130
203
  : {
131
204
  code: 'CONTROL-UNCLASSIFIED',
@@ -134,6 +207,7 @@ const discoveryGapsOf = (options) => {
134
207
  };
135
208
  });
136
209
  };
210
+ const isUndeclaredControlGap = (gap) => gap.code === 'CONTROL-UNDECLARED';
137
211
  const statesShownOf = (states) => states.length > 4 ? `${states.slice(0, 3).join(', ')} and ${states.length - 3} more` : states.join(', ');
138
212
  const overrideGapsOf = (options) => {
139
213
  var _a, _b;
@@ -190,17 +264,26 @@ const ownersOf = (frames) => [
190
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; }); })
191
265
  .filter((owner) => owner !== null)),
192
266
  ];
193
- const interactionGapOf = (options) => {
267
+ const undefinedClassesGapOf = (options) => {
194
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;
195
278
  const { frames, id, interaction } = options;
196
279
  const forced = frames.flatMap((frame) => frame.controls
197
280
  .filter((control) => control.id === id)
198
281
  .flatMap((control) => control.forced.filter((force) => force.interaction === interaction))
199
- .map((force) => ({ state: frame.state, differs: force.differs })));
282
+ .map((force) => ({ state: frame.state, differs: force.differs, undefinedClasses: force.undefinedClasses })));
200
283
  if (forced.some((force) => force.differs))
201
284
  return null;
202
285
  if (forced.length > 0)
203
- 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 - if a descendant draws ${interaction}, mark it with data-et-control-part`;
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`);
204
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`;
205
288
  };
206
289
  const keyboardActiveRowGapOf = (options) => {
@@ -237,12 +320,12 @@ const lookGapOf = (options) => {
237
320
  return null;
238
321
  };
239
322
  const forcedGapOf = (options) => {
240
- var _a;
323
+ var _a, _b;
241
324
  const { forced, what, interaction } = options;
242
325
  if (forced.some((force) => force.differs))
243
326
  return null;
244
327
  if (forced.length > 0)
245
- return `state ${(_a = forced[0]) === null || _a === void 0 ? void 0 : _a.state} forces ${interaction} on ${what}, but its box draws no different pixel than at rest`;
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`);
246
329
  return `no state draws ${what} ${interaction} - force ${interaction} on it, e.g. with forceInteraction`;
247
330
  };
248
331
  const surfaceGapsOf = (options) => {
@@ -285,19 +368,21 @@ const keyboardActiveGapOf = (options) => {
285
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`;
286
369
  };
287
370
  const optionGapsOf = (options) => {
288
- const { frames, surfaceId } = options;
371
+ const { frames, surfaceId, interactions } = options;
289
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 : []; });
290
373
  const what = `an option in ${surfaceId}`;
291
374
  const gaps = [];
292
- for (const interaction of ['hover', 'focus-visible']) {
375
+ for (const interaction of interactions) {
293
376
  const forced = frames.flatMap((frame) => shownIn(frame).flatMap((option) => option.forced
294
377
  .filter((force) => force.interaction === interaction)
295
- .map((force) => ({ state: frame.state, differs: force.differs }))));
378
+ .map((force) => ({ state: frame.state, differs: force.differs, undefinedClasses: force.undefinedClasses }))));
296
379
  const gap = forcedGapOf({ forced, what, interaction });
297
380
  const missing = gap && interaction === 'focus-visible' ? keyboardActiveGapOf({ frames, shownIn, forcedGap: gap }) : gap;
298
381
  if (missing)
299
382
  gaps.push([`option-${interaction}`, missing]);
300
383
  }
384
+ if (!options.selected)
385
+ return gaps;
301
386
  const seen = frames
302
387
  .filter((frame) => frame.interaction === null)
303
388
  .flatMap((frame) => shownIn(frame)
@@ -334,7 +419,7 @@ const requirementGapsOf = (options) => {
334
419
  const forced = frames.flatMap((frame) => frame.controls
335
420
  .filter((seen) => seen.id === id && seen.selected)
336
421
  .flatMap((seen) => seen.forced.filter((force) => force.interaction === 'hover'))
337
- .map((force) => ({ state: frame.state, differs: force.differs })));
422
+ .map((force) => ({ state: frame.state, differs: force.differs, undefinedClasses: force.undefinedClasses })));
338
423
  const hovered = forcedGapOf({ forced, what: 'it selected', interaction: 'hover' });
339
424
  if (selected)
340
425
  gaps.push(['selected', selected]);
@@ -354,7 +439,7 @@ const requirementGapsOf = (options) => {
354
439
  if (kind.surface)
355
440
  gaps.push(...surfaceGapsOf(Object.assign(Object.assign({}, options), { surface: kind.surface, closed: kind.closed })));
356
441
  if (own && kind.options && kind.surface)
357
- gaps.push(...optionGapsOf({ frames, surfaceId: `${id}.${kind.surface}` }));
442
+ gaps.push(...optionGapsOf(Object.assign({ frames, surfaceId: `${id}.${kind.surface}` }, kind.options)));
358
443
  return gaps;
359
444
  };
360
445
  /**
@@ -431,7 +516,7 @@ const completenessGapsOf = (options) => {
431
516
  }
432
517
  return [
433
518
  ...gaps,
434
- ...discoveryGapsOf({ part, declared, frames }),
519
+ ...discoveryGapsOf({ part, declared, frames }).filter((gap) => !isUndeclaredControlGap(gap)),
435
520
  ...ownerGapsOf({ part, declared, frames, referenced }),
436
521
  ...overrideGapsOf({ part, frames }),
437
522
  ...inventoryGapsOf({ part, inventory }),
@@ -446,6 +531,10 @@ const inventoryGapsOf = ({ part, inventory }) => {
446
531
  };
447
532
  const completenessGapLineOf = (gap) => `${gap.code} ${gap.target} - ${gap.detail}`;
448
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;
449
538
  const NO_MOTION = '*, *::before, *::after { transition: none !important; animation: none !important; }';
450
539
  const PAD = 8;
451
540
  const COLOUR_PROPERTIES = [
@@ -489,6 +578,28 @@ const coversOf = (property) => {
489
578
  return ['all', 'border', 'border-color', property.replace(/-color$/, ''), property];
490
579
  return ['all', property];
491
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); });
492
603
  const stillChangesOf = (rest, forced) => forced.names.flatMap((name, element) => {
493
604
  var _a;
494
605
  const { properties, durations } = (_a = forced.transitions[element]) !== null && _a !== void 0 ? _a : { properties: [], durations: [] };
@@ -526,7 +637,7 @@ const INTERACTIVE_ROLES = [
526
637
  'textbox',
527
638
  'treeitem',
528
639
  ];
529
- const SDK_CONTROLS = [
640
+ exports.SDK_CONTROLS = [
530
641
  '[et-button]',
531
642
  '[et-icon-button]',
532
643
  '[et-text-button]',
@@ -566,23 +677,22 @@ const SDK_CONTROLS = [
566
677
  'et-time-input',
567
678
  'et-toggletip',
568
679
  ];
569
- const LIKELY_CONTROLS = [
570
- ...SDK_CONTROLS,
571
- 'button',
572
- 'a[href]',
573
- 'input:not([type="hidden"])',
574
- 'select',
575
- 'textarea',
576
- 'summary',
577
- '[tabindex]',
578
- ...INTERACTIVE_ROLES.map((role) => `[role="${role}"]`),
579
- ];
680
+ const PLAIN_CONTROLS = [...NATIVE_CONTROLS, '[tabindex]', ...INTERACTIVE_ROLES.map((role) => `[role="${role}"]`)];
681
+ const LIKELY_CONTROLS = [...exports.SDK_CONTROLS, ...PLAIN_CONTROLS];
580
682
  const LOOK_PROPERTIES = [...COLOUR_PROPERTIES, 'opacity', 'box-shadow', 'font-weight', 'text-decoration-line'];
581
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})`;
582
690
  const discoverControls = `((root) => {
583
691
  const LIKELY = ${JSON.stringify(LIKELY_CONTROLS)};
584
692
  const likely = LIKELY.join(', ');
585
- 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(', '))};
586
696
  const rendered = (element) => element.getClientRects().length > 0;
587
697
  const HOVER = /(?<!\\\\):hover(?![\\w-])/g;
588
698
  const FORCED_HOVER = /:is\\(:hover,\\s*\\[data-et-force~="?hover"?\\]\\)/g;
@@ -640,7 +750,7 @@ const discoverControls = `((root) => {
640
750
  const markedOf = (element) => {
641
751
  if (element.hasAttribute('data-et-control')) return element.getAttribute('data-et-control');
642
752
  for (let at = element.parentElement; at && at !== root; at = at.parentElement) {
643
- if (at.matches(likely)) return null;
753
+ if (isLikely(at)) return null;
644
754
  const id = markerOf(at, element);
645
755
  if (id !== null) return id;
646
756
  }
@@ -649,7 +759,7 @@ const discoverControls = `((root) => {
649
759
  const found = [];
650
760
  for (const element of root.querySelectorAll('*')) {
651
761
  if (!rendered(element)) continue;
652
- const what = LIKELY.find((selector) => element.matches(selector));
762
+ const what = !element.matches(container) && LIKELY.find((selector) => element.matches(selector));
653
763
  if (what) {
654
764
  const inSdk = element.parentElement.closest(sdk);
655
765
  if (inSdk !== null && root.contains(inSdk)) continue;
@@ -659,7 +769,9 @@ const discoverControls = `((root) => {
659
769
  const cursor = getComputedStyle(element).cursor;
660
770
  const pointer = cursor === 'pointer' && getComputedStyle(element.parentElement).cursor !== 'pointer';
661
771
  if (!pointer && !hovered.has(element)) continue;
662
- 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;
663
775
  const reason = [pointer ? 'cursor: pointer' : '', hovered.has(element) ? ':hover rule' : ''].filter(Boolean).join(' and ');
664
776
  found.push({ kind: 'unclassified', what: nameOf(element) + ' with ' + reason, path: pathOf(element), marked: null, within: [], owner: ownerOf(element)?.key ?? null, box: frameBoxOf(element) });
665
777
  }
@@ -819,7 +931,7 @@ const seeFrame = (options) => `(() => {
819
931
  const isKeyboard = (element) => element.getAttribute('data-active-source') === 'keyboard';
820
932
  let next = 0;
821
933
  const tagged = (element) => {
822
- const at = next++;
934
+ const at = element.hasAttribute('data-et-complete') ? Number(element.getAttribute('data-et-complete')) : next++;
823
935
  element.setAttribute('data-et-complete', String(at));
824
936
  const box = element.getBoundingClientRect();
825
937
  const x = Math.max(0, box.x + scrollX - ${PAD});
@@ -873,6 +985,7 @@ const seeFrames = (options) => __awaiter(void 0, void 0, void 0, function* () {
873
985
  const context = yield browser.newContext();
874
986
  const seen = [];
875
987
  const refused = [];
988
+ const shots = [];
876
989
  try {
877
990
  const page = yield context.newPage();
878
991
  for (const frame of frames) {
@@ -885,6 +998,13 @@ const seeFrames = (options) => __awaiter(void 0, void 0, void 0, function* () {
885
998
  refused.push(`${frame.state}: ${found.refused}`);
886
999
  continue;
887
1000
  }
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
+ }
888
1008
  const forcedOf = (element) => __awaiter(void 0, void 0, void 0, function* () {
889
1009
  const forced = [];
890
1010
  for (const interaction of element.forced.filter((name) => FORCEABLE.has(name))) {
@@ -900,11 +1020,12 @@ const seeFrames = (options) => __awaiter(void 0, void 0, void 0, function* () {
900
1020
  const rest = yield shot();
901
1021
  const restColours = (yield page.evaluate(colourSnapshot(element.at)));
902
1022
  yield page.evaluate(forceOn({ at: element.at, interaction, on: true }));
903
- forced.push({
904
- interaction,
905
- differs: !drawn.equals(rest),
906
- still: stillChangesOf(restColours, drawnColours),
907
- });
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 } : {})));
908
1029
  }
909
1030
  return forced;
910
1031
  });
@@ -931,7 +1052,20 @@ const seeFrames = (options) => __awaiter(void 0, void 0, void 0, function* () {
931
1052
  finally {
932
1053
  yield context.close();
933
1054
  }
934
- 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
+ };
935
1069
  });
936
1070
  /**
937
1071
  * Draws every state of one part of the product, at rest and, unless `restOnly`, in each interaction it declares,
@@ -984,13 +1118,14 @@ const seePart = (options) => __awaiter(void 0, void 0, void 0, function* () {
984
1118
  .filter((file) => file.endsWith('.css'))
985
1119
  .sort()
986
1120
  .map((file) => (0, fs_1.readFileSync)((0, path_1.join)(dir, file), 'utf8'));
987
- const { seen, refused } = yield draw((browser) => seeFrames({ browser, base, frames, part: key, ownCss }));
1121
+ const { seen, refused, changes } = yield draw((browser) => seeFrames({ browser, base, frames, part: key, ownCss }));
988
1122
  const owner = (0, inventory_approval_1.partOwnerOf)({ product, part });
989
1123
  return {
990
1124
  definition,
991
1125
  states,
992
1126
  seen,
993
1127
  refused,
1128
+ changes,
994
1129
  addresses: states.map((state) => ({ state, preset, width })),
995
1130
  owner,
996
1131
  hash: (0, baseline_1.partHashOf)({ product: owner, dir }),
@@ -1005,23 +1140,45 @@ exports.inventoryIdsOf = inventoryIdsOf;
1005
1140
  /**
1006
1141
  * Draws every state of one part, at rest and in each interaction it declares, once at its first preset or width,
1007
1142
  * and answers the gaps of its `controls` declaration, or why it could not draw. A part a control names in `part`,
1008
- * and an embedded part whose `data-et-part` root draws a control, is checked the same way, and its gaps fail it.
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.
1009
1145
  * A page fails until the user approves its control inventory in Studio. An exception the user approved in Studio
1010
1146
  * drops the gap it names until the part changes; a pending one still fails. With `sketch`, every part is drawn as
1011
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.
1012
1149
  */
1013
1150
  const completenessOf = (options) => __awaiter(void 0, void 0, void 0, function* () {
1014
1151
  var _a, _b, _c, _d;
1015
1152
  const { target, config, product, part, sketch = null } = options;
1016
- const resolveChromium = (_a = options.chromiumOf) !== null && _a !== void 0 ? _a : browser_1.chromiumOf;
1017
1153
  const system = (0, system_modules_1.createSystemModules)({ target, config });
1018
- const parts = (_c = (_b = (sketch ? system.drawnOf(product.name, sketch) : product)) === null || _b === void 0 ? void 0 : _b.parts) !== null && _c !== void 0 ? _c : [];
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 : [];
1019
1155
  if (!parts.some((candidate) => (0, resolve_product_1.partKeyOf)(candidate) === part))
1020
1156
  return {
1021
1157
  problem: sketch
1022
1158
  ? `variant "${sketch.variant}" of the sketch on ${sketch.target} in product "${product.name}" has no part "${part}"`
1023
1159
  : `product "${product.name}" has no part "${part}"`,
1024
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;
1178
+ const resolveChromium = (_a = options.chromiumOf) !== null && _a !== void 0 ? _a : browser_1.chromiumOf;
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 });
1025
1182
  if (!(yield resolveChromium(target)))
1026
1183
  return { unavailable: 'neither the checkout nor this package resolves playwright or playwright-core' };
1027
1184
  const queue = (0, thumb_1.createDrawQueue)({ target, chromiumOf: resolveChromium });
@@ -1031,6 +1188,7 @@ const completenessOf = (options) => __awaiter(void 0, void 0, void 0, function*
1031
1188
  const { port } = (_d = server.httpServer) === null || _d === void 0 ? void 0 : _d.address();
1032
1189
  const checked = new Map();
1033
1190
  const exceptions = [];
1191
+ const hints = new Map();
1034
1192
  const gapsOfPart = (key, chain) => __awaiter(void 0, void 0, void 0, function* () {
1035
1193
  const known = checked.get(key);
1036
1194
  if (known !== undefined && !chain.includes(key))
@@ -1040,7 +1198,7 @@ const completenessOf = (options) => __awaiter(void 0, void 0, void 0, function*
1040
1198
  return gaps;
1041
1199
  });
1042
1200
  const freshGapsOfPart = (key, chain) => __awaiter(void 0, void 0, void 0, function* () {
1043
- var _a, _b, _c;
1201
+ var _a, _b, _c, _d;
1044
1202
  const resolved = parts.find((candidate) => (0, resolve_product_1.partKeyOf)(candidate) === key);
1045
1203
  if (!resolved)
1046
1204
  return `product "${product.name}" has no part "${key}"`;
@@ -1057,17 +1215,29 @@ const completenessOf = (options) => __awaiter(void 0, void 0, void 0, function*
1057
1215
  });
1058
1216
  if (typeof sight === 'string')
1059
1217
  return [{ code: 'RENDER', target: key, detail: (_a = sight.split('\n')[0]) !== null && _a !== void 0 ? _a : '' }];
1060
- const { definition, states, seen, refused, owner, hash } = sight;
1061
- const sketched = ((_b = product.parts.find((candidate) => (0, resolve_product_1.partKeyOf)(candidate) === key)) === null || _b === void 0 ? void 0 : _b.file) !== resolved.file;
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;
1062
1231
  const reviewed = sketched || (0, merged_parts_1.readMerged)(owner.dir)[key] === hash;
1063
1232
  const referenced = {};
1064
1233
  const used = [
1065
- ...Object.values((_c = (0, exports.declaredControlsOf)(definition === null || definition === void 0 ? void 0 : definition.controls)) !== null && _c !== void 0 ? _c : {}).map((control) => control.part),
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),
1066
1235
  ...ownersOf(seen),
1067
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'; };
1068
1238
  for (const other of used)
1069
1239
  if (other !== null && !(other in referenced))
1070
- referenced[other] = yield gapsOfPart(other, [...chain, key]);
1240
+ referenced[other] = isKitPart(other) ? [] : yield gapsOfPart(other, [...chain, key]);
1071
1241
  const excepted = (0, exceptions_1.withExceptions)({
1072
1242
  part: key,
1073
1243
  exceptions: (0, exceptions_1.readExceptions)(owner.dir),
@@ -1086,12 +1256,16 @@ const completenessOf = (options) => __awaiter(void 0, void 0, void 0, function*
1086
1256
  }),
1087
1257
  });
1088
1258
  exceptions.push(...excepted.listed);
1089
- return [...refused.map((detail) => ({ code: 'RENDER', target: key, detail })), ...excepted.gaps];
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,
1263
+ ];
1090
1264
  });
1091
- const gaps = yield gapsOfPart(part, []);
1092
- if (typeof gaps === 'string')
1093
- return { problem: gaps };
1094
- return exceptions.length > 0 ? { gaps, exceptions } : { 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 };
1095
1269
  }
1096
1270
  catch (error) {
1097
1271
  if (error instanceof thumb_1.ThumbUnavailableError)
@@ -1102,5 +1276,5 @@ const completenessOf = (options) => __awaiter(void 0, void 0, void 0, function*
1102
1276
  yield Promise.all([queue.close(), server.close()]);
1103
1277
  }
1104
1278
  });
1105
- exports.completenessOf = completenessOf;
1279
+ exports.partsCompletenessOf = partsCompletenessOf;
1106
1280
  //# sourceMappingURL=complete.js.map