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
@@ -23,6 +23,14 @@ interface ControllerManifest {
23
23
  readonly targets: readonly string[];
24
24
  /** Value names (camelCase) declared via `static values`. */
25
25
  readonly values: readonly string[];
26
+ /**
27
+ * Authoring constraints for literal Stimulus Values (stage 1). Reflection can
28
+ * expose a Value's name and decoder, but not semantic bounds such as a
29
+ * strictly positive step, so those contracts stay explicit and reviewable.
30
+ */
31
+ readonly valueConstraints: readonly ValueConstraint[];
32
+ /** Statically decidable relationships between literal Stimulus Values. */
33
+ readonly valueRelations: readonly ValueRelation[];
26
34
  /**
27
35
  * Public action method names declared via `static actions`, wired by
28
36
  * consumers as `data-action="…-><identifier>#<action>"`.
@@ -57,6 +65,13 @@ interface ControllerManifest {
57
65
  * toolbar, radio group, …) are deliberately excluded.
58
66
  */
59
67
  readonly keyboard: readonly KeyboardRequirement[];
68
+ /**
69
+ * Host-element contracts (stage 3): scope or target elements that must avoid
70
+ * native interactive semantics the controller cannot safely compose with.
71
+ * A rule may explicitly admit non-submitting buttons when the controller
72
+ * delegates their native activation to the browser.
73
+ */
74
+ readonly hosts: readonly HostRequirement[];
60
75
  /**
61
76
  * Author-futile attributes (stage 3): ARIA the controller recomputes
62
77
  * wholesale at runtime, where an authored value can neither
@@ -212,9 +227,11 @@ interface DocumentCondition {
212
227
  * A single accessibility requirement on a controller's markup (Inspector
213
228
  * stage 3). At least one of {@link attrs} must be present on the
214
229
  * {@link target} element; when {@link values} is given, the present
215
- * attribute's value must be one of them. {@link or} widens the requirement
216
- * with alternative attribute/value groups — the requirement is satisfied when
217
- * *any* group is (e.g. `role="status"` **or** `aria-live`).
230
+ * attribute's value must be one of them, and when it is not, the value must be
231
+ * non-empty — ARIA derives no name from `aria-label=""`, so an empty value
232
+ * leaves the requirement unmet. {@link or} widens the requirement with
233
+ * alternative attribute/value groups — the requirement is satisfied when *any*
234
+ * group is (e.g. `role="status"` **or** `aria-live`).
218
235
  */
219
236
  interface A11yRequirement {
220
237
  /**
@@ -328,6 +345,27 @@ interface KeyboardRequirement {
328
345
  /** Human-readable fix suggestion shown by the CLI (stage 4). */
329
346
  readonly suggestion: string;
330
347
  }
348
+ /**
349
+ * A host-element contract (stage 3): the element carrying a controller or one
350
+ * of its targets must use semantics the controller can own without competing
351
+ * with another native interaction.
352
+ *
353
+ * `"non-interactive"` admits only generic, non-interactive elements.
354
+ * `"non-interactive-or-button"` additionally admits a `<button>` whose
355
+ * literal `type` is listed in {@link buttonTypes}. The explicit type matters:
356
+ * a missing or invalid button type defaults to `submit`, so treating it as an
357
+ * ordinary action button would silently submit an enclosing form.
358
+ */
359
+ interface HostRequirement {
360
+ /** Target name; the empty string `""` means the controller scope element. */
361
+ readonly target: string;
362
+ /** Static host shapes the controller supports. */
363
+ readonly mode: "non-interactive" | "non-interactive-or-button";
364
+ /** Literal button types admitted by `"non-interactive-or-button"`. */
365
+ readonly buttonTypes?: readonly string[];
366
+ /** Human-readable fix suggestion shown by the CLI (stage 4). */
367
+ readonly suggestion: string;
368
+ }
331
369
  /**
332
370
  * An author-futile attribute rule (stage 3): authoring any of {@link attrs} on
333
371
  * the {@link target} element draws a `managed-aria` **warning** — the
@@ -561,6 +599,56 @@ interface ConditionalTargetRule {
561
599
  readonly whenPresent: string;
562
600
  /** Targets that become required once {@link whenPresent} appears. */
563
601
  readonly require: readonly string[];
602
+ /**
603
+ * Require each {@link require} entry to sit **inside** the `whenPresent`
604
+ * element rather than anywhere in the scope.
605
+ *
606
+ * A `<template>` is cloned at runtime, so a part declared outside it never
607
+ * reaches the clone. Without this the presence check passes on markup the
608
+ * controller then refuses to build from.
609
+ */
610
+ readonly requireInside?: boolean;
611
+ /** Human-readable fix suggestion shown by the CLI (stage 4). */
612
+ readonly suggestion: string;
613
+ }
614
+ /**
615
+ * A statically checkable numeric contract on one declared Stimulus Value.
616
+ *
617
+ * The Inspector decodes literals like Stimulus (`Number(raw.replace(/_/g,
618
+ * ""))`) before applying these bounds. ERB-generated values are undecidable and
619
+ * skipped rather than guessed.
620
+ */
621
+ interface ValueConstraint {
622
+ /** Value name (camelCase, as declared in `static values`). */
623
+ readonly value: string;
624
+ /** Decoder family. Numeric bounds are the first supported semantic family. */
625
+ readonly type: "number";
626
+ /** Reject `NaN` and infinities when true. */
627
+ readonly finite?: boolean;
628
+ /** Require a decoded number strictly greater than this bound. */
629
+ readonly greaterThan?: number;
630
+ /** Require a decoded number with no fractional part. */
631
+ readonly integer?: boolean;
632
+ /** Human-readable fix suggestion shown by the CLI (stage 4). */
633
+ readonly suggestion: string;
634
+ }
635
+ /**
636
+ * A statically checkable relationship between two numeric Stimulus Values.
637
+ *
638
+ * Missing attributes use the controller's public defaults. If either authored
639
+ * value is dynamic, the Inspector skips the relation rather than guessing.
640
+ */
641
+ interface ValueRelation {
642
+ /** Value on the left side of the comparison (camelCase). */
643
+ readonly left: string;
644
+ /** Supported numeric comparison. */
645
+ readonly operator: "less-than-or-equal";
646
+ /** Value on the right side of the comparison (camelCase). */
647
+ readonly right: string;
648
+ /** Effective left value when its attribute is absent. */
649
+ readonly leftDefault: number;
650
+ /** Effective right value when its attribute is absent. */
651
+ readonly rightDefault: number;
564
652
  /** Human-readable fix suggestion shown by the CLI (stage 4). */
565
653
  readonly suggestion: string;
566
654
  }
@@ -571,7 +659,7 @@ type DiagnosticSeverity = "error" | "warning";
571
659
  * for testing, machine-readable output, and `data-stimeo-ignore` suppression
572
660
  * lists (which is why the set also exists as a runtime array).
573
661
  */
574
- declare const DIAGNOSTIC_CODES: readonly ["unknown-controller", "unknown-target", "unknown-value", "unknown-action-controller", "unknown-action-method", "orphan-target", "missing-required-target", "missing-conditional-target", "missing-aria", "invalid-aria-value", "keyboard-inaccessible", "unresolved-idref", "managed-aria", "composition-mismatch", "missing-companion", "undeclared-target", "cardinality-violation", "forbidden-aria", "unknown-ignore-code"];
662
+ declare const DIAGNOSTIC_CODES: readonly ["unknown-controller", "unknown-target", "unknown-value", "invalid-value", "unknown-action-controller", "unknown-action-method", "orphan-target", "missing-required-target", "missing-conditional-target", "missing-aria", "invalid-aria-value", "keyboard-inaccessible", "invalid-host", "unresolved-idref", "managed-aria", "composition-mismatch", "missing-companion", "undeclared-target", "cardinality-violation", "forbidden-aria", "unknown-ignore-code"];
575
663
  /** See {@link DIAGNOSTIC_CODES}. */
576
664
  type DiagnosticCode = (typeof DIAGNOSTIC_CODES)[number];
577
665
  /**
@@ -610,6 +610,26 @@ function readEntry(code, start, base) {
610
610
  };
611
611
  }
612
612
  }
613
+ const number = /^[+-]?\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?/.exec(
614
+ code.slice(valueStart)
615
+ )?.[0];
616
+ if (number !== void 0) {
617
+ const end = valueStart + number.length;
618
+ const after = skipTrivia(code, end);
619
+ const whole = after >= code.length || code[after] === "," || code[after] === "}";
620
+ if (whole) {
621
+ return {
622
+ attr: {
623
+ name,
624
+ value: number,
625
+ ...anchor,
626
+ valueStart: base + valueStart,
627
+ valueEnd: base + end
628
+ },
629
+ end
630
+ };
631
+ }
632
+ }
613
633
  return { attr: { name, value: null, ...anchor }, end: skipValue(code, valueStart) };
614
634
  }
615
635
  function readKey(code, start) {
@@ -926,6 +946,7 @@ var DIAGNOSTIC_CODES = [
926
946
  "unknown-controller",
927
947
  "unknown-target",
928
948
  "unknown-value",
949
+ "invalid-value",
929
950
  "unknown-action-controller",
930
951
  "unknown-action-method",
931
952
  "orphan-target",
@@ -934,6 +955,7 @@ var DIAGNOSTIC_CODES = [
934
955
  "missing-aria",
935
956
  "invalid-aria-value",
936
957
  "keyboard-inaccessible",
958
+ "invalid-host",
937
959
  "unresolved-idref",
938
960
  "managed-aria",
939
961
  "composition-mismatch",
@@ -945,6 +967,12 @@ var DIAGNOSTIC_CODES = [
945
967
  ];
946
968
 
947
969
  // src/inspector/check.ts
970
+ function isInside(node, ancestor) {
971
+ for (let current = node.parent; current; current = current.parent) {
972
+ if (current === ancestor) return true;
973
+ }
974
+ return false;
975
+ }
948
976
  function checkSource(source, manifest) {
949
977
  const diagnostics = [];
950
978
  const tree = parseHtml(neutralizeErb(source), erbElements(source));
@@ -965,6 +993,36 @@ function checkSource(source, manifest) {
965
993
  if (hostAttr && isDynamicValue(hostAttr)) return null;
966
994
  return when.equals.includes(hostAttr ? hostAttr.value.trim() : when.default);
967
995
  };
996
+ const isContentEditable2 = (element) => {
997
+ let current = element;
998
+ while (current && current.tag !== "#root") {
999
+ const attr = current.attrs.find((candidate) => candidate.name === "contenteditable");
1000
+ if (attr) {
1001
+ if (isDynamicValue(attr)) return null;
1002
+ const value = attr.value.trim().toLowerCase();
1003
+ if (value === "false") return false;
1004
+ if (value === "" || value === "true" || value === "plaintext-only") return true;
1005
+ }
1006
+ current = current.parent;
1007
+ }
1008
+ return false;
1009
+ };
1010
+ const hostIsAllowed = (element, rule) => {
1011
+ if (element.tag === "button") {
1012
+ if (rule.mode === "non-interactive") return false;
1013
+ const type = element.attrs.find((attr) => attr.name === "type");
1014
+ if (type && isDynamicValue(type)) return null;
1015
+ const effectiveType = type?.value.trim().toLowerCase() || "submit";
1016
+ return (rule.buttonTypes ?? []).includes(effectiveType);
1017
+ }
1018
+ if (ALWAYS_INTERACTIVE_HOSTS.has(element.tag)) return false;
1019
+ if ((element.tag === "a" || element.tag === "area") && hasAttr(element, "href")) return false;
1020
+ if ((element.tag === "audio" || element.tag === "video") && hasAttr(element, "controls")) {
1021
+ return false;
1022
+ }
1023
+ const editable = isContentEditable2(element);
1024
+ return editable === null ? null : !editable;
1025
+ };
968
1026
  const suppressed = (node, code) => {
969
1027
  if (code === "unknown-ignore-code") return false;
970
1028
  let current = node;
@@ -1072,6 +1130,7 @@ function checkSource(source, manifest) {
1072
1130
  focusableRoleTally.set(role, (focusableRoleTally.get(role) ?? 0) + 1);
1073
1131
  }
1074
1132
  }
1133
+ const authoredValueIdentifiers = /* @__PURE__ */ new Set();
1075
1134
  for (const attr of node.attrs) {
1076
1135
  if (attr.name === IGNORE_ATTR) {
1077
1136
  for (const token of attr.value.split(/\s+/).filter((t) => t.length > 0)) {
@@ -1196,6 +1255,7 @@ function checkSource(source, manifest) {
1196
1255
  attr
1197
1256
  );
1198
1257
  } else {
1258
+ authoredValueIdentifiers.add(parsed.identifier);
1199
1259
  const controller = known[parsed.identifier];
1200
1260
  const valid = controller?.values.some((v) => dasherize(v) === parsed.valueToken) ?? false;
1201
1261
  if (!valid) {
@@ -1207,6 +1267,20 @@ function checkSource(source, manifest) {
1207
1267
  attr,
1208
1268
  didYouMean(parsed.valueToken, (controller?.values ?? []).map(dasherize))
1209
1269
  );
1270
+ } else if (!isDynamicValue(attr)) {
1271
+ const constraint = controller?.valueConstraints.find(
1272
+ (candidate) => dasherize(candidate.value) === parsed.valueToken
1273
+ );
1274
+ if (constraint && !valueSatisfiesConstraint(attr.value, constraint)) {
1275
+ report(
1276
+ node,
1277
+ "invalid-value",
1278
+ "error",
1279
+ `Invalid value "${attr.value.trim()}" for "${parsed.identifier}.${parsed.valueToken}". Expected ${describeValueConstraint(constraint)}.`,
1280
+ attr,
1281
+ constraint.suggestion
1282
+ );
1283
+ }
1210
1284
  }
1211
1285
  }
1212
1286
  continue;
@@ -1241,6 +1315,34 @@ function checkSource(source, manifest) {
1241
1315
  }
1242
1316
  }
1243
1317
  }
1318
+ for (const identifier of authoredValueIdentifiers) {
1319
+ const controller = known[identifier];
1320
+ if (!controller) continue;
1321
+ for (const relation of controller.valueRelations) {
1322
+ const leftAttr = node.attrs.find(
1323
+ (attr) => attr.name === `data-${identifier}-${dasherize(relation.left)}-value`
1324
+ );
1325
+ const rightAttr = node.attrs.find(
1326
+ (attr) => attr.name === `data-${identifier}-${dasherize(relation.right)}-value`
1327
+ );
1328
+ const at = rightAttr ?? leftAttr;
1329
+ if (!at || leftAttr && isDynamicValue(leftAttr) || rightAttr && isDynamicValue(rightAttr)) {
1330
+ continue;
1331
+ }
1332
+ const left = leftAttr ? decodeNumericValue(leftAttr.value) : relation.leftDefault;
1333
+ const right = rightAttr ? decodeNumericValue(rightAttr.value) : relation.rightDefault;
1334
+ if (!Number.isFinite(left) || !Number.isFinite(right)) continue;
1335
+ if (relation.operator === "less-than-or-equal" && left <= right) continue;
1336
+ report(
1337
+ node,
1338
+ "invalid-value",
1339
+ "error",
1340
+ `Invalid values for "${identifier}": "${relation.left}" (${left}) must be less than or equal to "${relation.right}" (${right}).`,
1341
+ at,
1342
+ relation.suggestion
1343
+ );
1344
+ }
1345
+ }
1244
1346
  });
1245
1347
  const scopeIsComplete = (container, scope, identifier) => container.origin === "markup" && !targetsUnreadable(container, scope, identifier);
1246
1348
  for (const { node, identifier } of scopes) {
@@ -1268,13 +1370,16 @@ function checkSource(source, manifest) {
1268
1370
  const present = presence.get(node)?.get(identifier) ?? /* @__PURE__ */ new Set();
1269
1371
  for (const rule of controller.conditionalTargets) {
1270
1372
  if (!present.has(rule.whenPresent)) continue;
1271
- const missing = rule.require.filter((target) => !present.has(target));
1373
+ const nodesOf = (target) => targetNodes.get(node)?.get(identifier)?.get(target) ?? [];
1374
+ const owners = nodesOf(rule.whenPresent);
1375
+ const satisfied = (target) => rule.requireInside ? nodesOf(target).some((el) => owners.some((owner) => isInside(el, owner))) : present.has(target);
1376
+ const missing = rule.require.filter((target) => !satisfied(target));
1272
1377
  if (missing.length === 0) continue;
1273
1378
  report(
1274
1379
  node,
1275
1380
  "missing-conditional-target",
1276
1381
  complete ? "error" : "warning",
1277
- `"${identifier}" has a "${rule.whenPresent}" target, which also requires ${list(missing)}.`,
1382
+ rule.requireInside ? `"${identifier}" requires ${list(missing)} inside its "${rule.whenPresent}".` : `"${identifier}" has a "${rule.whenPresent}" target, which also requires ${list(missing)}.`,
1278
1383
  node,
1279
1384
  complete ? rule.suggestion : UNRESOLVED_TARGET_HINT
1280
1385
  );
@@ -1292,7 +1397,7 @@ function checkSource(source, manifest) {
1292
1397
  if (!hasReadableMarkup(el)) continue;
1293
1398
  if (!contentHolds(node, identifier, el, req.whenContains)) continue;
1294
1399
  if (!elementHolds(el, req.whenElement)) continue;
1295
- checkA11y(report, el, identifier, req, severity);
1400
+ checkA11y(report, el, identifier, req, severity, isDynamicValue);
1296
1401
  }
1297
1402
  }
1298
1403
  }
@@ -1318,6 +1423,28 @@ function checkSource(source, manifest) {
1318
1423
  }
1319
1424
  }
1320
1425
  }
1426
+ for (const { node, identifier } of scopes) {
1427
+ const controller = known[identifier];
1428
+ if (!controller) continue;
1429
+ for (const rule of controller.hosts) {
1430
+ const elements = rule.target === "" ? [node] : targetNodes.get(node)?.get(identifier)?.get(rule.target) ?? [];
1431
+ const where = rule.target === "" ? "scope element" : `"${rule.target}" target`;
1432
+ for (const element of elements) {
1433
+ if (!hasReadableMarkup(element)) continue;
1434
+ if (hostIsAllowed(element, rule) !== false) continue;
1435
+ const type = element.attrs.find((attr) => attr.name === "type");
1436
+ const detail = element.tag === "button" ? `the unsupported ${type ? `type="${type.value}"` : 'implicit type="submit"'}` : `an unsupported interactive <${element.tag}>`;
1437
+ report(
1438
+ element,
1439
+ "invalid-host",
1440
+ "error",
1441
+ `The ${where} of "${identifier}" uses ${detail} host.`,
1442
+ type ?? element,
1443
+ rule.suggestion
1444
+ );
1445
+ }
1446
+ }
1447
+ }
1321
1448
  for (const { node, identifier } of scopes) {
1322
1449
  const controller = known[identifier];
1323
1450
  if (!controller) continue;
@@ -1559,12 +1686,31 @@ function checkSource(source, manifest) {
1559
1686
  diagnostics.sort((a, b) => a.line - b.line || a.column - b.column);
1560
1687
  return diagnostics;
1561
1688
  }
1689
+ function valueSatisfiesConstraint(raw, constraint) {
1690
+ const value = decodeNumericValue(raw);
1691
+ if (Number.isNaN(value)) return false;
1692
+ if (constraint.finite && !Number.isFinite(value)) return false;
1693
+ if (constraint.greaterThan !== void 0 && !(value > constraint.greaterThan)) return false;
1694
+ if (constraint.integer && !Number.isInteger(value)) return false;
1695
+ return true;
1696
+ }
1697
+ function decodeNumericValue(raw) {
1698
+ return Number(raw.replace(/_/g, ""));
1699
+ }
1700
+ function describeValueConstraint(constraint) {
1701
+ const parts = [constraint.finite ? "a finite number" : "a number"];
1702
+ if (constraint.greaterThan !== void 0) {
1703
+ parts.push(`greater than ${constraint.greaterThan}`);
1704
+ }
1705
+ if (constraint.integer) parts.push("with no fractional part");
1706
+ return parts.join(" ");
1707
+ }
1562
1708
  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.";
1563
1709
  var UNRESOLVED_HOST_HINT = "An enclosing helper's data: option is not a literal hash, so the host controller may be declared at runtime.";
1564
1710
  function hasReadableMarkup(el) {
1565
1711
  return el.origin === "markup";
1566
1712
  }
1567
- function checkA11y(report, el, identifier, req, severity) {
1713
+ function checkA11y(report, el, identifier, req, severity, isDynamicValue) {
1568
1714
  const groups = [
1569
1715
  { attrs: req.attrs, values: req.values },
1570
1716
  ...req.or ?? []
@@ -1584,9 +1730,14 @@ function checkA11y(report, el, identifier, req, severity) {
1584
1730
  }
1585
1731
  const invalid = [];
1586
1732
  let anyPresent = false;
1733
+ let blank;
1587
1734
  for (const [name, allowed] of allowedByAttr) {
1588
1735
  const attr = el.attrs.find((a) => a.name === name);
1589
1736
  if (!attr) continue;
1737
+ if (allowed.anyValue && attr.value.trim() === "" && !isDynamicValue(attr)) {
1738
+ blank ??= attr;
1739
+ continue;
1740
+ }
1590
1741
  anyPresent = true;
1591
1742
  if (!allowed.anyValue && !allowed.values.has(attr.value.trim())) {
1592
1743
  invalid.push({ attr, values: [...allowed.values] });
@@ -1598,8 +1749,8 @@ function checkA11y(report, el, identifier, req, severity) {
1598
1749
  el,
1599
1750
  "missing-aria",
1600
1751
  severity,
1601
- `"${identifier}" ${verb} ${describeAttrs([...allowedByAttr.keys()])} on its ${where}.`,
1602
- el,
1752
+ blank ? `${blank.name} is empty on the ${where} of "${identifier}", which ${verb} a value there.` : `"${identifier}" ${verb} ${describeAttrs([...allowedByAttr.keys()])} on its ${where}.`,
1753
+ blank ?? el,
1603
1754
  req.suggestion
1604
1755
  );
1605
1756
  return;
@@ -1631,6 +1782,20 @@ function describeCounted(target, attr, values) {
1631
1782
  return `"${target}" targets with ${attr}${constrained}`;
1632
1783
  }
1633
1784
  var NATIVE_FOCUSABLE_TAGS = /* @__PURE__ */ new Set(["button", "select", "textarea", "summary"]);
1785
+ var ALWAYS_INTERACTIVE_HOSTS = /* @__PURE__ */ new Set([
1786
+ "input",
1787
+ "select",
1788
+ "textarea",
1789
+ "label",
1790
+ "summary",
1791
+ "details",
1792
+ "iframe",
1793
+ "object",
1794
+ "embed"
1795
+ ]);
1796
+ function hasAttr(element, name) {
1797
+ return element.attrs.some((attr) => attr.name === name);
1798
+ }
1634
1799
  function isNativelyFocusable(el) {
1635
1800
  if (NATIVE_FOCUSABLE_TAGS.has(el.tag)) return true;
1636
1801
  if (el.tag === "input") {
@@ -1781,12 +1946,15 @@ function isCheckableFile(path) {
1781
1946
  var CONTROLLER_ARRAY_FIELDS = [
1782
1947
  "targets",
1783
1948
  "values",
1949
+ "valueConstraints",
1950
+ "valueRelations",
1784
1951
  "actions",
1785
1952
  "events",
1786
1953
  "requiredTargets",
1787
1954
  "conditionalTargets",
1788
1955
  "a11y",
1789
1956
  "keyboard",
1957
+ "hosts",
1790
1958
  "managedAria",
1791
1959
  "compositions",
1792
1960
  "companions",