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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/design/define-call.ts +43 -0
  3. package/design/define-part.ts +28 -8
  4. package/design/embed-page.ts +37 -4
  5. package/design/icon-sheet.ts +151 -0
  6. package/design/mark-part.ts +79 -2
  7. package/design/motion-runtime.ts +212 -5
  8. package/design/spring.ts +53 -0
  9. package/design/token-sheet.ts +87 -0
  10. package/design/web/frame.ts +139 -24
  11. package/package.json +1 -1
  12. package/src/lib/design/calls-tsconfig.js +1 -2
  13. package/src/lib/design/calls-tsconfig.js.map +1 -1
  14. package/src/lib/design/design-command.js +34 -4
  15. package/src/lib/design/design-command.js.map +1 -1
  16. package/src/lib/design/force-interactions.d.ts +2 -1
  17. package/src/lib/design/force-interactions.js +7 -4
  18. package/src/lib/design/force-interactions.js.map +1 -1
  19. package/src/lib/design/mcp-command.d.ts +60 -0
  20. package/src/lib/design/mcp-command.js +327 -0
  21. package/src/lib/design/mcp-command.js.map +1 -0
  22. package/src/lib/design/motion-command.d.ts +71 -0
  23. package/src/lib/design/motion-command.js +314 -0
  24. package/src/lib/design/motion-command.js.map +1 -0
  25. package/src/lib/design/motion-endpoint.d.ts +61 -0
  26. package/src/lib/design/motion-endpoint.js +133 -0
  27. package/src/lib/design/motion-endpoint.js.map +1 -0
  28. package/src/lib/design/motion-flow.d.ts +5 -0
  29. package/src/lib/design/motion-flow.js +29 -0
  30. package/src/lib/design/motion-flow.js.map +1 -0
  31. package/src/lib/design/motion-shot.d.ts +43 -0
  32. package/src/lib/design/motion-shot.js +97 -0
  33. package/src/lib/design/motion-shot.js.map +1 -0
  34. package/src/lib/design/ready-shots.d.ts +7 -0
  35. package/src/lib/design/ready-shots.js +22 -1
  36. package/src/lib/design/ready-shots.js.map +1 -1
  37. package/src/lib/design/render-packages.d.ts +9 -0
  38. package/src/lib/design/render-packages.js +13 -3
  39. package/src/lib/design/render-packages.js.map +1 -1
  40. package/src/lib/design/render.js +3 -3
  41. package/src/lib/design/render.js.map +1 -1
  42. package/src/lib/design/serve.js +34 -3
  43. package/src/lib/design/serve.js.map +1 -1
  44. package/src/lib/design/shot-command.d.ts +8 -2
  45. package/src/lib/design/shot-command.js +12 -8
  46. package/src/lib/design/shot-command.js.map +1 -1
  47. package/src/lib/design/shot.d.ts +16 -0
  48. package/src/lib/design/shot.js +68 -4
  49. package/src/lib/design/shot.js.map +1 -1
  50. package/src/lib/design/sketch-command.js +78 -16
  51. package/src/lib/design/sketch-command.js.map +1 -1
  52. package/src/lib/design/studio-api.d.ts +19 -0
  53. package/src/lib/design/studio-api.js +52 -1
  54. package/src/lib/design/studio-api.js.map +1 -1
  55. package/src/lib/design/system/complete-command.d.ts +1 -1
  56. package/src/lib/design/system/complete-command.js +76 -6
  57. package/src/lib/design/system/complete-command.js.map +1 -1
  58. package/src/lib/design/system/complete.d.ts +107 -9
  59. package/src/lib/design/system/complete.js +592 -141
  60. package/src/lib/design/system/complete.js.map +1 -1
  61. package/src/lib/design/system/exceptions.d.ts +59 -0
  62. package/src/lib/design/system/exceptions.js +132 -0
  63. package/src/lib/design/system/exceptions.js.map +1 -0
  64. package/src/lib/design/system/flows/flow.d.ts +3 -0
  65. package/src/lib/design/system/flows/flow.js +5 -3
  66. package/src/lib/design/system/flows/flow.js.map +1 -1
  67. package/src/lib/design/system/handoff.d.ts +11 -0
  68. package/src/lib/design/system/handoff.js +30 -0
  69. package/src/lib/design/system/handoff.js.map +1 -0
  70. package/src/lib/design/system/inventory-approval.d.ts +41 -0
  71. package/src/lib/design/system/inventory-approval.js +71 -0
  72. package/src/lib/design/system/inventory-approval.js.map +1 -0
  73. package/src/lib/design/system/inventory.d.ts +65 -0
  74. package/src/lib/design/system/inventory.js +168 -0
  75. package/src/lib/design/system/inventory.js.map +1 -0
  76. package/src/lib/design/system/list-system.js +9 -2
  77. package/src/lib/design/system/list-system.js.map +1 -1
  78. package/src/lib/design/system/render-check.js +19 -1
  79. package/src/lib/design/system/render-check.js.map +1 -1
  80. package/src/lib/design/system/resolve-product.d.ts +2 -3
  81. package/src/lib/design/system/resolve-product.js +4 -6
  82. package/src/lib/design/system/resolve-product.js.map +1 -1
  83. package/src/lib/design/system/rules/stale-replace.js +41 -2
  84. package/src/lib/design/system/rules/stale-replace.js.map +1 -1
  85. package/src/lib/design/system/rules/token-selectors.js +1 -1
  86. package/src/lib/design/system/rules/token-selectors.js.map +1 -1
  87. package/src/lib/design/system/sketch/host-edit.d.ts +4 -0
  88. package/src/lib/design/system/sketch/host-edit.js +68 -0
  89. package/src/lib/design/system/sketch/host-edit.js.map +1 -0
  90. package/src/lib/design/system/sketch/merge.d.ts +3 -2
  91. package/src/lib/design/system/sketch/merge.js +47 -12
  92. package/src/lib/design/system/sketch/merge.js.map +1 -1
  93. package/src/lib/design/system/sketch/merged-parts.d.ts +10 -0
  94. package/src/lib/design/system/sketch/merged-parts.js +24 -3
  95. package/src/lib/design/system/sketch/merged-parts.js.map +1 -1
  96. package/src/lib/design/system/sketch/no-visible-change.d.ts +6 -0
  97. package/src/lib/design/system/sketch/no-visible-change.js +25 -1
  98. package/src/lib/design/system/sketch/no-visible-change.js.map +1 -1
  99. package/src/lib/design/system/sketch/overlay.js +11 -3
  100. package/src/lib/design/system/sketch/overlay.js.map +1 -1
  101. package/src/lib/design/system/sketch/sketch.d.ts +32 -0
  102. package/src/lib/design/system/sketch/sketch.js +73 -18
  103. package/src/lib/design/system/sketch/sketch.js.map +1 -1
  104. package/src/lib/design/system/sketch/unused-closure.d.ts +12 -0
  105. package/src/lib/design/system/sketch/unused-closure.js +185 -0
  106. package/src/lib/design/system/sketch/unused-closure.js.map +1 -0
  107. package/src/lib/design/system/system-modules.js +6 -9
  108. package/src/lib/design/system/system-modules.js.map +1 -1
  109. package/src/lib/design/system/system-tsconfig.d.ts +2 -1
  110. package/src/lib/design/system/system-tsconfig.js +9 -5
  111. package/src/lib/design/system/system-tsconfig.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.completenessOf = exports.inventoryIdsOf = exports.seePart = exports.completenessGapLineOf = exports.completenessGapsOf = exports.findingKeyOf = exports.isFindingOf = 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");
@@ -20,10 +21,36 @@ const resolve_product_1 = require("./resolve-product");
20
21
  const baseline_1 = require("./sketch/baseline");
21
22
  const merged_parts_1 = require("./sketch/merged-parts");
22
23
  const system_modules_1 = require("./system-modules");
23
- const CHECKED_KINDS = ['disclosure'];
24
+ const exceptions_1 = require("./exceptions");
25
+ const inventory_approval_1 = require("./inventory-approval");
26
+ const KINDS = {
27
+ disclosure: { surface: 'body', closed: 'collapsed' },
28
+ button: { adds: ['active'] },
29
+ select: { surface: 'open', options: true },
30
+ menu: { surface: 'open', options: true },
31
+ row: { selects: true, virtualFocus: true },
32
+ field: { noActive: true },
33
+ tooltip: { surface: 'tooltip' },
34
+ };
35
+ const CHECKED_KINDS = Object.keys(KINDS);
24
36
  const INPUT_INTERACTIONS = { pointer: ['hover', 'active'], keyboard: ['focus-visible'] };
37
+ const MARKS = {
38
+ selected: 'aria-selected="true", aria-checked="true", aria-pressed="true" or aria-current',
39
+ disabled: 'disabled or aria-disabled="true"',
40
+ };
41
+ const CONTROL_MARKS = Object.assign(Object.assign({}, MARKS), { disabled: `${MARKS.disabled} on it or a data-et-control-part in it` });
25
42
  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 : []; });
43
+ const requiredInteractionsOf = (control) => {
44
+ var _a, _b;
45
+ return [
46
+ ...new Set([
47
+ ...control.input
48
+ .flatMap((input) => { var _a; return (_a = INPUT_INTERACTIONS[input]) !== null && _a !== void 0 ? _a : []; })
49
+ .filter((interaction) => { var _a; return !(((_a = KINDS[control.kind]) === null || _a === void 0 ? void 0 : _a.noActive) && interaction === 'active'); }),
50
+ ...((_b = (_a = KINDS[control.kind]) === null || _a === void 0 ? void 0 : _a.adds) !== null && _b !== void 0 ? _b : []),
51
+ ]),
52
+ ];
53
+ };
27
54
  const gridGapOf = (options) => {
28
55
  const { grid, states, interactions } = options;
29
56
  const fix = `draw its states with interactionRows, which gives the grid a column per interaction (${interactions.join(', ')})`;
@@ -55,21 +82,33 @@ const declaredControlsOf = (value) => isRecord(value)
55
82
  surfaces: isRecord(declared['surfaces'])
56
83
  ? Object.fromEntries(Object.entries(declared['surfaces']).filter((entry) => typeof entry[1] === 'string'))
57
84
  : {},
85
+ disabled: declared['disabled'] === true,
58
86
  },
59
87
  ];
60
88
  }))
61
89
  : null;
90
+ exports.declaredControlsOf = declaredControlsOf;
91
+ /** Whether a discovered element is an undeclared or unclassified finding of the part, not owned or declared. */
92
+ const isFindingOf = (declared) => (found) => {
93
+ var _a;
94
+ return ((_a = found.owner) !== null && _a !== void 0 ? _a : null) === null &&
95
+ !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; }) &&
96
+ !(found.kind === 'control' && found.marked !== null);
97
+ };
98
+ exports.isFindingOf = isFindingOf;
99
+ const findingKeyOf = (found) => `${found.kind} ${found.what} ${found.path}`;
100
+ exports.findingKeyOf = findingKeyOf;
62
101
  const discoveryGapsOf = (options) => {
63
102
  var _a, _b, _c, _d;
64
103
  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; });
104
+ const isFinding = (0, exports.isFindingOf)(declared);
66
105
  const groups = new Map();
67
106
  for (const frame of frames.filter((seen) => seen.interaction === null)) {
68
107
  const counts = new Map();
69
108
  for (const found of (_a = frame.found) !== null && _a !== void 0 ? _a : []) {
70
- if (ownedByPart(found) || (found.kind === 'control' && found.marked !== null))
109
+ if (!isFinding(found))
71
110
  continue;
72
- const key = `${found.kind} ${found.what} ${found.path}`;
111
+ const key = (0, exports.findingKeyOf)(found);
73
112
  const group = (_b = groups.get(key)) !== null && _b !== void 0 ? _b : { found, states: [], count: 0 };
74
113
  counts.set(key, ((_c = counts.get(key)) !== null && _c !== void 0 ? _c : 0) + 1);
75
114
  if (!group.states.includes(frame.state))
@@ -81,8 +120,7 @@ const discoveryGapsOf = (options) => {
81
120
  return [...groups.values()]
82
121
  .sort((a, b) => a.found.path.localeCompare(b.found.path) || a.found.what.localeCompare(b.found.what))
83
122
  .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)` : ''}`;
123
+ const where = `${found.what} at ${found.path} in ${statesShownOf(states)}${count > 1 ? ` (${count} of them)` : ''}`;
86
124
  return found.kind === 'control'
87
125
  ? {
88
126
  code: 'CONTROL-UNDECLARED',
@@ -96,6 +134,62 @@ const discoveryGapsOf = (options) => {
96
134
  };
97
135
  });
98
136
  };
137
+ const statesShownOf = (states) => states.length > 4 ? `${states.slice(0, 3).join(', ')} and ${states.length - 3} more` : states.join(', ');
138
+ const overrideGapsOf = (options) => {
139
+ var _a, _b;
140
+ const groups = new Map();
141
+ for (const frame of options.frames.filter((seen) => seen.interaction === null))
142
+ for (const override of (_a = frame.overrides) !== null && _a !== void 0 ? _a : []) {
143
+ const key = `${override.owner} ${override.selector} ${override.path}`;
144
+ const group = (_b = groups.get(key)) !== null && _b !== void 0 ? _b : { override, states: [] };
145
+ if (!group.states.includes(frame.state))
146
+ group.states.push(frame.state);
147
+ groups.set(key, group);
148
+ }
149
+ return [...groups.values()]
150
+ .sort((a, b) => a.override.owner.localeCompare(b.override.owner) || a.override.path.localeCompare(b.override.path))
151
+ .map(({ override, states }) => ({
152
+ code: 'CONTROL-OVERRIDDEN',
153
+ target: options.part,
154
+ 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`,
155
+ }));
156
+ };
157
+ const ownerGapsOf = (options) => {
158
+ var _a, _b, _c;
159
+ const { part, declared, frames, referenced } = options;
160
+ const named = new Set(Object.values(declared).map((control) => control.part));
161
+ const owned = new Map();
162
+ for (const frame of frames.filter((seen) => seen.interaction === null))
163
+ for (const found of (_a = frame.found) !== null && _a !== void 0 ? _a : []) {
164
+ const owner = (_b = found.owner) !== null && _b !== void 0 ? _b : null;
165
+ if (owner === null || named.has(owner))
166
+ continue;
167
+ owned.set(owner, ((_c = owned.get(owner)) !== null && _c !== void 0 ? _c : new Set()).add(`${found.what} ${found.path}`));
168
+ }
169
+ return [...owned.entries()]
170
+ .sort(([a], [b]) => a.localeCompare(b))
171
+ .flatMap(([owner, controls]) => {
172
+ const used = referenced[owner];
173
+ const drawn = `${owner} draws ${controls.size} control(s) here`;
174
+ if (typeof used === 'string')
175
+ return [{ code: 'CONTROL-PART-INCOMPLETE', target: part, detail: `${drawn}: ${used}` }];
176
+ if (!used || used.length === 0)
177
+ return [];
178
+ return [
179
+ {
180
+ code: 'CONTROL-PART-INCOMPLETE',
181
+ target: part,
182
+ detail: `${drawn} and is not design complete: ${used.length} gap(s) - run design complete ${owner}`,
183
+ },
184
+ ];
185
+ });
186
+ };
187
+ const ownersOf = (frames) => [
188
+ ...new Set(frames
189
+ .filter((seen) => seen.interaction === null)
190
+ .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
+ .filter((owner) => owner !== null)),
192
+ ];
99
193
  const interactionGapOf = (options) => {
100
194
  var _a;
101
195
  const { frames, id, interaction } = options;
@@ -106,8 +200,20 @@ const interactionGapOf = (options) => {
106
200
  if (forced.some((force) => force.differs))
107
201
  return null;
108
202
  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`;
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`;
204
+ 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
+ };
206
+ const keyboardActiveRowGapOf = (options) => {
207
+ const { instances, forcedGap } = options;
208
+ const active = instances.filter((seen) => seen.keyboard);
209
+ const plain = instances.filter((seen) => !seen.keyboard && !seen.selected && !seen.disabled);
210
+ if (active.length === 0)
211
+ return `${forcedGap}; no state marks it data-active-source="keyboard" either`;
212
+ if (plain.length === 0)
213
+ return `a state marks it data-active-source="keyboard", but no state draws it plain (not selected, active or disabled) to compare it with`;
214
+ if (active.some((seen) => plain.some((other) => other.look !== seen.look)))
215
+ return null;
216
+ return `it looks the same with data-active-source="keyboard" as plain - give the active look its own colours`;
111
217
  };
112
218
  const stillGapOf = (options) => {
113
219
  var _a;
@@ -118,30 +224,137 @@ const stillGapOf = (options) => {
118
224
  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
225
  return null;
120
226
  };
121
- const disclosureGapsOf = (options) => {
122
- const { frames, id, control, states } = options;
227
+ const lookGapOf = (options) => {
228
+ const { seen, flag, what, marks = MARKS } = options;
229
+ const on = seen.filter((one) => one.flag);
230
+ const off = seen.filter((one) => !one.flag);
231
+ if (on.length === 0)
232
+ return `no state draws ${what} ${flag} - mark it with ${marks[flag]} in a state`;
233
+ if (off.length === 0)
234
+ return `every state draws ${what} ${flag} - draw it without ${flag} too, to compare`;
235
+ if (!on.some((one) => off.some((other) => one.look !== other.look)))
236
+ return `${what} looks the same ${flag} as without - give the ${flag} look its own colours`;
237
+ return null;
238
+ };
239
+ const forcedGapOf = (options) => {
240
+ var _a;
241
+ const { forced, what, interaction } = options;
242
+ if (forced.some((force) => force.differs))
243
+ return null;
244
+ 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`;
246
+ return `no state draws ${what} ${interaction} - force ${interaction} on it, e.g. with forceInteraction`;
247
+ };
248
+ const surfaceGapsOf = (options) => {
249
+ const { frames, id, control, states, surface, closed } = options;
123
250
  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);
251
+ const state = control.surfaces[surface];
252
+ const surfaceId = `${id}.${surface}`;
253
+ const shows = (frame) => frame.surfaces.some((seen) => seen.id === surfaceId && seen.shown);
127
254
  const drawsControl = (frame) => frame.controls.some((seen) => seen.id === id);
128
255
  const gaps = [];
129
- for (const interaction of control.part === null ? requiredInteractionsOf(control) : []) {
130
- const gap = interactionGapOf({ frames, id, interaction });
256
+ if (closed && !rest.some((frame) => drawsControl(frame) && !shows(frame)))
257
+ gaps.push([closed, `every state that draws it also shows its ${surface} - draw a state with ${surfaceId} closed`]);
258
+ if (state === undefined)
259
+ gaps.push([surface, `declares no surfaces.${surface} - name the state that draws it open`]);
260
+ else if (!states.includes(state))
261
+ gaps.push([surface, `surfaces.${surface} names state ${state}, which the part does not declare`]);
262
+ else if (!rest.some((frame) => frame.state === state && drawsControl(frame) && shows(frame)))
263
+ gaps.push([
264
+ surface,
265
+ `state ${state} draws no visible, non-empty data-et-surface="${surfaceId}" next to data-et-control="${id}"`,
266
+ ]);
267
+ return gaps;
268
+ };
269
+ const KEYBOARD_ACTIVE = 'aria-activedescendant or data-active-source="keyboard"';
270
+ const isKeyboardActive = (option) => !!option.named || !!option.keyboard;
271
+ const isPlain = (option) => !option.selected && !isKeyboardActive(option) && option.forced.length === 0;
272
+ const keyboardActiveGapOf = (options) => {
273
+ var _a, _b;
274
+ const { frames, shownIn, forcedGap } = options;
275
+ const active = frames
276
+ .map((frame) => ({ state: frame.state, options: shownIn(frame) }))
277
+ .filter((frame) => frame.options.some(isKeyboardActive));
278
+ const compared = active.filter((frame) => frame.options.some(isPlain));
279
+ if (active.length === 0)
280
+ return `${forcedGap}; no option is marked active by ${KEYBOARD_ACTIVE} either`;
281
+ if (compared.length === 0)
282
+ 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`;
283
+ if (compared.some((frame) => frame.options.some((option) => isKeyboardActive(option) && frame.options.some((plain) => isPlain(plain) && plain.look !== option.look))))
284
+ return null;
285
+ 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
+ };
287
+ const optionGapsOf = (options) => {
288
+ const { frames, surfaceId } = options;
289
+ 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
+ const what = `an option in ${surfaceId}`;
291
+ const gaps = [];
292
+ for (const interaction of ['hover', 'focus-visible']) {
293
+ const forced = frames.flatMap((frame) => shownIn(frame).flatMap((option) => option.forced
294
+ .filter((force) => force.interaction === interaction)
295
+ .map((force) => ({ state: frame.state, differs: force.differs }))));
296
+ const gap = forcedGapOf({ forced, what, interaction });
297
+ const missing = gap && interaction === 'focus-visible' ? keyboardActiveGapOf({ frames, shownIn, forcedGap: gap }) : gap;
298
+ if (missing)
299
+ gaps.push([`option-${interaction}`, missing]);
300
+ }
301
+ const seen = frames
302
+ .filter((frame) => frame.interaction === null)
303
+ .flatMap((frame) => shownIn(frame)
304
+ .filter((option) => option.forced.length === 0 && !isKeyboardActive(option))
305
+ .map((option) => ({ look: option.look, flag: option.selected })));
306
+ const selected = lookGapOf({ seen, flag: 'selected', what });
307
+ if (selected)
308
+ gaps.push(['option-selected', selected]);
309
+ return gaps;
310
+ };
311
+ const requirementGapsOf = (options) => {
312
+ var _a;
313
+ const { frames, id, control } = options;
314
+ const kind = (_a = KINDS[control.kind]) !== null && _a !== void 0 ? _a : {};
315
+ const own = control.part === null;
316
+ const instances = frames
317
+ .filter((frame) => frame.interaction === null)
318
+ .flatMap((frame) => frame.controls.filter((seen) => seen.id === id && seen.forced.length === 0));
319
+ const gaps = [];
320
+ for (const interaction of own ? requiredInteractionsOf(control) : []) {
321
+ const forcedGap = interactionGapOf({ frames, id, interaction });
322
+ const gap = forcedGap && interaction === 'focus-visible' && kind.virtualFocus
323
+ ? keyboardActiveRowGapOf({ instances, forcedGap })
324
+ : forcedGap;
131
325
  if (gap)
132
326
  gaps.push([interaction, gap]);
133
327
  }
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
- ]);
328
+ if (own && kind.selects) {
329
+ const selected = lookGapOf({
330
+ seen: instances.map((seen) => { var _a; return ({ look: seen.look, flag: (_a = seen.selected) !== null && _a !== void 0 ? _a : false }); }),
331
+ flag: 'selected',
332
+ what: 'it',
333
+ });
334
+ const forced = frames.flatMap((frame) => frame.controls
335
+ .filter((seen) => seen.id === id && seen.selected)
336
+ .flatMap((seen) => seen.forced.filter((force) => force.interaction === 'hover'))
337
+ .map((force) => ({ state: frame.state, differs: force.differs })));
338
+ const hovered = forcedGapOf({ forced, what: 'it selected', interaction: 'hover' });
339
+ if (selected)
340
+ gaps.push(['selected', selected]);
341
+ if (hovered)
342
+ gaps.push(['selected-hover', hovered]);
343
+ }
344
+ if (own && control.disabled) {
345
+ const disabled = lookGapOf({
346
+ seen: instances.map((seen) => { var _a; return ({ look: seen.look, flag: (_a = seen.disabled) !== null && _a !== void 0 ? _a : false }); }),
347
+ flag: 'disabled',
348
+ what: 'it',
349
+ marks: CONTROL_MARKS,
350
+ });
351
+ if (disabled)
352
+ gaps.push(['disabled', disabled]);
353
+ }
354
+ if (kind.surface)
355
+ gaps.push(...surfaceGapsOf(Object.assign(Object.assign({}, options), { surface: kind.surface, closed: kind.closed })));
356
+ if (own && kind.options && kind.surface)
357
+ gaps.push(...optionGapsOf({ frames, surfaceId: `${id}.${kind.surface}` }));
145
358
  return gaps;
146
359
  };
147
360
  /**
@@ -150,8 +363,8 @@ const disclosureGapsOf = (options) => {
150
363
  */
151
364
  const completenessGapsOf = (options) => {
152
365
  var _a;
153
- const { part, states, frames, reviewed, grid, referenced = {} } = options;
154
- const declared = (_a = declaredControlsOf(options.controls)) !== null && _a !== void 0 ? _a : {};
366
+ const { part, states, frames, reviewed, grid, referenced = {}, inventory } = options;
367
+ const declared = (_a = (0, exports.declaredControlsOf)(options.controls)) !== null && _a !== void 0 ? _a : {};
155
368
  const gaps = [];
156
369
  if (!reviewed)
157
370
  gaps.push({
@@ -208,7 +421,7 @@ const completenessGapsOf = (options) => {
208
421
  gaps.push({ code: 'CONTROL-UNDRAWN', target: `${part}.${id}`, detail: `no state draws data-et-control="${id}"` });
209
422
  continue;
210
423
  }
211
- for (const [requirement, detail] of disclosureGapsOf({ frames, id, control, states }))
424
+ for (const [requirement, detail] of requirementGapsOf({ frames, id, control, states }))
212
425
  gaps.push({ code: 'REQUIREMENT-MISSING', target: `${part}.${id}.${requirement}`, detail });
213
426
  for (const interaction of interactions) {
214
427
  const gap = stillGapOf({ frames, id, interaction });
@@ -216,9 +429,21 @@ const completenessGapsOf = (options) => {
216
429
  gaps.push({ code: 'CONTROL-TRANSITION-MISSING', target: `${part}.${id}.${interaction}`, detail: gap });
217
430
  }
218
431
  }
219
- return [...gaps, ...discoveryGapsOf({ part, declared, frames })];
432
+ return [
433
+ ...gaps,
434
+ ...discoveryGapsOf({ part, declared, frames }),
435
+ ...ownerGapsOf({ part, declared, frames, referenced }),
436
+ ...overrideGapsOf({ part, frames }),
437
+ ...inventoryGapsOf({ part, inventory }),
438
+ ];
220
439
  };
221
440
  exports.completenessGapsOf = completenessGapsOf;
441
+ const inventoryGapsOf = ({ part, inventory }) => {
442
+ const reason = inventory ? (0, inventory_approval_1.inventoryReasonOf)(inventory) : null;
443
+ return reason === null
444
+ ? []
445
+ : [{ code: 'INVENTORY-UNAPPROVED', target: part, detail: `${reason} - check and approve it in Studio` }];
446
+ };
222
447
  const completenessGapLineOf = (gap) => `${gap.code} ${gap.target} - ${gap.detail}`;
223
448
  exports.completenessGapLineOf = completenessGapLineOf;
224
449
  const NO_MOTION = '*, *::before, *::after { transition: none !important; animation: none !important; }';
@@ -352,37 +577,13 @@ const LIKELY_CONTROLS = [
352
577
  '[tabindex]',
353
578
  ...INTERACTIVE_ROLES.map((role) => `[role="${role}"]`),
354
579
  ];
580
+ const LOOK_PROPERTIES = [...COLOUR_PROPERTIES, 'opacity', 'box-shadow', 'font-weight', 'text-decoration-line'];
581
+ const OPTION_SELECTOR = '[role="option"], [role^="menuitem"], [data-et-option]';
355
582
  const discoverControls = `((root) => {
356
583
  const LIKELY = ${JSON.stringify(LIKELY_CONTROLS)};
357
584
  const likely = LIKELY.join(', ');
358
585
  const sdk = ${JSON.stringify(SDK_CONTROLS.join(', '))};
359
586
  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
587
  const HOVER = /(?<!\\\\):hover(?![\\w-])/g;
387
588
  const FORCED_HOVER = /:is\\(:hover,\\s*\\[data-et-force~="?hover"?\\]\\)/g;
388
589
  const hoveredOf = (written) => {
@@ -408,7 +609,7 @@ const discoverControls = `((root) => {
408
609
  for (const rule of rules) {
409
610
  if (rule instanceof CSSStyleRule) {
410
611
  const own = splitTop(rule.selectorText).map((selector) =>
411
- parent === null ? selector : selector.includes('&') ? selector.replaceAll('&', ':is(' + parent + ')') : ':is(' + parent + ') ' + selector,
612
+ nested(selector, parent),
412
613
  );
413
614
  for (const selector of own) {
414
615
  const target = hoveredOf(selector);
@@ -426,17 +627,22 @@ const discoverControls = `((root) => {
426
627
  walk(sheet.cssRules, null);
427
628
  } catch {}
428
629
  }
630
+ const markerOf = (at, element) =>
631
+ at.getAttribute('data-et-control') ?? (element.matches(${JSON.stringify(OPTION_SELECTOR)}) ? at.getAttribute('data-et-surface')?.split('.')[0] : null) ?? null;
429
632
  const withinOf = (element) => {
430
633
  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'));
634
+ for (let at = element; at && at !== root; at = at.parentElement) {
635
+ const id = markerOf(at, element);
636
+ if (id !== null) ids.push(id);
637
+ }
433
638
  return ids;
434
639
  };
435
640
  const markedOf = (element) => {
436
641
  if (element.hasAttribute('data-et-control')) return element.getAttribute('data-et-control');
437
642
  for (let at = element.parentElement; at && at !== root; at = at.parentElement) {
438
643
  if (at.matches(likely)) return null;
439
- if (at.hasAttribute('data-et-control')) return at.getAttribute('data-et-control');
644
+ const id = markerOf(at, element);
645
+ if (id !== null) return id;
440
646
  }
441
647
  return null;
442
648
  };
@@ -447,7 +653,7 @@ const discoverControls = `((root) => {
447
653
  if (what) {
448
654
  const inSdk = element.parentElement.closest(sdk);
449
655
  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) });
656
+ 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
657
  continue;
452
658
  }
453
659
  const cursor = getComputedStyle(element).cursor;
@@ -455,14 +661,130 @@ const discoverControls = `((root) => {
455
661
  if (!pointer && !hovered.has(element)) continue;
456
662
  if (element.closest('[data-et-control], ' + likely) !== null) continue;
457
663
  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: [] });
664
+ found.push({ kind: 'unclassified', what: nameOf(element) + ' with ' + reason, path: pathOf(element), marked: null, within: [], owner: ownerOf(element)?.key ?? null, box: frameBoxOf(element) });
459
665
  }
460
666
  return found;
461
667
  })`;
462
- const seeFrame = (discover) => `(() => {
668
+ const ROOT_LAYOUT = [
669
+ '^margin',
670
+ '^inset',
671
+ '^(top|right|bottom|left)$',
672
+ '^(min-|max-)?(width|height|inline-size|block-size)$',
673
+ '^position$',
674
+ '^z-index$',
675
+ '^flex(-grow|-shrink|-basis)?$',
676
+ '^grid-(area|row|column)',
677
+ '^(align|justify|place)-self$',
678
+ '^order$',
679
+ '^(transform|translate|rotate|scale)$',
680
+ ];
681
+ const findOverrides = `((ownCss) => {
682
+ const LAYOUT = new RegExp(${JSON.stringify(ROOT_LAYOUT.join('|'))});
683
+ const USER_ACTION = /(?<!\\\\):(hover|focus-visible|focus-within|focus|active)(?![\\w-])/g;
684
+ const PSEUDO_ELEMENT = /::?(before|after|marker|placeholder|selection|backdrop|file-selector-button|first-line|first-letter)(?![\\w-])/g;
685
+ const declaredByHost = (element) => {
686
+ for (let at = element; at && at !== root.parentElement; at = at.parentElement)
687
+ if (at.hasAttribute('data-et-control') && ownerOf(at) === null) return true;
688
+ return false;
689
+ };
690
+ const overrides = [];
691
+ const seen = new Set();
692
+ const check = (selector, style) => {
693
+ const properties = [...new Set(style.cssText.split(';').map((declaration) => declaration.split(':')[0].trim()).filter(Boolean))];
694
+ if (properties.length === 0) return;
695
+ let matched = [];
696
+ try {
697
+ matched = [...root.querySelectorAll(selector.replace(PSEUDO_ELEMENT, '').replace(USER_ACTION, ':where(*)'))];
698
+ } catch {
699
+ return;
700
+ }
701
+ for (const element of matched) {
702
+ const owner = ownerOf(element);
703
+ if (owner === null || declaredByHost(element)) continue;
704
+ const look = owner.at === element ? properties.filter((property) => !LAYOUT.test(property)) : properties;
705
+ const key = selector + ' ' + pathOf(element);
706
+ if (look.length === 0 || seen.has(key)) continue;
707
+ seen.add(key);
708
+ overrides.push({ owner: owner.key, selector, path: pathOf(element), properties: look });
709
+ }
710
+ };
711
+ const walk = (rules, parent) => {
712
+ for (const rule of rules) {
713
+ if (rule instanceof CSSStyleRule) {
714
+ const own = splitTop(rule.selectorText).map((selector) => nested(selector, parent));
715
+ for (const selector of own) check(selector, rule.style);
716
+ if (rule.cssRules && rule.cssRules.length > 0) walk(rule.cssRules, own.join(', '));
717
+ } else if (parent !== null && rule.constructor.name === 'CSSNestedDeclarations') check(parent, rule.style);
718
+ else if (rule.cssRules) walk(rule.cssRules, parent);
719
+ }
720
+ };
721
+ for (const text of ownCss) {
722
+ const sheet = new CSSStyleSheet();
723
+ try {
724
+ sheet.replaceSync(text);
725
+ } catch {
726
+ continue;
727
+ }
728
+ walk(sheet.cssRules, null);
729
+ }
730
+ return overrides;
731
+ })`;
732
+ /** A control root and every `data-et-control-part` whose nearest `data-et-control` is that root, in the page. */
733
+ const controlPartsOf = `((control) => [
734
+ control,
735
+ ...[...control.querySelectorAll('[data-et-control-part]')].filter((part) => part.parentElement.closest('[data-et-control]') === control),
736
+ ])`;
737
+ const seeFrame = (options) => `(() => {
463
738
  const root = document.getElementById('root');
464
739
  const refused = root.innerText.trim();
465
740
  if (refused.startsWith('design-explore:')) return { refused: refused.split('\\n')[0] };
741
+ const ORIGIN = ${options.clip} ? root.getBoundingClientRect() : { left: -scrollX, top: -scrollY };
742
+ const frameBoxOf = (element) => {
743
+ const box = element.getBoundingClientRect();
744
+ return box.width > 0 && box.height > 0
745
+ ? { x: Math.round(box.left - ORIGIN.left), y: Math.round(box.top - ORIGIN.top), width: Math.round(box.width), height: Math.round(box.height) }
746
+ : null;
747
+ };
748
+ const OWN = ${JSON.stringify(options.part)};
749
+ const tokensOf = (element, name) => (element.getAttribute(name) ?? '').split(' ').filter(Boolean);
750
+ const ownerOf = (element) => {
751
+ const slotted = tokensOf(element, 'data-et-slot');
752
+ for (let at = element; at; at = at.parentElement)
753
+ for (const key of tokensOf(at, 'data-et-part')) {
754
+ const index = slotted.indexOf(key);
755
+ if (index >= 0) slotted.splice(index, 1);
756
+ else return key === OWN ? null : { key, at };
757
+ }
758
+ return null;
759
+ };
760
+ const nameOf = (element) => {
761
+ const cls = [...element.classList].find((name) => /^[a-z][\\w-]*$/i.test(name));
762
+ return element.tagName.toLowerCase() + (cls ? '.' + cls : '');
763
+ };
764
+ const pathOf = (element) => {
765
+ const names = [];
766
+ for (let at = element; at && at !== root; at = at.parentElement) names.unshift(nameOf(at));
767
+ return (names.length > 4 ? '... > ' : '') + names.slice(-4).join(' > ');
768
+ };
769
+ const splitTop = (selector) => {
770
+ const parts = [];
771
+ let depth = 0;
772
+ let from = 0;
773
+ for (let at = 0; at < selector.length; at++) {
774
+ const char = selector[at];
775
+ if (char === '\\\\') at++;
776
+ else if (char === '(' || char === '[') depth++;
777
+ else if (char === ')' || char === ']') depth--;
778
+ else if (char === ',' && depth === 0) {
779
+ parts.push(selector.slice(from, at).trim());
780
+ from = at + 1;
781
+ }
782
+ }
783
+ parts.push(selector.slice(from).trim());
784
+ return parts.filter(Boolean);
785
+ };
786
+ const nested = (selector, parent) =>
787
+ parent === null ? selector : selector.includes('&') ? selector.replaceAll('&', ':is(' + parent + ')') : ':is(' + parent + ') ' + selector;
466
788
  const shown = (element) => {
467
789
  const box = element.getBoundingClientRect();
468
790
  let { left, top, right, bottom } = box;
@@ -479,35 +801,75 @@ const seeFrame = (discover) => `(() => {
479
801
  }
480
802
  return element.innerText.trim() !== '' || !!element.querySelector('img, svg, canvas, video, iframe, input, select, textarea');
481
803
  };
482
- const controls = [...root.querySelectorAll('[data-et-control]')].map((element, at) => {
804
+ const LOOK = ${JSON.stringify(LOOK_PROPERTIES)};
805
+ const lookOf = (element) => {
806
+ const own = LOOK.map((name) => getComputedStyle(element).getPropertyValue(name));
807
+ const inside = [...element.querySelectorAll('*')].map((child) => {
808
+ const style = getComputedStyle(child);
809
+ return LOOK.map((name) => style.getPropertyValue(name)).join(' ');
810
+ });
811
+ return JSON.stringify([own, [...new Set(inside)].sort()]);
812
+ };
813
+ const named = new Set([...root.querySelectorAll('[aria-activedescendant]')].filter((element) => ownerOf(element) === null).map((element) => element.getAttribute('aria-activedescendant')));
814
+ const isSelected = (element) =>
815
+ ['aria-selected', 'aria-checked', 'aria-pressed'].some((name) => element.getAttribute(name) === 'true') ||
816
+ (element.hasAttribute('aria-current') && element.getAttribute('aria-current') !== 'false');
817
+ const isDisabled = (element) =>
818
+ element.matches(':disabled') || element.getAttribute('aria-disabled') === 'true' || tokensOf(element, 'data-et-force').includes('disabled');
819
+ const isKeyboard = (element) => element.getAttribute('data-active-source') === 'keyboard';
820
+ let next = 0;
821
+ const tagged = (element) => {
822
+ const at = next++;
483
823
  element.setAttribute('data-et-complete', String(at));
484
824
  const box = element.getBoundingClientRect();
485
825
  const x = Math.max(0, box.x + scrollX - ${PAD});
486
826
  const y = Math.max(0, box.y + scrollY - ${PAD});
487
827
  return {
488
- id: element.getAttribute('data-et-control'),
489
828
  at,
490
- forced: (element.getAttribute('data-et-force') ?? '').split(' ').filter(Boolean),
829
+ forced: tokensOf(element, 'data-et-force'),
491
830
  box: box.width > 0 && box.height > 0 ? { x, y, width: box.width + ${PAD * 2}, height: box.height + ${PAD * 2} } : null,
831
+ selected: isSelected(element),
832
+ look: lookOf(element),
833
+ };
834
+ };
835
+ const controls = [...root.querySelectorAll('[data-et-control]')].filter((element) => ownerOf(element) === null).map((element) => {
836
+ const parts = ${controlPartsOf}(element);
837
+ const forced = new Set(parts.flatMap((part) => tokensOf(part, 'data-et-force')));
838
+ for (const part of parts) if (forced.size > 0) part.setAttribute('data-et-force', [...forced].join(' '));
839
+ return {
840
+ id: element.getAttribute('data-et-control'),
841
+ disabled: parts.some(isDisabled),
842
+ keyboard: parts.some(isKeyboard),
843
+ frameBox: frameBoxOf(element),
844
+ ...tagged(element),
492
845
  };
493
846
  });
494
- const surfaces = [...root.querySelectorAll('[data-et-surface]')].map((element) => ({
847
+ const surfaces = [...root.querySelectorAll('[data-et-surface]')].filter((element) => ownerOf(element) === null).map((element) => ({
495
848
  id: element.getAttribute('data-et-surface'),
496
849
  shown: shown(element),
850
+ 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
851
  }));
498
- return { controls, surfaces, found: ${discover} ? ${discoverControls}(root) : [] };
852
+ const discover = ${options.discover};
853
+ return {
854
+ controls,
855
+ surfaces,
856
+ found: discover ? ${discoverControls}(root) : [],
857
+ overrides: discover ? ${findOverrides}(${JSON.stringify(options.ownCss)}) : [],
858
+ };
499
859
  })()`;
500
860
  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');
861
+ const found = document.querySelector('[data-et-complete="' + at + '"]');
862
+ for (const element of found.hasAttribute('data-et-control') ? ${controlPartsOf}(found) : [found]) {
863
+ const tokens = new Set((element.getAttribute('data-et-force') ?? '').split(' ').filter(Boolean));
864
+ if (on) tokens.add(interaction);
865
+ else tokens.delete(interaction);
866
+ if (tokens.size > 0) element.setAttribute('data-et-force', [...tokens].join(' '));
867
+ else element.removeAttribute('data-et-force');
868
+ }
507
869
  })(${JSON.stringify(at)}, ${JSON.stringify(interaction)}, ${JSON.stringify(on)})`;
508
870
  const FORCEABLE = new Set(['hover', 'focus-visible', 'active']);
509
871
  const seeFrames = (options) => __awaiter(void 0, void 0, void 0, function* () {
510
- const { browser, base, frames } = options;
872
+ const { browser, base, frames, part, ownCss } = options;
511
873
  const context = yield browser.newContext();
512
874
  const seen = [];
513
875
  const refused = [];
@@ -518,41 +880,51 @@ const seeFrames = (options) => __awaiter(void 0, void 0, void 0, function* () {
518
880
  yield (0, thumb_1.openFrame)(page, `${base}/frame.html?${frame.search}`);
519
881
  yield page.addStyleTag({ content: NO_MOTION });
520
882
  yield page.evaluate(thumb_1.settled);
521
- const found = (yield page.evaluate(seeFrame(frame.interaction === null)));
883
+ const found = (yield page.evaluate(seeFrame({ discover: frame.interaction === null, part, ownCss, clip: frame.clip })));
522
884
  if ('refused' in found) {
523
885
  refused.push(`${frame.state}: ${found.refused}`);
524
886
  continue;
525
887
  }
526
- const controls = [];
527
- for (const control of found.controls) {
888
+ const forcedOf = (element) => __awaiter(void 0, void 0, void 0, function* () {
528
889
  const forced = [];
529
- for (const interaction of control.forced.filter((name) => FORCEABLE.has(name))) {
530
- if (!control.box) {
890
+ for (const interaction of element.forced.filter((name) => FORCEABLE.has(name))) {
891
+ if (!element.box) {
531
892
  forced.push({ interaction, differs: false });
532
893
  continue;
533
894
  }
534
- const shot = () => { var _a; return page.screenshot({ type: 'png', fullPage: true, clip: (_a = control.box) !== null && _a !== void 0 ? _a : undefined, animations: 'disabled' }); };
895
+ const shot = () => { var _a; return page.screenshot({ type: 'png', fullPage: true, clip: (_a = element.box) !== null && _a !== void 0 ? _a : undefined, animations: 'disabled' }); };
535
896
  const drawn = yield shot();
536
- const drawnColours = (yield page.evaluate(colourSnapshot(control.at)));
537
- yield page.evaluate(forceOn({ at: control.at, interaction, on: false }));
897
+ const drawnColours = (yield page.evaluate(colourSnapshot(element.at)));
898
+ yield page.evaluate(forceOn({ at: element.at, interaction, on: false }));
538
899
  yield page.evaluate(thumb_1.settled);
539
900
  const rest = yield shot();
540
- const restColours = (yield page.evaluate(colourSnapshot(control.at)));
541
- yield page.evaluate(forceOn({ at: control.at, interaction, on: true }));
901
+ const restColours = (yield page.evaluate(colourSnapshot(element.at)));
902
+ yield page.evaluate(forceOn({ at: element.at, interaction, on: true }));
542
903
  forced.push({
543
904
  interaction,
544
905
  differs: !drawn.equals(rest),
545
906
  still: stillChangesOf(restColours, drawnColours),
546
907
  });
547
908
  }
548
- controls.push({ id: control.id, forced });
909
+ return forced;
910
+ });
911
+ const controls = [];
912
+ const surfaces = [];
913
+ for (const control of found.controls)
914
+ 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 }));
915
+ for (const surface of found.surfaces) {
916
+ const options = [];
917
+ for (const option of surface.options)
918
+ options.push(Object.assign(Object.assign({ selected: option.selected, look: option.look, forced: yield forcedOf(option) }, (option.named ? { named: true } : {})), (option.keyboard ? { keyboard: true } : {})));
919
+ surfaces.push({ id: surface.id, shown: surface.shown, options });
549
920
  }
550
921
  seen.push({
551
922
  state: frame.state,
552
923
  interaction: frame.interaction,
553
924
  controls,
554
- surfaces: found.surfaces,
925
+ surfaces,
555
926
  found: found.found,
927
+ overrides: found.overrides,
556
928
  });
557
929
  }
558
930
  }
@@ -561,74 +933,146 @@ const seeFrames = (options) => __awaiter(void 0, void 0, void 0, function* () {
561
933
  }
562
934
  return { seen, refused };
563
935
  });
936
+ /**
937
+ * Draws every state of one part of the product, at rest and, unless `restOnly`, in each interaction it declares,
938
+ * once at its first preset or width, and answers what each frame showed, or why its module did not load as a string.
939
+ */
940
+ const seePart = (options) => __awaiter(void 0, void 0, void 0, function* () {
941
+ var _a, _b, _c, _d, _e;
942
+ const { product, part, system, base, load, draw, restOnly = false, sketch = null } = options;
943
+ const key = (0, resolve_product_1.partKeyOf)(part);
944
+ const loaded = yield load((0, system_modules_1.productModuleIdOf)({ file: part.file, product: product.name, sketch }))
945
+ .then((module) => ({ module: module }))
946
+ .catch((error) => ({ error: error instanceof Error ? error.message : String(error) }));
947
+ if ('error' in loaded)
948
+ return loaded.error;
949
+ const definition = loaded.module.default;
950
+ const states = isRecord(definition === null || definition === void 0 ? void 0 : definition.states) ? Object.keys(definition.states) : [];
951
+ const interactions = Array.isArray(definition === null || definition === void 0 ? void 0 : definition.interactions)
952
+ ? definition.interactions.filter((name) => force_interactions_1.INTERACTIONS.some((known) => known === name))
953
+ : [];
954
+ const widths = Array.isArray(definition === null || definition === void 0 ? void 0 : definition.widths)
955
+ ? definition.widths.filter((width) => typeof width === 'number')
956
+ : [];
957
+ const presets = Object.keys((_b = (_a = system.drawnOf(product.name, null)) === null || _a === void 0 ? void 0 : _a.presets) !== null && _b !== void 0 ? _b : {});
958
+ const preset = part.layer === 'pages' ? ((_c = presets[0]) !== null && _c !== void 0 ? _c : null) : null;
959
+ const width = preset === null ? ((_d = widths[0]) !== null && _d !== void 0 ? _d : null) : null;
960
+ const frames = [];
961
+ for (const state of states)
962
+ for (const interaction of [null, ...(restOnly ? [] : interactions)]) {
963
+ const frame = (0, shot_1.partFrameOf)({
964
+ system,
965
+ product: product.name,
966
+ part: key,
967
+ state,
968
+ preset,
969
+ width,
970
+ sketch,
971
+ interaction,
972
+ });
973
+ if (!(frame instanceof Error))
974
+ frames.push({
975
+ state,
976
+ interaction,
977
+ search: frame.search.toString(),
978
+ size: (_e = frame.size) !== null && _e !== void 0 ? _e : null,
979
+ clip: preset === null,
980
+ });
981
+ }
982
+ const dir = (0, path_1.dirname)(part.file);
983
+ const ownCss = (0, fs_1.readdirSync)(dir)
984
+ .filter((file) => file.endsWith('.css'))
985
+ .sort()
986
+ .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 }));
988
+ const owner = (0, inventory_approval_1.partOwnerOf)({ product, part });
989
+ return {
990
+ definition,
991
+ states,
992
+ seen,
993
+ refused,
994
+ addresses: states.map((state) => ({ state, preset, width })),
995
+ owner,
996
+ hash: (0, baseline_1.partHashOf)({ product: owner, dir }),
997
+ };
998
+ });
999
+ exports.seePart = seePart;
1000
+ /** The control ids a page draws at rest, the set its inventory approval holds. */
1001
+ const inventoryIdsOf = (frames) => [
1002
+ ...new Set(frames.filter((frame) => frame.interaction === null).flatMap((frame) => frame.controls.map(({ id }) => id))),
1003
+ ].sort();
1004
+ exports.inventoryIdsOf = inventoryIdsOf;
564
1005
  /**
565
1006
  * 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.
1007
+ * 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.
1009
+ * A page fails until the user approves its control inventory in Studio. An exception the user approved in Studio
1010
+ * drops the gap it names until the part changes; a pending one still fails. With `sketch`, every part is drawn as
1011
+ * that variant draws it, and a part the variant changes is not merged yet, so it never fails as unreviewed.
568
1012
  */
569
1013
  const completenessOf = (options) => __awaiter(void 0, void 0, void 0, function* () {
570
1014
  var _a, _b, _c, _d;
571
- const { target, config, product, part } = options;
1015
+ const { target, config, product, part, sketch = null } = options;
572
1016
  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}"` };
1017
+ 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 : [];
1019
+ if (!parts.some((candidate) => (0, resolve_product_1.partKeyOf)(candidate) === part))
1020
+ return {
1021
+ problem: sketch
1022
+ ? `variant "${sketch.variant}" of the sketch on ${sketch.target} in product "${product.name}" has no part "${part}"`
1023
+ : `product "${product.name}" has no part "${part}"`,
1024
+ };
575
1025
  if (!(yield resolveChromium(target)))
576
1026
  return { unavailable: 'neither the checkout nor this package resolves playwright or playwright-core' };
577
1027
  const queue = (0, thumb_1.createDrawQueue)({ target, chromiumOf: resolveChromium });
578
1028
  const server = yield (0, serve_1.createDesignServer)({ target, config, listen: true, port: 0 });
579
1029
  try {
580
1030
  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 : {});
1031
+ const { port } = (_d = server.httpServer) === null || _d === void 0 ? void 0 : _d.address();
1032
+ const checked = new Map();
1033
+ const exceptions = [];
584
1034
  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);
1035
+ const known = checked.get(key);
1036
+ if (known !== undefined && !chain.includes(key))
1037
+ return known;
1038
+ const gaps = yield freshGapsOfPart(key, chain);
1039
+ checked.set(key, gaps);
1040
+ return gaps;
1041
+ });
1042
+ const freshGapsOfPart = (key, chain) => __awaiter(void 0, void 0, void 0, function* () {
1043
+ var _a, _b, _c;
1044
+ const resolved = parts.find((candidate) => (0, resolve_product_1.partKeyOf)(candidate) === key);
587
1045
  if (!resolved)
588
1046
  return `product "${product.name}" has no part "${key}"`;
589
1047
  if (chain.includes(key))
590
1048
  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) });
1049
+ const sight = yield (0, exports.seePart)({
1050
+ product,
1051
+ part: resolved,
1052
+ system,
1053
+ base: `http://localhost:${port}`,
1054
+ load: (id) => server.ssrLoadModule(id),
1055
+ draw: (job) => queue.run(job),
1056
+ sketch,
1057
+ });
1058
+ if (typeof sight === 'string')
1059
+ 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;
1062
+ const reviewed = sketched || (0, merged_parts_1.readMerged)(owner.dir)[key] === hash;
625
1063
  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)({
1064
+ 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),
1066
+ ...ownersOf(seen),
1067
+ ];
1068
+ for (const other of used)
1069
+ if (other !== null && !(other in referenced))
1070
+ referenced[other] = yield gapsOfPart(other, [...chain, key]);
1071
+ const excepted = (0, exceptions_1.withExceptions)({
1072
+ part: key,
1073
+ exceptions: (0, exceptions_1.readExceptions)(owner.dir),
1074
+ hash,
1075
+ gaps: (0, exports.completenessGapsOf)({
632
1076
  part: key,
633
1077
  controls: definition === null || definition === void 0 ? void 0 : definition.controls,
634
1078
  states,
@@ -636,11 +1080,18 @@ const completenessOf = (options) => __awaiter(void 0, void 0, void 0, function*
636
1080
  reviewed,
637
1081
  grid: definition === null || definition === void 0 ? void 0 : definition.grid,
638
1082
  referenced,
1083
+ inventory: resolved.layer === 'pages'
1084
+ ? (0, inventory_approval_1.inventoryStateOf)({ approval: (0, inventory_approval_1.readInventory)(owner.dir)[key], ids: (0, exports.inventoryIdsOf)(seen), hash })
1085
+ : undefined,
639
1086
  }),
640
- ];
1087
+ });
1088
+ exceptions.push(...excepted.listed);
1089
+ return [...refused.map((detail) => ({ code: 'RENDER', target: key, detail })), ...excepted.gaps];
641
1090
  });
642
1091
  const gaps = yield gapsOfPart(part, []);
643
- return typeof gaps === 'string' ? { problem: gaps } : { gaps };
1092
+ if (typeof gaps === 'string')
1093
+ return { problem: gaps };
1094
+ return exceptions.length > 0 ? { gaps, exceptions } : { gaps };
644
1095
  }
645
1096
  catch (error) {
646
1097
  if (error instanceof thumb_1.ThumbUnavailableError)