stimeo-ui 0.16.0 → 0.18.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 (117) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +136 -0
  3. data/README.md +27 -11
  4. data/dist/cable/index.js +384 -449
  5. data/dist/controllers/accordion_controller.js +174 -55
  6. data/dist/controllers/alert_dialog_controller.js +903 -235
  7. data/dist/controllers/announcer_controller.js +238 -205
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +414 -145
  10. data/dist/controllers/avatar_controller.js +185 -107
  11. data/dist/controllers/breadcrumb_controller.js +0 -130
  12. data/dist/controllers/bulk_select_controller.js +282 -58
  13. data/dist/controllers/calendar_controller.js +228 -260
  14. data/dist/controllers/carousel_controller.js +536 -436
  15. data/dist/controllers/character_counter_controller.js +493 -207
  16. data/dist/controllers/checkbox_controller.js +38 -74
  17. data/dist/controllers/clipboard_controller.js +261 -149
  18. data/dist/controllers/collapsible_controller.js +304 -120
  19. data/dist/controllers/color_picker_controller.js +178 -146
  20. data/dist/controllers/combobox_controller.js +522 -156
  21. data/dist/controllers/command_palette_controller.js +1156 -395
  22. data/dist/controllers/conditional_fields_controller.js +300 -171
  23. data/dist/controllers/confirm_controller.js +980 -262
  24. data/dist/controllers/context_menu_controller.js +346 -153
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +281 -158
  27. data/dist/controllers/currency_input_controller.js +575 -225
  28. data/dist/controllers/data_grid_controller.js +248 -214
  29. data/dist/controllers/date_range_picker_controller.js +338 -235
  30. data/dist/controllers/dialog_controller.js +899 -237
  31. data/dist/controllers/direct_upload_controller.js +97 -167
  32. data/dist/controllers/dirty_form_controller.js +72 -69
  33. data/dist/controllers/dismissible_controller.js +196 -24
  34. data/dist/controllers/drawer_controller.js +974 -355
  35. data/dist/controllers/dropdown_controller.js +339 -96
  36. data/dist/controllers/editable_controller.js +315 -88
  37. data/dist/controllers/empty_state_controller.js +79 -58
  38. data/dist/controllers/file_dropzone_controller.js +322 -211
  39. data/dist/controllers/filter_controller.js +77 -42
  40. data/dist/controllers/flash_controller.js +238 -347
  41. data/dist/controllers/focus_controller.js +679 -240
  42. data/dist/controllers/form_field_controller.js +156 -132
  43. data/dist/controllers/form_validation_controller.js +31 -97
  44. data/dist/controllers/frame_loading_controller.js +275 -217
  45. data/dist/controllers/highlight_controller.js +107 -75
  46. data/dist/controllers/hover_card_controller.js +327 -130
  47. data/dist/controllers/idle_controller.js +271 -101
  48. data/dist/controllers/input_mask_controller.js +483 -97
  49. data/dist/controllers/intersection_controller.js +150 -106
  50. data/dist/controllers/lazy_frame_controller.js +24 -68
  51. data/dist/controllers/listbox_controller.js +523 -240
  52. data/dist/controllers/local_time_controller.js +102 -77
  53. data/dist/controllers/masonry_controller.js +80 -96
  54. data/dist/controllers/menu_controller.js +302 -188
  55. data/dist/controllers/menubar_controller.js +232 -345
  56. data/dist/controllers/meter_controller.js +417 -87
  57. data/dist/controllers/multi_select_controller.js +785 -393
  58. data/dist/controllers/navigation_menu_controller.js +190 -250
  59. data/dist/controllers/nested_form_controller.js +396 -192
  60. data/dist/controllers/network_status_controller.js +268 -62
  61. data/dist/controllers/number_input_controller.js +610 -303
  62. data/dist/controllers/optimistic_controller.js +45 -87
  63. data/dist/controllers/otp_controller.js +658 -255
  64. data/dist/controllers/overflow_indicator_controller.js +122 -113
  65. data/dist/controllers/overflow_menu_controller.js +470 -295
  66. data/dist/controllers/pagination_controller.js +332 -123
  67. data/dist/controllers/password_reveal_controller.js +392 -121
  68. data/dist/controllers/password_strength_controller.js +111 -158
  69. data/dist/controllers/persist_controller.js +165 -111
  70. data/dist/controllers/pointer_drag_controller.js +434 -188
  71. data/dist/controllers/popover_controller.js +269 -113
  72. data/dist/controllers/portal_controller.js +215 -76
  73. data/dist/controllers/preview_guard_controller.js +243 -116
  74. data/dist/controllers/progress_controller.js +391 -78
  75. data/dist/controllers/radio_group_controller.js +278 -211
  76. data/dist/controllers/range_slider_controller.js +220 -113
  77. data/dist/controllers/rating_controller.js +458 -211
  78. data/dist/controllers/read_more_controller.js +138 -63
  79. data/dist/controllers/reading_progress_controller.js +166 -137
  80. data/dist/controllers/relative_time_controller.js +211 -112
  81. data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +103 -38
  82. data/dist/controllers/resizable_controller.js +299 -150
  83. data/dist/controllers/roving_controller.js +49 -88
  84. data/dist/controllers/scroll_area_controller.js +490 -246
  85. data/dist/controllers/scroll_restore_controller.js +14 -55
  86. data/dist/controllers/scroll_visibility_controller.js +432 -149
  87. data/dist/controllers/scrollspy_controller.js +193 -248
  88. data/dist/controllers/separator_controller.js +285 -142
  89. data/dist/controllers/sidebar_controller.js +1069 -404
  90. data/dist/controllers/skeleton_controller.js +259 -101
  91. data/dist/controllers/slider_controller.js +187 -86
  92. data/dist/controllers/smart_sticky_header_controller.js +173 -66
  93. data/dist/controllers/sortable_controller.js +26 -125
  94. data/dist/controllers/spinner_controller.js +288 -173
  95. data/dist/controllers/step_indicator_controller.js +105 -46
  96. data/dist/controllers/stepper_controller.js +120 -73
  97. data/dist/controllers/stick_to_bottom_controller.js +275 -140
  98. data/dist/controllers/sticky_observer_controller.js +220 -49
  99. data/dist/controllers/submit_once_controller.js +376 -227
  100. data/dist/controllers/switch_controller.js +201 -84
  101. data/dist/controllers/tabs_controller.js +41 -30
  102. data/dist/controllers/tags_input_controller.js +293 -173
  103. data/dist/controllers/textarea_autosize_controller.js +108 -74
  104. data/dist/controllers/theme_controller.js +140 -116
  105. data/dist/controllers/time_picker_controller.js +331 -127
  106. data/dist/controllers/toast_controller.js +244 -324
  107. data/dist/controllers/toggle_group_controller.js +252 -191
  108. data/dist/controllers/toolbar_controller.js +219 -120
  109. data/dist/controllers/tooltip_controller.js +158 -124
  110. data/dist/controllers/transition_controller.js +123 -119
  111. data/dist/controllers/tree_view_controller.js +283 -369
  112. data/dist/index.js +8573 -10410
  113. data/dist/positioning/index.js +154 -47
  114. data/lib/generators/stimeo/install/install_generator.rb +7 -2
  115. data/lib/generators/stimeo/install/templates/stimeo.js +19 -6
  116. data/lib/stimeo/ui/version.rb +1 -1
  117. metadata +3 -3
@@ -1,28 +1,35 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/step_indicator_controller.ts
3
+ var ListenerSet = class {
4
+ #abort = new AbortController();
5
+ add(target, type, handler, options) {
6
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
7
+ }
8
+ dispose() {
9
+ this.#abort.abort();
10
+ this.#abort = new AbortController();
11
+ }
12
+ };
4
13
 
5
- // src/utils/microtask_coalescer.ts
6
14
  var MicrotaskCoalescer = class {
7
15
  #run;
8
16
  #queued = false;
9
17
  #active = false;
10
18
  #generation = 0;
11
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
12
19
  constructor(run) {
13
20
  this.#run = run;
14
21
  }
15
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
16
22
  activate() {
17
23
  this.#active = true;
18
24
  }
19
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
20
25
  cancel() {
21
26
  this.#active = false;
22
27
  this.#queued = false;
23
28
  this.#generation += 1;
24
29
  }
25
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
30
+ get pending() {
31
+ return this.#queued;
32
+ }
26
33
  schedule() {
27
34
  if (!this.#active || this.#queued) return;
28
35
  this.#queued = true;
@@ -35,52 +42,111 @@ var MicrotaskCoalescer = class {
35
42
  }
36
43
  };
37
44
 
38
- // src/controllers/step_indicator_controller.ts
39
- var StepIndicatorController = class extends Controller {
45
+ var MorphRenderWatcher = class {
46
+ #listeners = new ListenerSet();
47
+ #pass;
48
+ #element = null;
49
+ #onMorph = () => this.#pass.schedule();
50
+ constructor(run) {
51
+ this.#pass = new MicrotaskCoalescer(run);
52
+ }
53
+ observe(element) {
54
+ if (this.#element === element) return;
55
+ this.disconnect();
56
+ this.#element = element;
57
+ this.#pass.activate();
58
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
59
+ }
60
+ schedule() {
61
+ this.#pass.schedule();
62
+ }
63
+ get pending() {
64
+ return this.#pass.pending;
65
+ }
66
+ disconnect() {
67
+ this.#listeners.dispose();
68
+ this.#pass.cancel();
69
+ this.#element = null;
70
+ }
71
+ };
72
+
73
+ var NUMBER_BOUNDS = {
74
+ finite: { finite: true }};
75
+ function matchesNumberBounds(value, bounds) {
76
+ if (!Number.isFinite(value)) {
77
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
78
+ if (direction === null) return false;
79
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
80
+ }
81
+ if (bounds.min !== void 0 && value < bounds.min) return false;
82
+ if (bounds.max !== void 0 && value > bounds.max) return false;
83
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
84
+ if (bounds.integer && !Number.isInteger(value)) return false;
85
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
86
+ return true;
87
+ }
88
+
89
+ function readNumber(raw, fallback, bounds) {
90
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
91
+ }
92
+
93
+ var NumberValueReader = class {
94
+ #lastRejected = /* @__PURE__ */ new Map();
95
+ read(owner, name, raw, fallback, bounds) {
96
+ const resolved = readNumber(raw, fallback, bounds);
97
+ if (matchesNumberBounds(raw, bounds)) {
98
+ this.#lastRejected.delete(name);
99
+ return resolved;
100
+ }
101
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
102
+ const literal = owner.element.getAttribute(attribute);
103
+ if (literal === null) {
104
+ this.#lastRejected.delete(name);
105
+ return resolved;
106
+ }
107
+ if (this.#lastRejected.get(name) !== literal) {
108
+ this.#lastRejected.set(name, literal);
109
+ console.warn(
110
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
111
+ );
112
+ }
113
+ return resolved;
114
+ }
115
+ };
116
+
117
+ var StepIndicatorController = class _StepIndicatorController extends Controller {
118
+ #numbers = new NumberValueReader();
40
119
  static targets = ["step"];
41
120
  static values = {
42
121
  index: { type: Number, default: 0 }
43
122
  };
123
+ static valueConstraints = {
124
+ index: NUMBER_BOUNDS.finite
125
+ };
44
126
  static actions = ["setIndex"];
45
127
  static events = ["change"];
46
- /**
47
- * Collapses a batch of step callbacks — and a morph that swaps `index` with
48
- * them — into one repaint. Replacing a list of N steps delivers N callbacks, and
49
- * each one would otherwise rewrite every step's state.
50
- */
51
- #repaint = new MicrotaskCoalescer(() => this.#render());
52
- /** Renders the initial state from the `index` value. */
128
+ #repaint = new MorphRenderWatcher(() => this.#render());
53
129
  connect() {
54
- this.#repaint.activate();
130
+ this.#repaint.observe(this.element);
55
131
  this.#render();
56
132
  }
57
- /** Closes the window in which a queued repaint may still run. */
58
133
  disconnect() {
59
- this.#repaint.cancel();
134
+ this.#repaint.disconnect();
60
135
  }
61
- /** Syncs a step appended or replaced at runtime (the consumer owns the list). */
62
136
  stepTargetConnected() {
63
137
  this.#repaint.schedule();
64
138
  }
65
- /** Re-derives the remaining steps when one is removed at runtime. */
66
139
  stepTargetDisconnected() {
67
140
  this.#repaint.schedule();
68
141
  }
69
- /** Repaints when application code (or a Turbo morph) changes `index` at runtime. */
70
142
  indexValueChanged() {
71
143
  this.#repaint.schedule();
72
144
  }
73
- /**
74
- * Moves the indicator from an external event (`detail.index`, 0-based) and
75
- * dispatches `change`. Out-of-range positions are clamped to the step set,
76
- * and both sides of the no-op test are clamped, so moving onto the step an
77
- * out-of-range `index` already renders is not reported as a change.
78
- */
79
145
  setIndex(event) {
80
146
  const next = event.detail?.index;
81
147
  if (typeof next !== "number" || !Number.isFinite(next)) return;
82
148
  const clamped = this.#clamp(next);
83
- const previous = this.#clamp(this.indexValue);
149
+ const previous = this.#clamp(this.#safeIndex);
84
150
  const moved = clamped !== previous;
85
151
  this.indexValue = clamped;
86
152
  if (!moved) return;
@@ -89,18 +155,9 @@ var StepIndicatorController = class extends Controller {
89
155
  detail: { index: clamped, previous, total: this.stepTargets.length }
90
156
  });
91
157
  }
92
- /**
93
- * Applies `data-state`, `aria-current`, and the progress ratio custom property.
94
- *
95
- * A pure function of the step set and `index`, so running it again writes the
96
- * same values — which is what lets the action path paint synchronously (the event
97
- * goes out after the DOM is updated) while a coalesced pass may still follow.
98
- *
99
- * @stimeoRenderRoot
100
- */
101
158
  #render() {
102
159
  const total = this.stepTargets.length;
103
- const current = this.#clamp(this.indexValue);
160
+ const current = this.#clamp(this.#safeIndex);
104
161
  this.stepTargets.forEach((step, index) => {
105
162
  step.dataset.state = index < current ? "complete" : index === current ? "current" : "upcoming";
106
163
  if (index === current) {
@@ -112,19 +169,21 @@ var StepIndicatorController = class extends Controller {
112
169
  const ratio = total > 1 ? current / (total - 1) : 0;
113
170
  this.element.style.setProperty("--stimeo--step-indicator-ratio", String(ratio));
114
171
  }
115
- /**
116
- * Constrains an index to `[0, total-1]` (or `0` when there are no steps). A
117
- * non-finite index falls back to the first step: `index` is read from markup,
118
- * so an unparsable attribute arrives as `NaN` and would otherwise propagate
119
- * into every state hook.
120
- */
121
172
  #clamp(index) {
122
173
  const last = this.stepTargets.length - 1;
123
- if (last < 0 || !Number.isFinite(index)) return 0;
174
+ if (last < 0) return 0;
124
175
  return Math.min(last, Math.max(0, Math.trunc(index)));
125
176
  }
177
+ get #safeIndex() {
178
+ return this.#numbers.read(
179
+ this,
180
+ "index",
181
+ this.indexValue,
182
+ _StepIndicatorController.values.index.default,
183
+ _StepIndicatorController.valueConstraints.index
184
+ );
185
+ }
126
186
  };
127
187
 
128
188
  export { StepIndicatorController };
129
- //# sourceMappingURL=step_indicator_controller.js.map
130
189
  //# sourceMappingURL=step_indicator_controller.js.map
@@ -1,28 +1,61 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/stepper_controller.ts
3
+ var NUMBER_BOUNDS = {
4
+ finite: { finite: true }};
5
+ function matchesNumberBounds(value, bounds) {
6
+ if (!Number.isFinite(value)) {
7
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
8
+ if (direction === null) return false;
9
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
10
+ }
11
+ if (bounds.min !== void 0 && value < bounds.min) return false;
12
+ if (bounds.max !== void 0 && value > bounds.max) return false;
13
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
14
+ if (bounds.integer && !Number.isInteger(value)) return false;
15
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
16
+ return true;
17
+ }
18
+
19
+ function toFiniteNumber(raw) {
20
+ if (typeof raw !== "number" && typeof raw !== "string") return null;
21
+ if (typeof raw === "string" && raw.trim().length === 0) return null;
22
+ const value = Number(raw);
23
+ return Number.isFinite(value) ? value : null;
24
+ }
25
+ function readNumber(raw, fallback, bounds) {
26
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
27
+ }
28
+
29
+ var ListenerSet = class {
30
+ #abort = new AbortController();
31
+ add(target, type, handler, options) {
32
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
33
+ }
34
+ dispose() {
35
+ this.#abort.abort();
36
+ this.#abort = new AbortController();
37
+ }
38
+ };
4
39
 
5
- // src/utils/microtask_coalescer.ts
6
40
  var MicrotaskCoalescer = class {
7
41
  #run;
8
42
  #queued = false;
9
43
  #active = false;
10
44
  #generation = 0;
11
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
12
45
  constructor(run) {
13
46
  this.#run = run;
14
47
  }
15
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
16
48
  activate() {
17
49
  this.#active = true;
18
50
  }
19
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
20
51
  cancel() {
21
52
  this.#active = false;
22
53
  this.#queued = false;
23
54
  this.#generation += 1;
24
55
  }
25
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
56
+ get pending() {
57
+ return this.#queued;
58
+ }
26
59
  schedule() {
27
60
  if (!this.#active || this.#queued) return;
28
61
  this.#queued = true;
@@ -35,95 +68,116 @@ var MicrotaskCoalescer = class {
35
68
  }
36
69
  };
37
70
 
38
- // src/controllers/stepper_controller.ts
39
- var StepperController = class extends Controller {
71
+ var MorphRenderWatcher = class {
72
+ #listeners = new ListenerSet();
73
+ #pass;
74
+ #element = null;
75
+ #onMorph = () => this.#pass.schedule();
76
+ constructor(run) {
77
+ this.#pass = new MicrotaskCoalescer(run);
78
+ }
79
+ observe(element) {
80
+ if (this.#element === element) return;
81
+ this.disconnect();
82
+ this.#element = element;
83
+ this.#pass.activate();
84
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
85
+ }
86
+ schedule() {
87
+ this.#pass.schedule();
88
+ }
89
+ get pending() {
90
+ return this.#pass.pending;
91
+ }
92
+ disconnect() {
93
+ this.#listeners.dispose();
94
+ this.#pass.cancel();
95
+ this.#element = null;
96
+ }
97
+ };
98
+
99
+ var NumberValueReader = class {
100
+ #lastRejected = /* @__PURE__ */ new Map();
101
+ read(owner, name, raw, fallback, bounds) {
102
+ const resolved = readNumber(raw, fallback, bounds);
103
+ if (matchesNumberBounds(raw, bounds)) {
104
+ this.#lastRejected.delete(name);
105
+ return resolved;
106
+ }
107
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
108
+ const literal = owner.element.getAttribute(attribute);
109
+ if (literal === null) {
110
+ this.#lastRejected.delete(name);
111
+ return resolved;
112
+ }
113
+ if (this.#lastRejected.get(name) !== literal) {
114
+ this.#lastRejected.set(name, literal);
115
+ console.warn(
116
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
117
+ );
118
+ }
119
+ return resolved;
120
+ }
121
+ };
122
+
123
+ var StepperController = class _StepperController extends Controller {
124
+ #numbers = new NumberValueReader();
40
125
  static targets = ["step"];
41
126
  static values = {
42
127
  index: { type: Number, default: 0 },
43
128
  linear: { type: Boolean, default: false }
44
129
  };
130
+ static valueConstraints = {
131
+ index: NUMBER_BOUNDS.finite
132
+ };
45
133
  static actions = ["goto", "next", "prev"];
46
134
  static events = ["change", "reconcile"];
47
- /**
48
- * Collapses the Value and target callbacks of one mutation into one pass, and
49
- * refuses the ones Stimulus delivers before `connect()`, which renders itself.
50
- */
51
- #repaint = new MicrotaskCoalescer(() => this.#reconcileStep());
52
- /**
53
- * The position shown current last, which the next move is measured from, or
54
- * `null` before any step has been shown. While no step is present it keeps the
55
- * last one.
56
- */
135
+ #repaint = new MorphRenderWatcher(() => this.#reconcileStep());
57
136
  #shown = null;
58
- /** Renders the initial state from `index`, clamped into the step range. */
59
137
  connect() {
60
- this.#repaint.activate();
138
+ this.#repaint.observe(this.element);
61
139
  this.#shown = this.#currentStep;
62
140
  this.#render();
63
141
  }
64
- /** Drops a pending pass, so nothing renders or reports for a stepper that left. */
65
142
  disconnect() {
66
- this.#repaint.cancel();
143
+ this.#repaint.disconnect();
67
144
  }
68
- /** Re-renders when Turbo Morph or application code changes `index` at runtime. */
69
145
  indexValueChanged() {
70
146
  this.#repaint.schedule();
71
147
  }
72
- /** Re-derives every step's state for a step added at runtime. */
73
148
  stepTargetConnected() {
74
149
  this.#repaint.schedule();
75
150
  }
76
- /** Re-derives every step's state for a step removed at runtime. */
77
151
  stepTargetDisconnected() {
78
152
  this.#repaint.schedule();
79
153
  }
80
- /** Advances to the next step (ignored at the last step). */
81
154
  next() {
82
- this.#moveTo(this.#clampIndex(this.indexValue) + 1);
155
+ this.#moveTo(this.#clampIndex(this.#safeIndex) + 1);
83
156
  }
84
- /** Returns to the previous step (ignored at the first step). */
85
157
  prev() {
86
- this.#moveTo(this.#clampIndex(this.indexValue) - 1);
158
+ this.#moveTo(this.#clampIndex(this.#safeIndex) - 1);
87
159
  }
88
- /**
89
- * Jumps to the step carried in the action's `index` param. A param that is empty,
90
- * or that Stimulus parses into anything but a number or a string, is no index,
91
- * so it jumps nowhere.
92
- */
93
160
  goto(event) {
94
161
  const { index } = event.params;
95
- if (index === "") return;
96
- if (typeof index !== "number" && typeof index !== "string") return;
97
- const target = Number(index);
98
- if (!Number.isFinite(target) || !Number.isInteger(target)) return;
162
+ const target = toFiniteNumber(index);
163
+ if (target === null || !Number.isInteger(target)) return;
99
164
  this.#moveTo(target);
100
165
  }
101
- /**
102
- * Moves the current step to `target` when allowed: in range, not a no-op, and
103
- * — under `linear` — not skipping more than one step ahead. Re-renders and
104
- * dispatches `change`.
105
- */
106
166
  #moveTo(target) {
107
167
  const total = this.stepTargets.length;
108
- if (!Number.isFinite(target) || !Number.isInteger(target)) return;
109
168
  if (target < 0 || target >= total) return;
110
- const current = this.#clampIndex(this.indexValue);
111
- if (target === current) return;
169
+ const current = this.#clampIndex(this.#safeIndex);
170
+ if (target === current && target === this.#shown) return;
112
171
  if (this.linearValue && target > current + 1) return;
113
- const previous = current;
172
+ const previous = this.#shown ?? current;
114
173
  this.indexValue = target;
115
174
  this.#shown = target;
116
175
  this.#render();
176
+ if (target === previous) return;
117
177
  this.dispatch("change", {
118
178
  detail: { index: target, previous, total, step: this.stepTargets[target] }
119
179
  });
120
180
  }
121
- /**
122
- * Re-derives one settled batch of Value and target changes, and reports a
123
- * current position that moved from the one shown before as `reconcile`. With no
124
- * step present there is nothing to report, and the position shown last stays the
125
- * one the next move is measured from.
126
- */
127
181
  #reconcileStep() {
128
182
  const previous = this.#shown;
129
183
  const index = this.#currentStep;
@@ -135,25 +189,11 @@ var StepperController = class extends Controller {
135
189
  detail: { index, previous, total: steps.length, step: steps[index] }
136
190
  });
137
191
  }
138
- /** The current position, clamped into the step range, or `null` when there is no step. */
139
192
  get #currentStep() {
140
- return this.stepTargets.length > 0 ? this.#clampIndex(this.indexValue) : null;
141
- }
142
- /**
143
- * Derives each step's `data-state` and the current button's `aria-current` from
144
- * `index`, clamped into the step range, without dispatching an action event.
145
- *
146
- * The clamp decides only what is shown. This body never writes `index`, so an
147
- * out-of-range declaration stays in the attribute as the page wrote it.
148
- *
149
- * `aria-current="step"` is placed on the step's **first** `<button>`; the markup
150
- * contract assumes one operable button per step. If a step needs multiple
151
- * buttons, mark the navigational one first (or this would target the wrong one).
152
- *
153
- * @stimeoRenderRoot
154
- */
193
+ return this.stepTargets.length > 0 ? this.#clampIndex(this.#safeIndex) : null;
194
+ }
155
195
  #render() {
156
- const current = this.#clampIndex(this.indexValue);
196
+ const current = this.#clampIndex(this.#safeIndex);
157
197
  this.stepTargets.forEach((step, index) => {
158
198
  step.dataset.state = index < current ? "complete" : index === current ? "current" : "upcoming";
159
199
  const button = step.querySelector("button");
@@ -165,14 +205,21 @@ var StepperController = class extends Controller {
165
205
  }
166
206
  });
167
207
  }
168
- /** Constrains an index to `[0, total-1]` (or `0` when there are no steps). */
169
208
  #clampIndex(index) {
170
209
  const last = this.stepTargets.length - 1;
171
- if (last < 0 || !Number.isFinite(index)) return 0;
210
+ if (last < 0) return 0;
172
211
  return Math.min(last, Math.max(0, Math.trunc(index)));
173
212
  }
213
+ get #safeIndex() {
214
+ return this.#numbers.read(
215
+ this,
216
+ "index",
217
+ this.indexValue,
218
+ _StepperController.values.index.default,
219
+ _StepperController.valueConstraints.index
220
+ );
221
+ }
174
222
  };
175
223
 
176
224
  export { StepperController };
177
- //# sourceMappingURL=stepper_controller.js.map
178
225
  //# sourceMappingURL=stepper_controller.js.map