stimeo-ui 0.6.0 → 0.7.0

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 (199) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/controllers/alert_dialog_controller.d.ts +2 -0
  3. package/dist/controllers/alert_dialog_controller.js +75 -4
  4. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  5. package/dist/controllers/auto_submit_controller.d.ts +2 -0
  6. package/dist/controllers/auto_submit_controller.js.map +1 -1
  7. package/dist/controllers/avatar_controller.js +47 -5
  8. package/dist/controllers/avatar_controller.js.map +1 -1
  9. package/dist/controllers/breadcrumb_controller.d.ts +2 -0
  10. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  11. package/dist/controllers/bulk_select_controller.d.ts +2 -0
  12. package/dist/controllers/bulk_select_controller.js.map +1 -1
  13. package/dist/controllers/calendar_controller.d.ts +2 -0
  14. package/dist/controllers/calendar_controller.js.map +1 -1
  15. package/dist/controllers/character_counter_controller.d.ts +52 -20
  16. package/dist/controllers/character_counter_controller.js +338 -63
  17. package/dist/controllers/character_counter_controller.js.map +1 -1
  18. package/dist/controllers/checkbox_controller.d.ts +3 -0
  19. package/dist/controllers/checkbox_controller.js.map +1 -1
  20. package/dist/controllers/color_picker_controller.d.ts +3 -0
  21. package/dist/controllers/color_picker_controller.js.map +1 -1
  22. package/dist/controllers/combobox_controller.d.ts +2 -0
  23. package/dist/controllers/combobox_controller.js.map +1 -1
  24. package/dist/controllers/command_palette_controller.d.ts +2 -0
  25. package/dist/controllers/command_palette_controller.js +75 -4
  26. package/dist/controllers/command_palette_controller.js.map +1 -1
  27. package/dist/controllers/conditional_fields_controller.d.ts +42 -14
  28. package/dist/controllers/conditional_fields_controller.js +345 -51
  29. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  30. package/dist/controllers/confirm_controller.d.ts +2 -0
  31. package/dist/controllers/confirm_controller.js +75 -4
  32. package/dist/controllers/confirm_controller.js.map +1 -1
  33. package/dist/controllers/count_up_controller.d.ts +2 -0
  34. package/dist/controllers/count_up_controller.js.map +1 -1
  35. package/dist/controllers/countdown_controller.d.ts +4 -0
  36. package/dist/controllers/countdown_controller.js.map +1 -1
  37. package/dist/controllers/currency_input_controller.d.ts +2 -0
  38. package/dist/controllers/currency_input_controller.js.map +1 -1
  39. package/dist/controllers/data_grid_controller.d.ts +3 -0
  40. package/dist/controllers/data_grid_controller.js.map +1 -1
  41. package/dist/controllers/dialog_controller.js +75 -4
  42. package/dist/controllers/dialog_controller.js.map +1 -1
  43. package/dist/controllers/direct_upload_controller.d.ts +56 -24
  44. package/dist/controllers/direct_upload_controller.js +201 -45
  45. package/dist/controllers/direct_upload_controller.js.map +1 -1
  46. package/dist/controllers/dirty_form_controller.d.ts +14 -6
  47. package/dist/controllers/dirty_form_controller.js +192 -29
  48. package/dist/controllers/dirty_form_controller.js.map +1 -1
  49. package/dist/controllers/dismissible_controller.d.ts +2 -0
  50. package/dist/controllers/dismissible_controller.js +83 -18
  51. package/dist/controllers/dismissible_controller.js.map +1 -1
  52. package/dist/controllers/drawer_controller.js +75 -4
  53. package/dist/controllers/drawer_controller.js.map +1 -1
  54. package/dist/controllers/empty_state_controller.d.ts +2 -0
  55. package/dist/controllers/empty_state_controller.js.map +1 -1
  56. package/dist/controllers/file_dropzone_controller.d.ts +2 -0
  57. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  58. package/dist/controllers/filter_controller.d.ts +2 -0
  59. package/dist/controllers/filter_controller.js.map +1 -1
  60. package/dist/controllers/flash_controller.d.ts +4 -0
  61. package/dist/controllers/flash_controller.js.map +1 -1
  62. package/dist/controllers/focus_controller.d.ts +2 -0
  63. package/dist/controllers/focus_controller.js +75 -4
  64. package/dist/controllers/focus_controller.js.map +1 -1
  65. package/dist/controllers/form_field_controller.d.ts +50 -10
  66. package/dist/controllers/form_field_controller.js +280 -62
  67. package/dist/controllers/form_field_controller.js.map +1 -1
  68. package/dist/controllers/form_validation_controller.d.ts +10 -8
  69. package/dist/controllers/form_validation_controller.js +208 -83
  70. package/dist/controllers/form_validation_controller.js.map +1 -1
  71. package/dist/controllers/frame_loading_controller.d.ts +2 -0
  72. package/dist/controllers/frame_loading_controller.js.map +1 -1
  73. package/dist/controllers/highlight_controller.d.ts +2 -0
  74. package/dist/controllers/highlight_controller.js.map +1 -1
  75. package/dist/controllers/idle_controller.d.ts +2 -0
  76. package/dist/controllers/idle_controller.js.map +1 -1
  77. package/dist/controllers/input_mask_controller.d.ts +2 -0
  78. package/dist/controllers/input_mask_controller.js.map +1 -1
  79. package/dist/controllers/lazy_frame_controller.d.ts +2 -0
  80. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  81. package/dist/controllers/listbox_controller.d.ts +2 -0
  82. package/dist/controllers/listbox_controller.js.map +1 -1
  83. package/dist/controllers/local_time_controller.d.ts +2 -0
  84. package/dist/controllers/local_time_controller.js.map +1 -1
  85. package/dist/controllers/masonry_controller.d.ts +2 -0
  86. package/dist/controllers/masonry_controller.js.map +1 -1
  87. package/dist/controllers/meter_controller.d.ts +2 -0
  88. package/dist/controllers/meter_controller.js.map +1 -1
  89. package/dist/controllers/multi_select_controller.d.ts +2 -0
  90. package/dist/controllers/multi_select_controller.js.map +1 -1
  91. package/dist/controllers/nested_form_controller.d.ts +2 -0
  92. package/dist/controllers/nested_form_controller.js.map +1 -1
  93. package/dist/controllers/network_status_controller.d.ts +2 -0
  94. package/dist/controllers/network_status_controller.js.map +1 -1
  95. package/dist/controllers/number_input_controller.d.ts +2 -0
  96. package/dist/controllers/number_input_controller.js +47 -5
  97. package/dist/controllers/number_input_controller.js.map +1 -1
  98. package/dist/controllers/otp_controller.d.ts +3 -0
  99. package/dist/controllers/otp_controller.js.map +1 -1
  100. package/dist/controllers/overflow_indicator_controller.d.ts +2 -0
  101. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  102. package/dist/controllers/overflow_menu_controller.d.ts +2 -0
  103. package/dist/controllers/overflow_menu_controller.js +2 -1
  104. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  105. package/dist/controllers/pagination_controller.d.ts +2 -0
  106. package/dist/controllers/pagination_controller.js +2 -1
  107. package/dist/controllers/pagination_controller.js.map +1 -1
  108. package/dist/controllers/password_reveal_controller.d.ts +2 -0
  109. package/dist/controllers/password_reveal_controller.js.map +1 -1
  110. package/dist/controllers/password_strength_controller.d.ts +2 -0
  111. package/dist/controllers/password_strength_controller.js.map +1 -1
  112. package/dist/controllers/persist_controller.d.ts +30 -16
  113. package/dist/controllers/persist_controller.js +432 -122
  114. package/dist/controllers/persist_controller.js.map +1 -1
  115. package/dist/controllers/pointer_drag_controller.d.ts +3 -0
  116. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  117. package/dist/controllers/popover_controller.js +77 -4
  118. package/dist/controllers/popover_controller.js.map +1 -1
  119. package/dist/controllers/portal_controller.d.ts +2 -0
  120. package/dist/controllers/portal_controller.js.map +1 -1
  121. package/dist/controllers/preview_guard_controller.d.ts +2 -0
  122. package/dist/controllers/preview_guard_controller.js.map +1 -1
  123. package/dist/controllers/progress_controller.d.ts +3 -0
  124. package/dist/controllers/progress_controller.js.map +1 -1
  125. package/dist/controllers/radio_group_controller.d.ts +3 -0
  126. package/dist/controllers/radio_group_controller.js.map +1 -1
  127. package/dist/controllers/rating_controller.d.ts +2 -0
  128. package/dist/controllers/rating_controller.js +9 -5
  129. package/dist/controllers/rating_controller.js.map +1 -1
  130. package/dist/controllers/reading_progress_controller.d.ts +2 -0
  131. package/dist/controllers/reading_progress_controller.js.map +1 -1
  132. package/dist/controllers/resizable_controller.d.ts +2 -0
  133. package/dist/controllers/resizable_controller.js.map +1 -1
  134. package/dist/controllers/roving_controller.d.ts +2 -0
  135. package/dist/controllers/roving_controller.js.map +1 -1
  136. package/dist/controllers/scroll_area_controller.d.ts +23 -15
  137. package/dist/controllers/scroll_area_controller.js +462 -162
  138. package/dist/controllers/scroll_area_controller.js.map +1 -1
  139. package/dist/controllers/scroll_visibility_controller.d.ts +2 -0
  140. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  141. package/dist/controllers/scrollspy_controller.d.ts +2 -0
  142. package/dist/controllers/scrollspy_controller.js.map +1 -1
  143. package/dist/controllers/separator_controller.d.ts +54 -11
  144. package/dist/controllers/separator_controller.js +354 -38
  145. package/dist/controllers/separator_controller.js.map +1 -1
  146. package/dist/controllers/sidebar_controller.js +83 -10
  147. package/dist/controllers/sidebar_controller.js.map +1 -1
  148. package/dist/controllers/skeleton_controller.d.ts +2 -0
  149. package/dist/controllers/skeleton_controller.js.map +1 -1
  150. package/dist/controllers/slider_controller.d.ts +2 -0
  151. package/dist/controllers/slider_controller.js.map +1 -1
  152. package/dist/controllers/smart_sticky_header_controller.d.ts +2 -0
  153. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  154. package/dist/controllers/spinner_controller.d.ts +2 -0
  155. package/dist/controllers/spinner_controller.js.map +1 -1
  156. package/dist/controllers/step_indicator_controller.d.ts +2 -0
  157. package/dist/controllers/step_indicator_controller.js.map +1 -1
  158. package/dist/controllers/stepper_controller.d.ts +2 -0
  159. package/dist/controllers/stepper_controller.js.map +1 -1
  160. package/dist/controllers/stick_to_bottom_controller.d.ts +2 -0
  161. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  162. package/dist/controllers/sticky_observer_controller.d.ts +2 -0
  163. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  164. package/dist/controllers/submit_once_controller.d.ts +94 -38
  165. package/dist/controllers/submit_once_controller.js +399 -121
  166. package/dist/controllers/submit_once_controller.js.map +1 -1
  167. package/dist/controllers/switch_controller.d.ts +2 -0
  168. package/dist/controllers/switch_controller.js.map +1 -1
  169. package/dist/controllers/tags_input_controller.d.ts +2 -0
  170. package/dist/controllers/tags_input_controller.js.map +1 -1
  171. package/dist/controllers/textarea_autosize_controller.d.ts +2 -0
  172. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  173. package/dist/controllers/theme_controller.d.ts +2 -0
  174. package/dist/controllers/theme_controller.js +8 -6
  175. package/dist/controllers/theme_controller.js.map +1 -1
  176. package/dist/controllers/time_picker_controller.d.ts +2 -0
  177. package/dist/controllers/time_picker_controller.js.map +1 -1
  178. package/dist/controllers/toast_controller.d.ts +4 -0
  179. package/dist/controllers/toast_controller.js.map +1 -1
  180. package/dist/controllers/toggle_group_controller.d.ts +2 -0
  181. package/dist/controllers/toggle_group_controller.js.map +1 -1
  182. package/dist/controllers/toolbar_controller.js.map +1 -1
  183. package/dist/controllers/transition_controller.d.ts +2 -0
  184. package/dist/controllers/transition_controller.js.map +1 -1
  185. package/dist/controllers/tree_view_controller.d.ts +2 -0
  186. package/dist/controllers/tree_view_controller.js +2 -1
  187. package/dist/controllers/tree_view_controller.js.map +1 -1
  188. package/dist/index.js +2387 -861
  189. package/dist/index.js.map +1 -1
  190. package/dist/inspector/cli.d.ts +107 -3
  191. package/dist/inspector/cli.js +192 -14
  192. package/dist/inspector/cli.js.map +1 -1
  193. package/dist/inspector/cli_bin.js +254 -46
  194. package/dist/inspector/cli_bin.js.map +1 -1
  195. package/dist/inspector/examples.json +7 -7
  196. package/dist/inspector/manifest.json +434 -44
  197. package/dist/positioning/index.d.ts +2 -0
  198. package/dist/positioning/index.js.map +1 -1
  199. package/package.json +1 -1
@@ -1,12 +1,71 @@
1
1
  #!/usr/bin/env node
2
2
  import process2 from 'process';
3
- import { statSync, readdirSync, readFileSync } from 'fs';
3
+ import { readFileSync, statSync, readdirSync } from 'fs';
4
4
  import { relative, join, dirname, extname } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { createInterface } from 'readline';
7
7
 
8
8
  // src/inspector/cli_bin.ts
9
9
 
10
+ // src/inspector/announcer_rules.ts
11
+ var ANNOUNCER_IDENTIFIER = "stimeo--announcer";
12
+ var ANNOUNCING_CONTROLLERS = [
13
+ "stimeo--auto-submit",
14
+ "stimeo--character-counter",
15
+ "stimeo--countdown",
16
+ "stimeo--direct-upload",
17
+ "stimeo--empty-state",
18
+ "stimeo--flash",
19
+ "stimeo--form-field",
20
+ "stimeo--frame-loading",
21
+ "stimeo--meter",
22
+ "stimeo--multi-select",
23
+ "stimeo--nested-form",
24
+ "stimeo--network-status",
25
+ "stimeo--progress",
26
+ "stimeo--skeleton",
27
+ "stimeo--spinner",
28
+ "stimeo--submit-once",
29
+ "stimeo--tags-input"
30
+ ];
31
+ function positionAt(source, index) {
32
+ const before = source.slice(0, index);
33
+ const line = before.split("\n").length;
34
+ const lastBreak = before.lastIndexOf("\n");
35
+ return { line, column: index - lastBreak };
36
+ }
37
+ function firstAnnouncingHit(scanned) {
38
+ let best = null;
39
+ let bestIndex = Number.POSITIVE_INFINITY;
40
+ for (const identifier of ANNOUNCING_CONTROLLERS) {
41
+ const index = scanned.source.indexOf(identifier);
42
+ if (index === -1 || index >= bestIndex) continue;
43
+ bestIndex = index;
44
+ best = { file: scanned.file, identifier, ...positionAt(scanned.source, index) };
45
+ }
46
+ return best;
47
+ }
48
+ function announcerSeatingDiagnostic(sources) {
49
+ let speaker = null;
50
+ for (const scanned of sources) {
51
+ if (scanned.source.includes(ANNOUNCER_IDENTIFIER)) return null;
52
+ if (!speaker) speaker = firstAnnouncingHit(scanned);
53
+ }
54
+ if (!speaker) return null;
55
+ return {
56
+ file: speaker.file,
57
+ diagnostic: {
58
+ code: "missing-announcer",
59
+ severity: "warning",
60
+ message: `"${speaker.identifier}" speaks through the shared announcer, and none of the scanned files seats one.`,
61
+ line: speaker.line,
62
+ column: speaker.column,
63
+ length: speaker.identifier.length,
64
+ suggestion: `Add an element with data-controller="${ANNOUNCER_IDENTIFIER}" (and an assertive/polite target) to the layout, or include the layout in the checked paths.`
65
+ }
66
+ };
67
+ }
68
+
10
69
  // src/inspector/html_parser.ts
11
70
  var ERB_ELEMENT_TAG = "#erb";
12
71
  var VOID_ELEMENTS = /* @__PURE__ */ new Set([
@@ -932,10 +991,16 @@ function actionDescriptors(dataActionValue) {
932
991
  if (hash === -1) continue;
933
992
  let lhs = descriptor.slice(0, hash);
934
993
  const arrow = lhs.indexOf("->");
935
- if (arrow !== -1) lhs = lhs.slice(arrow + 2);
994
+ let eventType = "";
995
+ if (arrow !== -1) {
996
+ eventType = lhs.slice(0, arrow);
997
+ lhs = lhs.slice(arrow + 2);
998
+ const at = eventType.indexOf("@");
999
+ if (at !== -1) eventType = eventType.slice(0, at);
1000
+ }
936
1001
  if (!lhs.startsWith(NAMESPACE_PREFIX)) continue;
937
1002
  const method = /^[a-zA-Z_$][\w$]*/.exec(descriptor.slice(hash + 1))?.[0] ?? "";
938
- descriptors.push({ identifier: lhs, method });
1003
+ descriptors.push({ identifier: lhs, method, eventType });
939
1004
  }
940
1005
  return descriptors;
941
1006
  }
@@ -954,6 +1019,8 @@ var DIAGNOSTIC_CODES = [
954
1019
  "orphan-target",
955
1020
  "missing-required-target",
956
1021
  "missing-conditional-target",
1022
+ "missing-required-action",
1023
+ "missing-action-completion",
957
1024
  "missing-aria",
958
1025
  "invalid-aria-value",
959
1026
  "keyboard-inaccessible",
@@ -965,6 +1032,7 @@ var DIAGNOSTIC_CODES = [
965
1032
  "undeclared-target",
966
1033
  "cardinality-violation",
967
1034
  "forbidden-aria",
1035
+ "missing-announcer",
968
1036
  "unknown-ignore-code"
969
1037
  ];
970
1038
 
@@ -975,6 +1043,19 @@ function isInside(node, ancestor) {
975
1043
  }
976
1044
  return false;
977
1045
  }
1046
+ function matchesHost(node, selector) {
1047
+ if (node.tag !== selector.tag) return false;
1048
+ if (!selector.attr) return true;
1049
+ const attr = node.attrs.find((candidate) => candidate.name === selector.attr);
1050
+ return attr !== void 0 && (selector.values ?? []).includes(attr.value.trim().toLowerCase());
1051
+ }
1052
+ function nearestHost(node, selectors) {
1053
+ for (let current = node.parent; current; current = current.parent) {
1054
+ if (current.origin !== "markup" || current.opaque) return void 0;
1055
+ if (selectors.some((selector) => matchesHost(current, selector))) return current;
1056
+ }
1057
+ return null;
1058
+ }
978
1059
  function checkSource(source, manifest) {
979
1060
  const diagnostics = [];
980
1061
  const tree = parseHtml(neutralizeErb(source), erbElements(source));
@@ -993,7 +1074,9 @@ function checkSource(source, manifest) {
993
1074
  (a) => a.name === `data-${identifier}-${dasherize(when.value)}-value`
994
1075
  );
995
1076
  if (hostAttr && isDynamicValue(hostAttr)) return null;
996
- return when.equals.includes(hostAttr ? hostAttr.value.trim() : when.default);
1077
+ const raw = hostAttr ? hostAttr.value : when.default;
1078
+ const effective = when.type === "boolean" ? String(!(raw === "0" || raw.toLowerCase() === "false")) : raw;
1079
+ return when.equals.includes(effective);
997
1080
  };
998
1081
  const isContentEditable2 = (element) => {
999
1082
  let current = element;
@@ -1066,6 +1149,17 @@ function checkSource(source, manifest) {
1066
1149
  const scopes = [];
1067
1150
  const presence = /* @__PURE__ */ new Map();
1068
1151
  const targetNodes = /* @__PURE__ */ new Map();
1152
+ const wiredActions = /* @__PURE__ */ new Map();
1153
+ const recordAction = (owner, identifier, element, method, eventType) => {
1154
+ let byId = wiredActions.get(owner);
1155
+ if (!byId) {
1156
+ byId = /* @__PURE__ */ new Map();
1157
+ wiredActions.set(owner, byId);
1158
+ }
1159
+ const found = byId.get(identifier);
1160
+ if (found) found.push({ element, method, eventType });
1161
+ else byId.set(identifier, [{ element, method, eventType }]);
1162
+ };
1069
1163
  const recordPresence = (owner, identifier, target) => {
1070
1164
  let byId = presence.get(owner);
1071
1165
  if (!byId) {
@@ -1274,11 +1368,12 @@ function checkSource(source, manifest) {
1274
1368
  (candidate) => dasherize(candidate.value) === parsed.valueToken
1275
1369
  );
1276
1370
  if (constraint && !valueSatisfiesConstraint(attr.value, constraint)) {
1371
+ const literal = constraint.type === "string" ? attr.value : attr.value.trim();
1277
1372
  report(
1278
1373
  node,
1279
1374
  "invalid-value",
1280
1375
  "error",
1281
- `Invalid value "${attr.value.trim()}" for "${parsed.identifier}.${parsed.valueToken}". Expected ${describeValueConstraint(constraint)}.`,
1376
+ `Invalid value "${literal}" for "${parsed.identifier}.${parsed.valueToken}". Expected ${describeValueConstraint(constraint)}.`,
1282
1377
  attr,
1283
1378
  constraint.suggestion
1284
1379
  );
@@ -1289,8 +1384,12 @@ function checkSource(source, manifest) {
1289
1384
  }
1290
1385
  }
1291
1386
  if (attr.name === "data-action") {
1292
- for (const { identifier, method } of actionDescriptors(attr.value)) {
1387
+ for (const { identifier, method, eventType } of actionDescriptors(attr.value)) {
1293
1388
  const controller = known[identifier];
1389
+ if (controller && method.length > 0) {
1390
+ const owner = findOwner(node, identifier);
1391
+ if (owner) recordAction(owner, identifier, node, method, eventType);
1392
+ }
1294
1393
  if (!controller) {
1295
1394
  const best = nearestName(identifier, knownIdentifiers);
1296
1395
  report(
@@ -1374,19 +1473,87 @@ function checkSource(source, manifest) {
1374
1473
  if (!present.has(rule.whenPresent)) continue;
1375
1474
  const nodesOf = (target) => targetNodes.get(node)?.get(identifier)?.get(target) ?? [];
1376
1475
  const owners = nodesOf(rule.whenPresent);
1377
- const satisfied = (target) => rule.requireInside ? nodesOf(target).some((el) => owners.some((owner) => isInside(el, owner))) : present.has(target);
1476
+ const hosts = rule.requireSameHost;
1477
+ const sharesHost = (target, selectors) => {
1478
+ const ownerHosts = owners.map((owner) => nearestHost(owner, selectors));
1479
+ if (ownerHosts.some((host) => !host)) return true;
1480
+ const candidates = nodesOf(target);
1481
+ if (candidates.length === 0) return false;
1482
+ return candidates.some((el) => {
1483
+ const host = nearestHost(el, selectors);
1484
+ return host ? ownerHosts.includes(host) : true;
1485
+ });
1486
+ };
1487
+ const satisfied = (target) => {
1488
+ if (hosts) return sharesHost(target, hosts);
1489
+ if (rule.requireInside)
1490
+ return nodesOf(target).some((el) => owners.some((owner) => isInside(el, owner)));
1491
+ return present.has(target);
1492
+ };
1378
1493
  const missing = rule.require.filter((target) => !satisfied(target));
1379
1494
  if (missing.length === 0) continue;
1380
1495
  report(
1381
1496
  node,
1382
1497
  "missing-conditional-target",
1383
1498
  complete ? "error" : "warning",
1384
- rule.requireInside ? `"${identifier}" requires ${list(missing)} inside its "${rule.whenPresent}".` : `"${identifier}" has a "${rule.whenPresent}" target, which also requires ${list(missing)}.`,
1499
+ hosts ? `"${identifier}" requires ${list(missing)} in the same ${rule.hostLabel ?? "element"} as its "${rule.whenPresent}".` : rule.requireInside ? `"${identifier}" requires ${list(missing)} inside its "${rule.whenPresent}".` : `"${identifier}" has a "${rule.whenPresent}" target, which also requires ${list(missing)}.`,
1385
1500
  node,
1386
1501
  complete ? rule.suggestion : UNRESOLVED_TARGET_HINT
1387
1502
  );
1388
1503
  }
1389
1504
  }
1505
+ for (const { node, identifier } of scopes) {
1506
+ const controller = known[identifier];
1507
+ if (!controller) continue;
1508
+ const wired = wiredActions.get(node)?.get(identifier) ?? [];
1509
+ for (const rule of controller.requiredActions) {
1510
+ if (conditionHolds(node, identifier, rule.when) !== true) continue;
1511
+ const elements = rule.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(rule.target) ?? [];
1512
+ for (const element of elements) {
1513
+ const actionAttr = element.attrs.find((attr) => attr.name === "data-action");
1514
+ if (element.opaque || actionAttr && isDynamicValue(actionAttr)) continue;
1515
+ const satisfied = wired.some(
1516
+ (action) => action.element === element && action.method === rule.action && rule.eventTypes.includes(action.eventType)
1517
+ );
1518
+ if (satisfied) continue;
1519
+ const where = rule.target === "" ? "scope element" : `"${rule.target}" target`;
1520
+ report(
1521
+ element,
1522
+ "missing-required-action",
1523
+ "error",
1524
+ `The ${where} of "${identifier}" requires action "${rule.action}" on ${list(rule.eventTypes)} in this configuration.`,
1525
+ element,
1526
+ rule.suggestion
1527
+ );
1528
+ }
1529
+ }
1530
+ }
1531
+ for (const { node, identifier } of scopes) {
1532
+ const controller = known[identifier];
1533
+ if (!controller) continue;
1534
+ const wired = wiredActions.get(node)?.get(identifier) ?? [];
1535
+ if (wired.length === 0) continue;
1536
+ for (const rule of controller.actionCompletion) {
1537
+ const opens = wired.some(
1538
+ (action) => action.method === rule.opens && rule.whenTriggeredBy.includes(action.eventType)
1539
+ );
1540
+ if (!opens) continue;
1541
+ if (wired.some((action) => rule.closedBy.includes(action.method))) continue;
1542
+ if (rule.escapeValue) {
1543
+ const attrName = `data-${identifier}-${dasherize(rule.escapeValue)}-value`;
1544
+ const authored = node.attrs.find((attr) => attr.name === attrName);
1545
+ if (authored && (isDynamicValue(authored) || Number(authored.value.trim()) !== 0)) continue;
1546
+ }
1547
+ report(
1548
+ node,
1549
+ "missing-action-completion",
1550
+ "warning",
1551
+ `"${identifier}" opens a state with "${rule.opens}" here, and nothing in this scope closes it.`,
1552
+ node,
1553
+ rule.suggestion
1554
+ );
1555
+ }
1556
+ }
1390
1557
  for (const { node, identifier } of scopes) {
1391
1558
  const controller = known[identifier];
1392
1559
  if (!controller) continue;
@@ -1689,6 +1856,7 @@ function checkSource(source, manifest) {
1689
1856
  return diagnostics;
1690
1857
  }
1691
1858
  function valueSatisfiesConstraint(raw, constraint) {
1859
+ if (constraint.type === "string") return constraint.allowedValues.includes(raw);
1692
1860
  const value = decodeNumericValue(raw);
1693
1861
  if (Number.isNaN(value)) return false;
1694
1862
  if (constraint.finite && !Number.isFinite(value)) return false;
@@ -1700,6 +1868,7 @@ function decodeNumericValue(raw) {
1700
1868
  return Number(raw.replace(/_/g, ""));
1701
1869
  }
1702
1870
  function describeValueConstraint(constraint) {
1871
+ if (constraint.type === "string") return `one of ${list(constraint.allowedValues)}`;
1703
1872
  const parts = [constraint.finite ? "a finite number" : "a number"];
1704
1873
  if (constraint.greaterThan !== void 0) {
1705
1874
  parts.push(`greater than ${constraint.greaterThan}`);
@@ -1954,6 +2123,8 @@ var CONTROLLER_ARRAY_FIELDS = [
1954
2123
  "events",
1955
2124
  "requiredTargets",
1956
2125
  "conditionalTargets",
2126
+ "requiredActions",
2127
+ "actionCompletion",
1957
2128
  "a11y",
1958
2129
  "keyboard",
1959
2130
  "hosts",
@@ -2016,10 +2187,16 @@ function collectFiles(target, out) {
2016
2187
  }
2017
2188
  if (isCheckableFile(target)) out.push(target);
2018
2189
  }
2019
- function checkFile(file, manifest) {
2020
- const source = readFileSync(file, "utf8");
2190
+ function checkFile(file, source, manifest) {
2021
2191
  return { file, diagnostics: checkSource(source, manifest) };
2022
2192
  }
2193
+ function withAnnouncerSeating(reports, sources) {
2194
+ const seating = announcerSeatingDiagnostic(sources);
2195
+ if (!seating) return [...reports];
2196
+ return reports.map(
2197
+ (report) => report.file === seating.file ? { file: report.file, diagnostics: [...report.diagnostics, seating.diagnostic] } : report
2198
+ );
2199
+ }
2023
2200
  function formatDiagnostic(d) {
2024
2201
  const where = `${d.line}:${d.column}`.padEnd(7);
2025
2202
  const head = ` ${where} ${d.severity.padEnd(7)} ${d.message} [${d.code}]`;
@@ -2152,13 +2329,14 @@ ${USAGE}`);
2152
2329
  const manifest = load();
2153
2330
  const cwd = process2.cwd();
2154
2331
  const reports = [];
2332
+ const sources = [];
2155
2333
  for (const file of files.sort()) {
2156
- reports.push({
2157
- file: relative(cwd, file) || file,
2158
- diagnostics: checkFile(file, manifest).diagnostics
2159
- });
2334
+ const path = relative(cwd, file) || file;
2335
+ const source = readFileSync(file, "utf8");
2336
+ sources.push({ file: path, source });
2337
+ reports.push(checkFile(path, source, manifest));
2160
2338
  }
2161
- const summary = buildCheckReport(reports, files.length);
2339
+ const summary = buildCheckReport(withAnnouncerSeating(reports, sources), files.length);
2162
2340
  if (json) {
2163
2341
  write(JSON.stringify(summary, null, 2));
2164
2342
  return summary.errorCount > 0 ? 1 : 0;
@@ -2388,6 +2566,17 @@ var EXAMPLE_RESULT_SCHEMA = {
2388
2566
  },
2389
2567
  required: ["id", "file", "source", "guidance"]
2390
2568
  };
2569
+ var VALUE_CONDITION_SCHEMA = {
2570
+ type: "object",
2571
+ description: "Own-value condition arming a rule; absent means unconditional.",
2572
+ properties: {
2573
+ value: { type: "string" },
2574
+ type: { type: "string", enum: ["boolean", "string"] },
2575
+ equals: STRING_ARRAY,
2576
+ default: { type: "string" }
2577
+ },
2578
+ required: ["value", "type", "equals", "default"]
2579
+ };
2391
2580
  var CONTENT_CONDITION_SCHEMA = {
2392
2581
  type: "object",
2393
2582
  description: "Condition on the element's own contents arming the rule; absent means it does not depend on them. Evaluated per element, unlike the scope-level `when`.",
@@ -2436,9 +2625,14 @@ var CONTROLLER_CONTRACT_SCHEMA = {
2436
2625
  type: "object",
2437
2626
  properties: {
2438
2627
  value: { type: "string", description: "Camel-case declared Value name." },
2439
- type: { type: "string", enum: ["number"] },
2628
+ type: { type: "string", enum: ["number", "string"] },
2440
2629
  finite: { type: "boolean" },
2441
2630
  greaterThan: { type: "number" },
2631
+ integer: { type: "boolean" },
2632
+ allowedValues: {
2633
+ ...STRING_ARRAY,
2634
+ description: "Accepted literals for a String Value constraint."
2635
+ },
2442
2636
  suggestion: { type: "string" }
2443
2637
  },
2444
2638
  required: ["value", "type", "suggestion"]
@@ -2479,6 +2673,45 @@ var CONTROLLER_CONTRACT_SCHEMA = {
2479
2673
  required: ["whenPresent", "require", "suggestion"]
2480
2674
  }
2481
2675
  },
2676
+ requiredActions: {
2677
+ type: "array",
2678
+ description: "Action bindings required on a particular element in one configured Value state.",
2679
+ items: {
2680
+ type: "object",
2681
+ properties: {
2682
+ target: { type: "string", description: 'Target name; "" means the scope element.' },
2683
+ action: { type: "string", description: "Public action method that must be wired." },
2684
+ eventTypes: {
2685
+ ...STRING_ARRAY,
2686
+ description: "Event types that make the binding usable for this contract."
2687
+ },
2688
+ when: VALUE_CONDITION_SCHEMA,
2689
+ suggestion: { type: "string" }
2690
+ },
2691
+ required: ["target", "action", "eventTypes", "suggestion"]
2692
+ }
2693
+ },
2694
+ actionCompletion: {
2695
+ type: "array",
2696
+ description: "States an action opens that the consumer's own wiring has to close. Wiring the opening action without any closing one leaves the controller busy for good, and nothing else in the markup reveals it.",
2697
+ items: {
2698
+ type: "object",
2699
+ properties: {
2700
+ opens: { type: "string", description: "Action that opens the state." },
2701
+ whenTriggeredBy: {
2702
+ ...STRING_ARRAY,
2703
+ description: "Event types for that action the controller cannot complete itself."
2704
+ },
2705
+ closedBy: { ...STRING_ARRAY, description: "Any one of these closes it again." },
2706
+ escapeValue: {
2707
+ type: "string",
2708
+ description: "Value whose non-zero number supplies an automatic exit instead."
2709
+ },
2710
+ suggestion: { type: "string" }
2711
+ },
2712
+ required: ["opens", "whenTriggeredBy", "closedBy", "suggestion"]
2713
+ }
2714
+ },
2482
2715
  a11y: {
2483
2716
  type: "array",
2484
2717
  description: "Author-supplied ARIA the controller relies on but never sets itself.",
@@ -2500,16 +2733,7 @@ var CONTROLLER_CONTRACT_SCHEMA = {
2500
2733
  required: ["attrs"]
2501
2734
  }
2502
2735
  },
2503
- when: {
2504
- type: "object",
2505
- description: "Own-value condition arming the requirement; absent means unconditional. Outside the named configuration the ARIA is not optional but wrong.",
2506
- properties: {
2507
- value: { type: "string" },
2508
- equals: STRING_ARRAY,
2509
- default: { type: "string" }
2510
- },
2511
- required: ["value", "equals", "default"]
2512
- },
2736
+ when: VALUE_CONDITION_SCHEMA,
2513
2737
  whenContains: CONTENT_CONDITION_SCHEMA,
2514
2738
  whenElement: ELEMENT_CONDITION_SCHEMA,
2515
2739
  whenDocument: DOCUMENT_CONDITION_SCHEMA,
@@ -2593,16 +2817,7 @@ var CONTROLLER_CONTRACT_SCHEMA = {
2593
2817
  description: "Check the scope element when the target is absent."
2594
2818
  },
2595
2819
  coController: { type: "string", description: "Companion controller identifier." },
2596
- when: {
2597
- type: "object",
2598
- description: "Host-value condition arming the rule; absent means unconditional.",
2599
- properties: {
2600
- value: { type: "string" },
2601
- equals: STRING_ARRAY,
2602
- default: { type: "string" }
2603
- },
2604
- required: ["value", "equals", "default"]
2605
- },
2820
+ when: VALUE_CONDITION_SCHEMA,
2606
2821
  require: {
2607
2822
  type: "object",
2608
2823
  description: "Companion-value requirement (first allowed value is canonical).",
@@ -2666,16 +2881,7 @@ var CONTROLLER_CONTRACT_SCHEMA = {
2666
2881
  values: { ...STRING_ARRAY, description: "Restrict the attribute to these values." },
2667
2882
  min: { type: "integer", description: "Smallest permitted count (inclusive)." },
2668
2883
  max: { type: "integer", description: "Largest permitted count (inclusive)." },
2669
- when: {
2670
- type: "object",
2671
- description: "Own-value condition arming the bound; absent means unconditional.",
2672
- properties: {
2673
- value: { type: "string" },
2674
- equals: STRING_ARRAY,
2675
- default: { type: "string" }
2676
- },
2677
- required: ["value", "equals", "default"]
2678
- },
2884
+ when: VALUE_CONDITION_SCHEMA,
2679
2885
  suggestion: { type: "string" }
2680
2886
  },
2681
2887
  required: ["within", "target", "suggestion"]
@@ -2707,6 +2913,8 @@ var CONTROLLER_CONTRACT_SCHEMA = {
2707
2913
  "events",
2708
2914
  "requiredTargets",
2709
2915
  "conditionalTargets",
2916
+ "requiredActions",
2917
+ "actionCompletion",
2710
2918
  "a11y",
2711
2919
  "keyboard",
2712
2920
  "hosts",