stimeo-ui 0.16.0 → 0.17.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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/README.md +8 -1
  4. data/dist/cable/index.js +363 -436
  5. data/dist/controllers/accordion_controller.js +166 -55
  6. data/dist/controllers/alert_dialog_controller.js +862 -221
  7. data/dist/controllers/announcer_controller.js +135 -198
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +271 -127
  10. data/dist/controllers/avatar_controller.js +171 -105
  11. data/dist/controllers/breadcrumb_controller.js +0 -130
  12. data/dist/controllers/bulk_select_controller.js +262 -52
  13. data/dist/controllers/calendar_controller.js +175 -209
  14. data/dist/controllers/carousel_controller.js +461 -399
  15. data/dist/controllers/character_counter_controller.js +246 -156
  16. data/dist/controllers/checkbox_controller.js +32 -74
  17. data/dist/controllers/clipboard_controller.js +168 -147
  18. data/dist/controllers/collapsible_controller.js +301 -120
  19. data/dist/controllers/color_picker_controller.js +149 -139
  20. data/dist/controllers/combobox_controller.js +360 -133
  21. data/dist/controllers/command_palette_controller.js +970 -358
  22. data/dist/controllers/conditional_fields_controller.js +166 -124
  23. data/dist/controllers/confirm_controller.js +939 -248
  24. data/dist/controllers/context_menu_controller.js +256 -128
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +183 -148
  27. data/dist/controllers/currency_input_controller.js +134 -158
  28. data/dist/controllers/data_grid_controller.js +114 -186
  29. data/dist/controllers/date_range_picker_controller.js +288 -206
  30. data/dist/controllers/dialog_controller.js +858 -223
  31. data/dist/controllers/direct_upload_controller.js +73 -153
  32. data/dist/controllers/dirty_form_controller.js +14 -48
  33. data/dist/controllers/dismissible_controller.js +175 -21
  34. data/dist/controllers/drawer_controller.js +933 -341
  35. data/dist/controllers/dropdown_controller.js +312 -82
  36. data/dist/controllers/editable_controller.js +158 -68
  37. data/dist/controllers/empty_state_controller.js +67 -53
  38. data/dist/controllers/file_dropzone_controller.js +311 -207
  39. data/dist/controllers/filter_controller.js +67 -39
  40. data/dist/controllers/flash_controller.js +172 -314
  41. data/dist/controllers/focus_controller.js +654 -240
  42. data/dist/controllers/form_field_controller.js +147 -132
  43. data/dist/controllers/form_validation_controller.js +13 -86
  44. data/dist/controllers/frame_loading_controller.js +264 -213
  45. data/dist/controllers/highlight_controller.js +107 -75
  46. data/dist/controllers/hover_card_controller.js +309 -118
  47. data/dist/controllers/idle_controller.js +267 -101
  48. data/dist/controllers/input_mask_controller.js +71 -72
  49. data/dist/controllers/intersection_controller.js +147 -106
  50. data/dist/controllers/lazy_frame_controller.js +9 -65
  51. data/dist/controllers/listbox_controller.js +306 -204
  52. data/dist/controllers/local_time_controller.js +54 -64
  53. data/dist/controllers/masonry_controller.js +70 -93
  54. data/dist/controllers/menu_controller.js +214 -156
  55. data/dist/controllers/menubar_controller.js +170 -329
  56. data/dist/controllers/meter_controller.js +151 -67
  57. data/dist/controllers/multi_select_controller.js +593 -358
  58. data/dist/controllers/navigation_menu_controller.js +124 -232
  59. data/dist/controllers/nested_form_controller.js +352 -182
  60. data/dist/controllers/network_status_controller.js +242 -54
  61. data/dist/controllers/number_input_controller.js +320 -263
  62. data/dist/controllers/optimistic_controller.js +16 -76
  63. data/dist/controllers/otp_controller.js +198 -193
  64. data/dist/controllers/overflow_indicator_controller.js +116 -113
  65. data/dist/controllers/overflow_menu_controller.js +343 -283
  66. data/dist/controllers/pagination_controller.js +295 -119
  67. data/dist/controllers/password_reveal_controller.js +389 -121
  68. data/dist/controllers/password_strength_controller.js +99 -153
  69. data/dist/controllers/persist_controller.js +237 -103
  70. data/dist/controllers/pointer_drag_controller.js +185 -165
  71. data/dist/controllers/popover_controller.js +236 -97
  72. data/dist/controllers/portal_controller.js +209 -76
  73. data/dist/controllers/preview_guard_controller.js +232 -116
  74. data/dist/controllers/progress_controller.js +125 -56
  75. data/dist/controllers/radio_group_controller.js +117 -137
  76. data/dist/controllers/range_slider_controller.js +192 -105
  77. data/dist/controllers/rating_controller.js +418 -207
  78. data/dist/controllers/read_more_controller.js +134 -63
  79. data/dist/controllers/reading_progress_controller.js +147 -135
  80. data/dist/controllers/relative_time_controller.js +145 -110
  81. data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
  82. data/dist/controllers/resizable_controller.js +255 -139
  83. data/dist/controllers/roving_controller.js +39 -84
  84. data/dist/controllers/scroll_area_controller.js +428 -225
  85. data/dist/controllers/scroll_restore_controller.js +0 -53
  86. data/dist/controllers/scroll_visibility_controller.js +395 -145
  87. data/dist/controllers/scrollspy_controller.js +181 -242
  88. data/dist/controllers/separator_controller.js +267 -140
  89. data/dist/controllers/sidebar_controller.js +1023 -386
  90. data/dist/controllers/skeleton_controller.js +248 -97
  91. data/dist/controllers/slider_controller.js +159 -79
  92. data/dist/controllers/smart_sticky_header_controller.js +162 -63
  93. data/dist/controllers/sortable_controller.js +0 -112
  94. data/dist/controllers/spinner_controller.js +277 -169
  95. data/dist/controllers/step_indicator_controller.js +99 -46
  96. data/dist/controllers/stepper_controller.js +114 -73
  97. data/dist/controllers/stick_to_bottom_controller.js +262 -138
  98. data/dist/controllers/sticky_observer_controller.js +214 -49
  99. data/dist/controllers/submit_once_controller.js +294 -205
  100. data/dist/controllers/switch_controller.js +33 -60
  101. data/dist/controllers/tabs_controller.js +35 -29
  102. data/dist/controllers/tags_input_controller.js +136 -145
  103. data/dist/controllers/textarea_autosize_controller.js +71 -58
  104. data/dist/controllers/theme_controller.js +130 -112
  105. data/dist/controllers/time_picker_controller.js +146 -80
  106. data/dist/controllers/toast_controller.js +186 -297
  107. data/dist/controllers/toggle_group_controller.js +95 -136
  108. data/dist/controllers/toolbar_controller.js +68 -98
  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 +150 -345
  112. data/dist/index.js +6370 -9346
  113. data/dist/positioning/index.js +148 -47
  114. data/lib/stimeo/ui/version.rb +1 -1
  115. metadata +3 -3
@@ -1,28 +1,32 @@
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. */
26
30
  schedule() {
27
31
  if (!this.#active || this.#queued) return;
28
32
  this.#queued = true;
@@ -35,52 +39,108 @@ var MicrotaskCoalescer = class {
35
39
  }
36
40
  };
37
41
 
38
- // src/controllers/step_indicator_controller.ts
39
- var StepIndicatorController = class extends Controller {
42
+ var MorphRenderWatcher = class {
43
+ #listeners = new ListenerSet();
44
+ #pass;
45
+ #element = null;
46
+ #onMorph = () => this.#pass.schedule();
47
+ constructor(run) {
48
+ this.#pass = new MicrotaskCoalescer(run);
49
+ }
50
+ observe(element) {
51
+ if (this.#element === element) return;
52
+ this.disconnect();
53
+ this.#element = element;
54
+ this.#pass.activate();
55
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
56
+ }
57
+ schedule() {
58
+ this.#pass.schedule();
59
+ }
60
+ disconnect() {
61
+ this.#listeners.dispose();
62
+ this.#pass.cancel();
63
+ this.#element = null;
64
+ }
65
+ };
66
+
67
+ var NUMBER_BOUNDS = {
68
+ finite: { finite: true }};
69
+ function matchesNumberBounds(value, bounds) {
70
+ if (!Number.isFinite(value)) {
71
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
72
+ if (direction === null) return false;
73
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
74
+ }
75
+ if (bounds.min !== void 0 && value < bounds.min) return false;
76
+ if (bounds.max !== void 0 && value > bounds.max) return false;
77
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
78
+ if (bounds.integer && !Number.isInteger(value)) return false;
79
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
80
+ return true;
81
+ }
82
+
83
+ function readNumber(raw, fallback, bounds) {
84
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
85
+ }
86
+
87
+ var NumberValueReader = class {
88
+ #lastRejected = /* @__PURE__ */ new Map();
89
+ read(owner, name, raw, fallback, bounds) {
90
+ const resolved = readNumber(raw, fallback, bounds);
91
+ if (matchesNumberBounds(raw, bounds)) {
92
+ this.#lastRejected.delete(name);
93
+ return resolved;
94
+ }
95
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
96
+ const literal = owner.element.getAttribute(attribute);
97
+ if (literal === null) {
98
+ this.#lastRejected.delete(name);
99
+ return resolved;
100
+ }
101
+ if (this.#lastRejected.get(name) !== literal) {
102
+ this.#lastRejected.set(name, literal);
103
+ console.warn(
104
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
105
+ );
106
+ }
107
+ return resolved;
108
+ }
109
+ };
110
+
111
+ var StepIndicatorController = class _StepIndicatorController extends Controller {
112
+ #numbers = new NumberValueReader();
40
113
  static targets = ["step"];
41
114
  static values = {
42
115
  index: { type: Number, default: 0 }
43
116
  };
117
+ static valueConstraints = {
118
+ index: NUMBER_BOUNDS.finite
119
+ };
44
120
  static actions = ["setIndex"];
45
121
  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. */
122
+ #repaint = new MorphRenderWatcher(() => this.#render());
53
123
  connect() {
54
- this.#repaint.activate();
124
+ this.#repaint.observe(this.element);
55
125
  this.#render();
56
126
  }
57
- /** Closes the window in which a queued repaint may still run. */
58
127
  disconnect() {
59
- this.#repaint.cancel();
128
+ this.#repaint.disconnect();
60
129
  }
61
- /** Syncs a step appended or replaced at runtime (the consumer owns the list). */
62
130
  stepTargetConnected() {
63
131
  this.#repaint.schedule();
64
132
  }
65
- /** Re-derives the remaining steps when one is removed at runtime. */
66
133
  stepTargetDisconnected() {
67
134
  this.#repaint.schedule();
68
135
  }
69
- /** Repaints when application code (or a Turbo morph) changes `index` at runtime. */
70
136
  indexValueChanged() {
71
137
  this.#repaint.schedule();
72
138
  }
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
139
  setIndex(event) {
80
140
  const next = event.detail?.index;
81
141
  if (typeof next !== "number" || !Number.isFinite(next)) return;
82
142
  const clamped = this.#clamp(next);
83
- const previous = this.#clamp(this.indexValue);
143
+ const previous = this.#clamp(this.#safeIndex);
84
144
  const moved = clamped !== previous;
85
145
  this.indexValue = clamped;
86
146
  if (!moved) return;
@@ -89,18 +149,9 @@ var StepIndicatorController = class extends Controller {
89
149
  detail: { index: clamped, previous, total: this.stepTargets.length }
90
150
  });
91
151
  }
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
152
  #render() {
102
153
  const total = this.stepTargets.length;
103
- const current = this.#clamp(this.indexValue);
154
+ const current = this.#clamp(this.#safeIndex);
104
155
  this.stepTargets.forEach((step, index) => {
105
156
  step.dataset.state = index < current ? "complete" : index === current ? "current" : "upcoming";
106
157
  if (index === current) {
@@ -112,19 +163,21 @@ var StepIndicatorController = class extends Controller {
112
163
  const ratio = total > 1 ? current / (total - 1) : 0;
113
164
  this.element.style.setProperty("--stimeo--step-indicator-ratio", String(ratio));
114
165
  }
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
166
  #clamp(index) {
122
167
  const last = this.stepTargets.length - 1;
123
- if (last < 0 || !Number.isFinite(index)) return 0;
168
+ if (last < 0) return 0;
124
169
  return Math.min(last, Math.max(0, Math.trunc(index)));
125
170
  }
171
+ get #safeIndex() {
172
+ return this.#numbers.read(
173
+ this,
174
+ "index",
175
+ this.indexValue,
176
+ _StepIndicatorController.values.index.default,
177
+ _StepIndicatorController.valueConstraints.index
178
+ );
179
+ }
126
180
  };
127
181
 
128
182
  export { StepIndicatorController };
129
- //# sourceMappingURL=step_indicator_controller.js.map
130
183
  //# sourceMappingURL=step_indicator_controller.js.map
@@ -1,28 +1,58 @@
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. */
26
56
  schedule() {
27
57
  if (!this.#active || this.#queued) return;
28
58
  this.#queued = true;
@@ -35,95 +65,113 @@ var MicrotaskCoalescer = class {
35
65
  }
36
66
  };
37
67
 
38
- // src/controllers/stepper_controller.ts
39
- var StepperController = class extends Controller {
68
+ var MorphRenderWatcher = class {
69
+ #listeners = new ListenerSet();
70
+ #pass;
71
+ #element = null;
72
+ #onMorph = () => this.#pass.schedule();
73
+ constructor(run) {
74
+ this.#pass = new MicrotaskCoalescer(run);
75
+ }
76
+ observe(element) {
77
+ if (this.#element === element) return;
78
+ this.disconnect();
79
+ this.#element = element;
80
+ this.#pass.activate();
81
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
82
+ }
83
+ schedule() {
84
+ this.#pass.schedule();
85
+ }
86
+ disconnect() {
87
+ this.#listeners.dispose();
88
+ this.#pass.cancel();
89
+ this.#element = null;
90
+ }
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 StepperController = class _StepperController extends Controller {
118
+ #numbers = new NumberValueReader();
40
119
  static targets = ["step"];
41
120
  static values = {
42
121
  index: { type: Number, default: 0 },
43
122
  linear: { type: Boolean, default: false }
44
123
  };
124
+ static valueConstraints = {
125
+ index: NUMBER_BOUNDS.finite
126
+ };
45
127
  static actions = ["goto", "next", "prev"];
46
128
  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
- */
129
+ #repaint = new MorphRenderWatcher(() => this.#reconcileStep());
57
130
  #shown = null;
58
- /** Renders the initial state from `index`, clamped into the step range. */
59
131
  connect() {
60
- this.#repaint.activate();
132
+ this.#repaint.observe(this.element);
61
133
  this.#shown = this.#currentStep;
62
134
  this.#render();
63
135
  }
64
- /** Drops a pending pass, so nothing renders or reports for a stepper that left. */
65
136
  disconnect() {
66
- this.#repaint.cancel();
137
+ this.#repaint.disconnect();
67
138
  }
68
- /** Re-renders when Turbo Morph or application code changes `index` at runtime. */
69
139
  indexValueChanged() {
70
140
  this.#repaint.schedule();
71
141
  }
72
- /** Re-derives every step's state for a step added at runtime. */
73
142
  stepTargetConnected() {
74
143
  this.#repaint.schedule();
75
144
  }
76
- /** Re-derives every step's state for a step removed at runtime. */
77
145
  stepTargetDisconnected() {
78
146
  this.#repaint.schedule();
79
147
  }
80
- /** Advances to the next step (ignored at the last step). */
81
148
  next() {
82
- this.#moveTo(this.#clampIndex(this.indexValue) + 1);
149
+ this.#moveTo(this.#clampIndex(this.#safeIndex) + 1);
83
150
  }
84
- /** Returns to the previous step (ignored at the first step). */
85
151
  prev() {
86
- this.#moveTo(this.#clampIndex(this.indexValue) - 1);
152
+ this.#moveTo(this.#clampIndex(this.#safeIndex) - 1);
87
153
  }
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
154
  goto(event) {
94
155
  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;
156
+ const target = toFiniteNumber(index);
157
+ if (target === null || !Number.isInteger(target)) return;
99
158
  this.#moveTo(target);
100
159
  }
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
160
  #moveTo(target) {
107
161
  const total = this.stepTargets.length;
108
- if (!Number.isFinite(target) || !Number.isInteger(target)) return;
109
162
  if (target < 0 || target >= total) return;
110
- const current = this.#clampIndex(this.indexValue);
111
- if (target === current) return;
163
+ const current = this.#clampIndex(this.#safeIndex);
164
+ if (target === current && target === this.#shown) return;
112
165
  if (this.linearValue && target > current + 1) return;
113
- const previous = current;
166
+ const previous = this.#shown ?? current;
114
167
  this.indexValue = target;
115
168
  this.#shown = target;
116
169
  this.#render();
170
+ if (target === previous) return;
117
171
  this.dispatch("change", {
118
172
  detail: { index: target, previous, total, step: this.stepTargets[target] }
119
173
  });
120
174
  }
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
175
  #reconcileStep() {
128
176
  const previous = this.#shown;
129
177
  const index = this.#currentStep;
@@ -135,25 +183,11 @@ var StepperController = class extends Controller {
135
183
  detail: { index, previous, total: steps.length, step: steps[index] }
136
184
  });
137
185
  }
138
- /** The current position, clamped into the step range, or `null` when there is no step. */
139
186
  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
- */
187
+ return this.stepTargets.length > 0 ? this.#clampIndex(this.#safeIndex) : null;
188
+ }
155
189
  #render() {
156
- const current = this.#clampIndex(this.indexValue);
190
+ const current = this.#clampIndex(this.#safeIndex);
157
191
  this.stepTargets.forEach((step, index) => {
158
192
  step.dataset.state = index < current ? "complete" : index === current ? "current" : "upcoming";
159
193
  const button = step.querySelector("button");
@@ -165,14 +199,21 @@ var StepperController = class extends Controller {
165
199
  }
166
200
  });
167
201
  }
168
- /** Constrains an index to `[0, total-1]` (or `0` when there are no steps). */
169
202
  #clampIndex(index) {
170
203
  const last = this.stepTargets.length - 1;
171
- if (last < 0 || !Number.isFinite(index)) return 0;
204
+ if (last < 0) return 0;
172
205
  return Math.min(last, Math.max(0, Math.trunc(index)));
173
206
  }
207
+ get #safeIndex() {
208
+ return this.#numbers.read(
209
+ this,
210
+ "index",
211
+ this.indexValue,
212
+ _StepperController.values.index.default,
213
+ _StepperController.valueConstraints.index
214
+ );
215
+ }
174
216
  };
175
217
 
176
218
  export { StepperController };
177
- //# sourceMappingURL=stepper_controller.js.map
178
219
  //# sourceMappingURL=stepper_controller.js.map