stimeo-ui 0.4.0 → 0.6.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 (173) hide show
  1. package/CHANGELOG.md +184 -0
  2. package/dist/controllers/announcer_controller.js.map +1 -1
  3. package/dist/controllers/aspect_ratio_controller.d.ts +3 -5
  4. package/dist/controllers/aspect_ratio_controller.js +19 -11
  5. package/dist/controllers/aspect_ratio_controller.js.map +1 -1
  6. package/dist/controllers/avatar_controller.d.ts +36 -15
  7. package/dist/controllers/avatar_controller.js +195 -40
  8. package/dist/controllers/avatar_controller.js.map +1 -1
  9. package/dist/controllers/breadcrumb_controller.js +5 -1
  10. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  11. package/dist/controllers/carousel_controller.d.ts +12 -3
  12. package/dist/controllers/carousel_controller.js +90 -10
  13. package/dist/controllers/carousel_controller.js.map +1 -1
  14. package/dist/controllers/checkbox_controller.d.ts +31 -5
  15. package/dist/controllers/checkbox_controller.js +136 -25
  16. package/dist/controllers/checkbox_controller.js.map +1 -1
  17. package/dist/controllers/clipboard_controller.js +8 -3
  18. package/dist/controllers/clipboard_controller.js.map +1 -1
  19. package/dist/controllers/collapsible_controller.d.ts +1 -1
  20. package/dist/controllers/collapsible_controller.js +4 -1
  21. package/dist/controllers/collapsible_controller.js.map +1 -1
  22. package/dist/controllers/color_picker_controller.d.ts +9 -3
  23. package/dist/controllers/color_picker_controller.js +41 -11
  24. package/dist/controllers/color_picker_controller.js.map +1 -1
  25. package/dist/controllers/combobox_controller.js.map +1 -1
  26. package/dist/controllers/command_palette_controller.js.map +1 -1
  27. package/dist/controllers/context_menu_controller.d.ts +1 -1
  28. package/dist/controllers/context_menu_controller.js +2 -2
  29. package/dist/controllers/context_menu_controller.js.map +1 -1
  30. package/dist/controllers/countdown_controller.d.ts +5 -3
  31. package/dist/controllers/countdown_controller.js +5 -1
  32. package/dist/controllers/countdown_controller.js.map +1 -1
  33. package/dist/controllers/date_range_picker_controller.d.ts +23 -4
  34. package/dist/controllers/date_range_picker_controller.js +162 -31
  35. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  36. package/dist/controllers/direct_upload_controller.d.ts +1 -1
  37. package/dist/controllers/direct_upload_controller.js +3 -3
  38. package/dist/controllers/direct_upload_controller.js.map +1 -1
  39. package/dist/controllers/empty_state_controller.d.ts +27 -7
  40. package/dist/controllers/empty_state_controller.js +107 -16
  41. package/dist/controllers/empty_state_controller.js.map +1 -1
  42. package/dist/controllers/file_dropzone_controller.d.ts +7 -1
  43. package/dist/controllers/file_dropzone_controller.js +26 -3
  44. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  45. package/dist/controllers/flash_controller.d.ts +25 -5
  46. package/dist/controllers/flash_controller.js +161 -21
  47. package/dist/controllers/flash_controller.js.map +1 -1
  48. package/dist/controllers/focus_controller.d.ts +2 -3
  49. package/dist/controllers/focus_controller.js.map +1 -1
  50. package/dist/controllers/form_validation_controller.js +8 -2
  51. package/dist/controllers/form_validation_controller.js.map +1 -1
  52. package/dist/controllers/frame_loading_controller.d.ts +19 -2
  53. package/dist/controllers/frame_loading_controller.js +94 -22
  54. package/dist/controllers/frame_loading_controller.js.map +1 -1
  55. package/dist/controllers/highlight_controller.d.ts +8 -4
  56. package/dist/controllers/highlight_controller.js +38 -1
  57. package/dist/controllers/highlight_controller.js.map +1 -1
  58. package/dist/controllers/hover_card_controller.d.ts +2 -2
  59. package/dist/controllers/hover_card_controller.js.map +1 -1
  60. package/dist/controllers/idle_controller.d.ts +5 -4
  61. package/dist/controllers/idle_controller.js +39 -6
  62. package/dist/controllers/idle_controller.js.map +1 -1
  63. package/dist/controllers/listbox_controller.js.map +1 -1
  64. package/dist/controllers/local_time_controller.js +2 -0
  65. package/dist/controllers/local_time_controller.js.map +1 -1
  66. package/dist/controllers/masonry_controller.d.ts +1 -1
  67. package/dist/controllers/masonry_controller.js +1 -1
  68. package/dist/controllers/masonry_controller.js.map +1 -1
  69. package/dist/controllers/menubar_controller.js +5 -3
  70. package/dist/controllers/menubar_controller.js.map +1 -1
  71. package/dist/controllers/meter_controller.js +3 -1
  72. package/dist/controllers/meter_controller.js.map +1 -1
  73. package/dist/controllers/multi_select_controller.d.ts +46 -12
  74. package/dist/controllers/multi_select_controller.js +460 -151
  75. package/dist/controllers/multi_select_controller.js.map +1 -1
  76. package/dist/controllers/network_status_controller.d.ts +15 -13
  77. package/dist/controllers/network_status_controller.js +1 -3
  78. package/dist/controllers/network_status_controller.js.map +1 -1
  79. package/dist/controllers/number_input_controller.d.ts +29 -3
  80. package/dist/controllers/number_input_controller.js +455 -64
  81. package/dist/controllers/number_input_controller.js.map +1 -1
  82. package/dist/controllers/overflow_menu_controller.js +5 -1
  83. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  84. package/dist/controllers/pagination_controller.js +38 -1
  85. package/dist/controllers/pagination_controller.js.map +1 -1
  86. package/dist/controllers/password_strength_controller.d.ts +4 -4
  87. package/dist/controllers/password_strength_controller.js +21 -3
  88. package/dist/controllers/password_strength_controller.js.map +1 -1
  89. package/dist/controllers/persist_controller.d.ts +4 -4
  90. package/dist/controllers/persist_controller.js +24 -5
  91. package/dist/controllers/persist_controller.js.map +1 -1
  92. package/dist/controllers/pointer_drag_controller.js +10 -0
  93. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  94. package/dist/controllers/popover_controller.d.ts +2 -2
  95. package/dist/controllers/popover_controller.js.map +1 -1
  96. package/dist/controllers/portal_controller.d.ts +1 -2
  97. package/dist/controllers/portal_controller.js +10 -0
  98. package/dist/controllers/portal_controller.js.map +1 -1
  99. package/dist/controllers/progress_controller.d.ts +1 -1
  100. package/dist/controllers/progress_controller.js +7 -3
  101. package/dist/controllers/progress_controller.js.map +1 -1
  102. package/dist/controllers/radio_group_controller.d.ts +41 -15
  103. package/dist/controllers/radio_group_controller.js +540 -56
  104. package/dist/controllers/radio_group_controller.js.map +1 -1
  105. package/dist/controllers/range_slider_controller.d.ts +23 -0
  106. package/dist/controllers/range_slider_controller.js +385 -94
  107. package/dist/controllers/range_slider_controller.js.map +1 -1
  108. package/dist/controllers/rating_controller.d.ts +36 -31
  109. package/dist/controllers/rating_controller.js +274 -89
  110. package/dist/controllers/rating_controller.js.map +1 -1
  111. package/dist/controllers/relative_time_controller.js +2 -0
  112. package/dist/controllers/relative_time_controller.js.map +1 -1
  113. package/dist/controllers/resizable_controller.js +33 -0
  114. package/dist/controllers/resizable_controller.js.map +1 -1
  115. package/dist/controllers/roving_controller.d.ts +4 -0
  116. package/dist/controllers/roving_controller.js +60 -5
  117. package/dist/controllers/roving_controller.js.map +1 -1
  118. package/dist/controllers/scroll_area_controller.d.ts +7 -1
  119. package/dist/controllers/scroll_area_controller.js +155 -23
  120. package/dist/controllers/scroll_area_controller.js.map +1 -1
  121. package/dist/controllers/scroll_visibility_controller.js +33 -0
  122. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  123. package/dist/controllers/separator_controller.js +13 -17
  124. package/dist/controllers/separator_controller.js.map +1 -1
  125. package/dist/controllers/skeleton_controller.d.ts +2 -2
  126. package/dist/controllers/skeleton_controller.js +71 -3
  127. package/dist/controllers/skeleton_controller.js.map +1 -1
  128. package/dist/controllers/slider_controller.d.ts +17 -1
  129. package/dist/controllers/slider_controller.js +325 -48
  130. package/dist/controllers/slider_controller.js.map +1 -1
  131. package/dist/controllers/spinner_controller.d.ts +15 -10
  132. package/dist/controllers/spinner_controller.js +18 -3
  133. package/dist/controllers/spinner_controller.js.map +1 -1
  134. package/dist/controllers/step_indicator_controller.d.ts +1 -1
  135. package/dist/controllers/step_indicator_controller.js +3 -1
  136. package/dist/controllers/step_indicator_controller.js.map +1 -1
  137. package/dist/controllers/stepper_controller.js +2 -0
  138. package/dist/controllers/stepper_controller.js.map +1 -1
  139. package/dist/controllers/switch_controller.d.ts +12 -8
  140. package/dist/controllers/switch_controller.js +162 -18
  141. package/dist/controllers/switch_controller.js.map +1 -1
  142. package/dist/controllers/tags_input_controller.d.ts +41 -11
  143. package/dist/controllers/tags_input_controller.js +356 -120
  144. package/dist/controllers/tags_input_controller.js.map +1 -1
  145. package/dist/controllers/textarea_autosize_controller.js +1 -1
  146. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  147. package/dist/controllers/time_picker_controller.d.ts +42 -10
  148. package/dist/controllers/time_picker_controller.js +296 -104
  149. package/dist/controllers/time_picker_controller.js.map +1 -1
  150. package/dist/controllers/toggle_group_controller.d.ts +39 -13
  151. package/dist/controllers/toggle_group_controller.js +378 -55
  152. package/dist/controllers/toggle_group_controller.js.map +1 -1
  153. package/dist/controllers/toolbar_controller.js +5 -3
  154. package/dist/controllers/toolbar_controller.js.map +1 -1
  155. package/dist/controllers/tooltip_controller.d.ts +2 -2
  156. package/dist/controllers/tooltip_controller.js.map +1 -1
  157. package/dist/controllers/transition_controller.d.ts +2 -3
  158. package/dist/controllers/transition_controller.js.map +1 -1
  159. package/dist/controllers/tree_view_controller.d.ts +1 -2
  160. package/dist/controllers/tree_view_controller.js +24 -4
  161. package/dist/controllers/tree_view_controller.js.map +1 -1
  162. package/dist/index.js +3811 -1048
  163. package/dist/index.js.map +1 -1
  164. package/dist/inspector/cli.d.ts +92 -4
  165. package/dist/inspector/cli.js +174 -6
  166. package/dist/inspector/cli.js.map +1 -1
  167. package/dist/inspector/cli_bin.js +234 -8
  168. package/dist/inspector/cli_bin.js.map +1 -1
  169. package/dist/inspector/examples.json +24 -24
  170. package/dist/inspector/manifest.json +573 -59
  171. package/dist/positioning/index.d.ts +2 -2
  172. package/dist/positioning/index.js.map +1 -1
  173. package/package.json +3 -3
@@ -612,6 +612,26 @@ function readEntry(code, start, base) {
612
612
  };
613
613
  }
614
614
  }
615
+ const number = /^[+-]?\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?/.exec(
616
+ code.slice(valueStart)
617
+ )?.[0];
618
+ if (number !== void 0) {
619
+ const end = valueStart + number.length;
620
+ const after = skipTrivia(code, end);
621
+ const whole = after >= code.length || code[after] === "," || code[after] === "}";
622
+ if (whole) {
623
+ return {
624
+ attr: {
625
+ name,
626
+ value: number,
627
+ ...anchor,
628
+ valueStart: base + valueStart,
629
+ valueEnd: base + end
630
+ },
631
+ end
632
+ };
633
+ }
634
+ }
615
635
  return { attr: { name, value: null, ...anchor }, end: skipValue(code, valueStart) };
616
636
  }
617
637
  function readKey(code, start) {
@@ -928,6 +948,7 @@ var DIAGNOSTIC_CODES = [
928
948
  "unknown-controller",
929
949
  "unknown-target",
930
950
  "unknown-value",
951
+ "invalid-value",
931
952
  "unknown-action-controller",
932
953
  "unknown-action-method",
933
954
  "orphan-target",
@@ -936,6 +957,7 @@ var DIAGNOSTIC_CODES = [
936
957
  "missing-aria",
937
958
  "invalid-aria-value",
938
959
  "keyboard-inaccessible",
960
+ "invalid-host",
939
961
  "unresolved-idref",
940
962
  "managed-aria",
941
963
  "composition-mismatch",
@@ -947,6 +969,12 @@ var DIAGNOSTIC_CODES = [
947
969
  ];
948
970
 
949
971
  // src/inspector/check.ts
972
+ function isInside(node, ancestor) {
973
+ for (let current = node.parent; current; current = current.parent) {
974
+ if (current === ancestor) return true;
975
+ }
976
+ return false;
977
+ }
950
978
  function checkSource(source, manifest) {
951
979
  const diagnostics = [];
952
980
  const tree = parseHtml(neutralizeErb(source), erbElements(source));
@@ -967,6 +995,36 @@ function checkSource(source, manifest) {
967
995
  if (hostAttr && isDynamicValue(hostAttr)) return null;
968
996
  return when.equals.includes(hostAttr ? hostAttr.value.trim() : when.default);
969
997
  };
998
+ const isContentEditable2 = (element) => {
999
+ let current = element;
1000
+ while (current && current.tag !== "#root") {
1001
+ const attr = current.attrs.find((candidate) => candidate.name === "contenteditable");
1002
+ if (attr) {
1003
+ if (isDynamicValue(attr)) return null;
1004
+ const value = attr.value.trim().toLowerCase();
1005
+ if (value === "false") return false;
1006
+ if (value === "" || value === "true" || value === "plaintext-only") return true;
1007
+ }
1008
+ current = current.parent;
1009
+ }
1010
+ return false;
1011
+ };
1012
+ const hostIsAllowed = (element, rule) => {
1013
+ if (element.tag === "button") {
1014
+ if (rule.mode === "non-interactive") return false;
1015
+ const type = element.attrs.find((attr) => attr.name === "type");
1016
+ if (type && isDynamicValue(type)) return null;
1017
+ const effectiveType = type?.value.trim().toLowerCase() || "submit";
1018
+ return (rule.buttonTypes ?? []).includes(effectiveType);
1019
+ }
1020
+ if (ALWAYS_INTERACTIVE_HOSTS.has(element.tag)) return false;
1021
+ if ((element.tag === "a" || element.tag === "area") && hasAttr(element, "href")) return false;
1022
+ if ((element.tag === "audio" || element.tag === "video") && hasAttr(element, "controls")) {
1023
+ return false;
1024
+ }
1025
+ const editable = isContentEditable2(element);
1026
+ return editable === null ? null : !editable;
1027
+ };
970
1028
  const suppressed = (node, code) => {
971
1029
  if (code === "unknown-ignore-code") return false;
972
1030
  let current = node;
@@ -1074,6 +1132,7 @@ function checkSource(source, manifest) {
1074
1132
  focusableRoleTally.set(role, (focusableRoleTally.get(role) ?? 0) + 1);
1075
1133
  }
1076
1134
  }
1135
+ const authoredValueIdentifiers = /* @__PURE__ */ new Set();
1077
1136
  for (const attr of node.attrs) {
1078
1137
  if (attr.name === IGNORE_ATTR) {
1079
1138
  for (const token of attr.value.split(/\s+/).filter((t) => t.length > 0)) {
@@ -1198,6 +1257,7 @@ function checkSource(source, manifest) {
1198
1257
  attr
1199
1258
  );
1200
1259
  } else {
1260
+ authoredValueIdentifiers.add(parsed.identifier);
1201
1261
  const controller = known[parsed.identifier];
1202
1262
  const valid = controller?.values.some((v) => dasherize(v) === parsed.valueToken) ?? false;
1203
1263
  if (!valid) {
@@ -1209,6 +1269,20 @@ function checkSource(source, manifest) {
1209
1269
  attr,
1210
1270
  didYouMean(parsed.valueToken, (controller?.values ?? []).map(dasherize))
1211
1271
  );
1272
+ } else if (!isDynamicValue(attr)) {
1273
+ const constraint = controller?.valueConstraints.find(
1274
+ (candidate) => dasherize(candidate.value) === parsed.valueToken
1275
+ );
1276
+ if (constraint && !valueSatisfiesConstraint(attr.value, constraint)) {
1277
+ report(
1278
+ node,
1279
+ "invalid-value",
1280
+ "error",
1281
+ `Invalid value "${attr.value.trim()}" for "${parsed.identifier}.${parsed.valueToken}". Expected ${describeValueConstraint(constraint)}.`,
1282
+ attr,
1283
+ constraint.suggestion
1284
+ );
1285
+ }
1212
1286
  }
1213
1287
  }
1214
1288
  continue;
@@ -1243,6 +1317,34 @@ function checkSource(source, manifest) {
1243
1317
  }
1244
1318
  }
1245
1319
  }
1320
+ for (const identifier of authoredValueIdentifiers) {
1321
+ const controller = known[identifier];
1322
+ if (!controller) continue;
1323
+ for (const relation of controller.valueRelations) {
1324
+ const leftAttr = node.attrs.find(
1325
+ (attr) => attr.name === `data-${identifier}-${dasherize(relation.left)}-value`
1326
+ );
1327
+ const rightAttr = node.attrs.find(
1328
+ (attr) => attr.name === `data-${identifier}-${dasherize(relation.right)}-value`
1329
+ );
1330
+ const at = rightAttr ?? leftAttr;
1331
+ if (!at || leftAttr && isDynamicValue(leftAttr) || rightAttr && isDynamicValue(rightAttr)) {
1332
+ continue;
1333
+ }
1334
+ const left = leftAttr ? decodeNumericValue(leftAttr.value) : relation.leftDefault;
1335
+ const right = rightAttr ? decodeNumericValue(rightAttr.value) : relation.rightDefault;
1336
+ if (!Number.isFinite(left) || !Number.isFinite(right)) continue;
1337
+ if (relation.operator === "less-than-or-equal" && left <= right) continue;
1338
+ report(
1339
+ node,
1340
+ "invalid-value",
1341
+ "error",
1342
+ `Invalid values for "${identifier}": "${relation.left}" (${left}) must be less than or equal to "${relation.right}" (${right}).`,
1343
+ at,
1344
+ relation.suggestion
1345
+ );
1346
+ }
1347
+ }
1246
1348
  });
1247
1349
  const scopeIsComplete = (container, scope, identifier) => container.origin === "markup" && !targetsUnreadable(container, scope, identifier);
1248
1350
  for (const { node, identifier } of scopes) {
@@ -1270,13 +1372,16 @@ function checkSource(source, manifest) {
1270
1372
  const present = presence.get(node)?.get(identifier) ?? /* @__PURE__ */ new Set();
1271
1373
  for (const rule of controller.conditionalTargets) {
1272
1374
  if (!present.has(rule.whenPresent)) continue;
1273
- const missing = rule.require.filter((target) => !present.has(target));
1375
+ const nodesOf = (target) => targetNodes.get(node)?.get(identifier)?.get(target) ?? [];
1376
+ const owners = nodesOf(rule.whenPresent);
1377
+ const satisfied = (target) => rule.requireInside ? nodesOf(target).some((el) => owners.some((owner) => isInside(el, owner))) : present.has(target);
1378
+ const missing = rule.require.filter((target) => !satisfied(target));
1274
1379
  if (missing.length === 0) continue;
1275
1380
  report(
1276
1381
  node,
1277
1382
  "missing-conditional-target",
1278
1383
  complete ? "error" : "warning",
1279
- `"${identifier}" has a "${rule.whenPresent}" target, which also requires ${list(missing)}.`,
1384
+ rule.requireInside ? `"${identifier}" requires ${list(missing)} inside its "${rule.whenPresent}".` : `"${identifier}" has a "${rule.whenPresent}" target, which also requires ${list(missing)}.`,
1280
1385
  node,
1281
1386
  complete ? rule.suggestion : UNRESOLVED_TARGET_HINT
1282
1387
  );
@@ -1294,7 +1399,7 @@ function checkSource(source, manifest) {
1294
1399
  if (!hasReadableMarkup(el)) continue;
1295
1400
  if (!contentHolds(node, identifier, el, req.whenContains)) continue;
1296
1401
  if (!elementHolds(el, req.whenElement)) continue;
1297
- checkA11y(report, el, identifier, req, severity);
1402
+ checkA11y(report, el, identifier, req, severity, isDynamicValue);
1298
1403
  }
1299
1404
  }
1300
1405
  }
@@ -1320,6 +1425,28 @@ function checkSource(source, manifest) {
1320
1425
  }
1321
1426
  }
1322
1427
  }
1428
+ for (const { node, identifier } of scopes) {
1429
+ const controller = known[identifier];
1430
+ if (!controller) continue;
1431
+ for (const rule of controller.hosts) {
1432
+ const elements = rule.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(rule.target) ?? [];
1433
+ const where = rule.target === "" ? "scope element" : `"${rule.target}" target`;
1434
+ for (const element of elements) {
1435
+ if (!hasReadableMarkup(element)) continue;
1436
+ if (hostIsAllowed(element, rule) !== false) continue;
1437
+ const type = element.attrs.find((attr) => attr.name === "type");
1438
+ const detail = element.tag === "button" ? `the unsupported ${type ? `type="${type.value}"` : 'implicit type="submit"'}` : `an unsupported interactive <${element.tag}>`;
1439
+ report(
1440
+ element,
1441
+ "invalid-host",
1442
+ "error",
1443
+ `The ${where} of "${identifier}" uses ${detail} host.`,
1444
+ type ?? element,
1445
+ rule.suggestion
1446
+ );
1447
+ }
1448
+ }
1449
+ }
1323
1450
  for (const { node, identifier } of scopes) {
1324
1451
  const controller = known[identifier];
1325
1452
  if (!controller) continue;
@@ -1561,12 +1688,31 @@ function checkSource(source, manifest) {
1561
1688
  diagnostics.sort((a, b) => a.line - b.line || a.column - b.column);
1562
1689
  return diagnostics;
1563
1690
  }
1691
+ function valueSatisfiesConstraint(raw, constraint) {
1692
+ const value = decodeNumericValue(raw);
1693
+ if (Number.isNaN(value)) return false;
1694
+ if (constraint.finite && !Number.isFinite(value)) return false;
1695
+ if (constraint.greaterThan !== void 0 && !(value > constraint.greaterThan)) return false;
1696
+ if (constraint.integer && !Number.isInteger(value)) return false;
1697
+ return true;
1698
+ }
1699
+ function decodeNumericValue(raw) {
1700
+ return Number(raw.replace(/_/g, ""));
1701
+ }
1702
+ function describeValueConstraint(constraint) {
1703
+ const parts = [constraint.finite ? "a finite number" : "a number"];
1704
+ if (constraint.greaterThan !== void 0) {
1705
+ parts.push(`greater than ${constraint.greaterThan}`);
1706
+ }
1707
+ if (constraint.integer) parts.push("with no fractional part");
1708
+ return parts.join(" ");
1709
+ }
1564
1710
  var UNRESOLVED_TARGET_HINT = "Part of this scope is rendered by a helper or a non-literal data: hash, so the target may exist at runtime.";
1565
1711
  var UNRESOLVED_HOST_HINT = "An enclosing helper's data: option is not a literal hash, so the host controller may be declared at runtime.";
1566
1712
  function hasReadableMarkup(el) {
1567
1713
  return el.origin === "markup";
1568
1714
  }
1569
- function checkA11y(report, el, identifier, req, severity) {
1715
+ function checkA11y(report, el, identifier, req, severity, isDynamicValue) {
1570
1716
  const groups = [
1571
1717
  { attrs: req.attrs, values: req.values },
1572
1718
  ...req.or ?? []
@@ -1586,9 +1732,14 @@ function checkA11y(report, el, identifier, req, severity) {
1586
1732
  }
1587
1733
  const invalid = [];
1588
1734
  let anyPresent = false;
1735
+ let blank;
1589
1736
  for (const [name, allowed] of allowedByAttr) {
1590
1737
  const attr = el.attrs.find((a) => a.name === name);
1591
1738
  if (!attr) continue;
1739
+ if (allowed.anyValue && attr.value.trim() === "" && !isDynamicValue(attr)) {
1740
+ blank ??= attr;
1741
+ continue;
1742
+ }
1592
1743
  anyPresent = true;
1593
1744
  if (!allowed.anyValue && !allowed.values.has(attr.value.trim())) {
1594
1745
  invalid.push({ attr, values: [...allowed.values] });
@@ -1600,8 +1751,8 @@ function checkA11y(report, el, identifier, req, severity) {
1600
1751
  el,
1601
1752
  "missing-aria",
1602
1753
  severity,
1603
- `"${identifier}" ${verb} ${describeAttrs([...allowedByAttr.keys()])} on its ${where}.`,
1604
- el,
1754
+ blank ? `${blank.name} is empty on the ${where} of "${identifier}", which ${verb} a value there.` : `"${identifier}" ${verb} ${describeAttrs([...allowedByAttr.keys()])} on its ${where}.`,
1755
+ blank ?? el,
1605
1756
  req.suggestion
1606
1757
  );
1607
1758
  return;
@@ -1633,6 +1784,20 @@ function describeCounted(target, attr, values) {
1633
1784
  return `"${target}" targets with ${attr}${constrained}`;
1634
1785
  }
1635
1786
  var NATIVE_FOCUSABLE_TAGS = /* @__PURE__ */ new Set(["button", "select", "textarea", "summary"]);
1787
+ var ALWAYS_INTERACTIVE_HOSTS = /* @__PURE__ */ new Set([
1788
+ "input",
1789
+ "select",
1790
+ "textarea",
1791
+ "label",
1792
+ "summary",
1793
+ "details",
1794
+ "iframe",
1795
+ "object",
1796
+ "embed"
1797
+ ]);
1798
+ function hasAttr(element, name) {
1799
+ return element.attrs.some((attr) => attr.name === name);
1800
+ }
1636
1801
  function isNativelyFocusable(el) {
1637
1802
  if (NATIVE_FOCUSABLE_TAGS.has(el.tag)) return true;
1638
1803
  if (el.tag === "input") {
@@ -1783,12 +1948,15 @@ function isCheckableFile(path) {
1783
1948
  var CONTROLLER_ARRAY_FIELDS = [
1784
1949
  "targets",
1785
1950
  "values",
1951
+ "valueConstraints",
1952
+ "valueRelations",
1786
1953
  "actions",
1787
1954
  "events",
1788
1955
  "requiredTargets",
1789
1956
  "conditionalTargets",
1790
1957
  "a11y",
1791
1958
  "keyboard",
1959
+ "hosts",
1792
1960
  "managedAria",
1793
1961
  "compositions",
1794
1962
  "companions",
@@ -2261,6 +2429,37 @@ var CONTROLLER_CONTRACT_SCHEMA = {
2261
2429
  id: { type: "string", description: 'Controller identifier, e.g. "stimeo--tabs".' },
2262
2430
  targets: STRING_ARRAY,
2263
2431
  values: STRING_ARRAY,
2432
+ valueConstraints: {
2433
+ type: "array",
2434
+ description: "Statically checkable semantic bounds on authored Stimulus Values.",
2435
+ items: {
2436
+ type: "object",
2437
+ properties: {
2438
+ value: { type: "string", description: "Camel-case declared Value name." },
2439
+ type: { type: "string", enum: ["number"] },
2440
+ finite: { type: "boolean" },
2441
+ greaterThan: { type: "number" },
2442
+ suggestion: { type: "string" }
2443
+ },
2444
+ required: ["value", "type", "suggestion"]
2445
+ }
2446
+ },
2447
+ valueRelations: {
2448
+ type: "array",
2449
+ description: "Statically checkable relationships between authored Stimulus Values.",
2450
+ items: {
2451
+ type: "object",
2452
+ properties: {
2453
+ left: { type: "string", description: "Camel-case left Value name." },
2454
+ operator: { type: "string", enum: ["less-than-or-equal"] },
2455
+ right: { type: "string", description: "Camel-case right Value name." },
2456
+ leftDefault: { type: "number" },
2457
+ rightDefault: { type: "number" },
2458
+ suggestion: { type: "string" }
2459
+ },
2460
+ required: ["left", "operator", "right", "leftDefault", "rightDefault", "suggestion"]
2461
+ }
2462
+ },
2264
2463
  actions: STRING_ARRAY,
2265
2464
  events: STRING_ARRAY,
2266
2465
  requiredTargets: {
@@ -2342,6 +2541,30 @@ var CONTROLLER_CONTRACT_SCHEMA = {
2342
2541
  required: ["target", "suggestion"]
2343
2542
  }
2344
2543
  },
2544
+ hosts: {
2545
+ type: "array",
2546
+ description: "Scope or target host-element contracts that prevent the controller from competing with an incompatible native interaction.",
2547
+ items: {
2548
+ type: "object",
2549
+ properties: {
2550
+ target: {
2551
+ type: "string",
2552
+ description: 'Target name; "" means the data-controller element itself.'
2553
+ },
2554
+ mode: {
2555
+ type: "string",
2556
+ enum: ["non-interactive", "non-interactive-or-button"],
2557
+ description: "Supported static host family."
2558
+ },
2559
+ buttonTypes: {
2560
+ ...STRING_ARRAY,
2561
+ description: "Literal button types admitted by non-interactive-or-button."
2562
+ },
2563
+ suggestion: { type: "string" }
2564
+ },
2565
+ required: ["target", "mode", "suggestion"]
2566
+ }
2567
+ },
2345
2568
  managedAria: {
2346
2569
  type: "array",
2347
2570
  description: "ARIA the controller recomputes at runtime; authoring it is a warning.",
@@ -2478,12 +2701,15 @@ var CONTROLLER_CONTRACT_SCHEMA = {
2478
2701
  "id",
2479
2702
  "targets",
2480
2703
  "values",
2704
+ "valueConstraints",
2705
+ "valueRelations",
2481
2706
  "actions",
2482
2707
  "events",
2483
2708
  "requiredTargets",
2484
2709
  "conditionalTargets",
2485
2710
  "a11y",
2486
2711
  "keyboard",
2712
+ "hosts",
2487
2713
  "managedAria",
2488
2714
  "compositions",
2489
2715
  "companions",
@@ -2502,7 +2728,7 @@ var TOOL_DESCRIPTORS = [
2502
2728
  {
2503
2729
  name: "stimeo_check",
2504
2730
  title: "Check Stimeo UI markup",
2505
- description: "Statically check HTML or ERB source that uses Stimeo UI (stimeo--*) controllers \u2014 the headless, accessible Stimulus UI component library for Rails. Returns a CheckReport with diagnostics \u2014 unknown controllers/targets/values/action methods, missing required targets, missing or invalid author-supplied ARIA, keyboard-focusability prerequisites, and misaligned cross-controller composition values \u2014 each with a fix suggestion when available. Call this to validate any Rails view, ERB template, or HTML you write or edit with these components, before finalizing it; ok=true means no errors.",
2731
+ description: "Statically check HTML or ERB source that uses Stimeo UI (stimeo--*) controllers \u2014 the headless, accessible Stimulus UI component library for Rails. Returns a CheckReport with diagnostics \u2014 unknown controllers/targets/values/action methods, missing required targets, missing or invalid author-supplied ARIA, keyboard-focusability prerequisites, incompatible interactive hosts, and misaligned cross-controller composition values \u2014 each with a fix suggestion when available. Call this to validate any Rails view, ERB template, or HTML you write or edit with these components, before finalizing it; ok=true means no errors.",
2506
2732
  annotations: READ_ONLY_ANNOTATIONS,
2507
2733
  inputSchema: {
2508
2734
  type: "object",
@@ -2536,7 +2762,7 @@ var TOOL_DESCRIPTORS = [
2536
2762
  {
2537
2763
  name: "stimeo_controller",
2538
2764
  title: "Get one controller's usage contract",
2539
- description: 'Fetch the full usage contract of one Stimeo UI controller by identifier (e.g. "stimeo--tabs"): targets, values, actions, events, required targets, and the accessibility contract \u2014 author-supplied ARIA requirements, keyboard-focusability prerequisites, the ARIA attributes the controller manages at runtime (which the author must NOT hardcode), and cross-controller composition value alignments. Use it after stimeo_catalog to write correct, accessible Rails ERB or HTML markup for that Stimulus component.',
2765
+ description: 'Fetch the full usage contract of one Stimeo UI controller by identifier (e.g. "stimeo--tabs"): targets, values, actions, events, required targets, and the accessibility contract \u2014 author-supplied ARIA requirements, keyboard-focusability prerequisites, supported host-element shapes, the ARIA attributes the controller manages at runtime (which the author must NOT hardcode), and cross-controller composition value alignments. Use it after stimeo_catalog to write correct, accessible Rails ERB or HTML markup for that Stimulus component.',
2540
2766
  annotations: READ_ONLY_ANNOTATIONS,
2541
2767
  inputSchema: {
2542
2768
  type: "object",