stimeo-ui 0.15.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 +209 -0
  3. data/README.md +128 -1
  4. data/dist/cable/index.js +392 -417
  5. data/dist/controllers/accordion_controller.js +228 -28
  6. data/dist/controllers/alert_dialog_controller.js +862 -221
  7. data/dist/controllers/announcer_controller.js +135 -191
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +295 -110
  10. data/dist/controllers/avatar_controller.js +172 -106
  11. data/dist/controllers/breadcrumb_controller.js +0 -129
  12. data/dist/controllers/bulk_select_controller.js +264 -53
  13. data/dist/controllers/calendar_controller.js +346 -180
  14. data/dist/controllers/carousel_controller.js +587 -300
  15. data/dist/controllers/character_counter_controller.js +271 -143
  16. data/dist/controllers/checkbox_controller.js +77 -75
  17. data/dist/controllers/clipboard_controller.js +187 -108
  18. data/dist/controllers/collapsible_controller.js +374 -108
  19. data/dist/controllers/color_picker_controller.js +183 -127
  20. data/dist/controllers/combobox_controller.js +426 -109
  21. data/dist/controllers/command_palette_controller.js +994 -350
  22. data/dist/controllers/conditional_fields_controller.js +212 -125
  23. data/dist/controllers/confirm_controller.js +939 -245
  24. data/dist/controllers/context_menu_controller.js +275 -127
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +235 -97
  27. data/dist/controllers/currency_input_controller.js +241 -118
  28. data/dist/controllers/data_grid_controller.js +229 -135
  29. data/dist/controllers/date_range_picker_controller.js +359 -165
  30. data/dist/controllers/dialog_controller.js +887 -225
  31. data/dist/controllers/direct_upload_controller.js +80 -142
  32. data/dist/controllers/dirty_form_controller.js +26 -47
  33. data/dist/controllers/dismissible_controller.js +175 -20
  34. data/dist/controllers/drawer_controller.js +963 -336
  35. data/dist/controllers/dropdown_controller.js +342 -85
  36. data/dist/controllers/editable_controller.js +182 -58
  37. data/dist/controllers/empty_state_controller.js +67 -53
  38. data/dist/controllers/file_dropzone_controller.js +426 -229
  39. data/dist/controllers/filter_controller.js +78 -36
  40. data/dist/controllers/flash_controller.js +328 -211
  41. data/dist/controllers/focus_controller.js +654 -239
  42. data/dist/controllers/form_field_controller.js +153 -136
  43. data/dist/controllers/form_validation_controller.js +29 -96
  44. data/dist/controllers/frame_loading_controller.js +263 -198
  45. data/dist/controllers/highlight_controller.js +119 -70
  46. data/dist/controllers/hover_card_controller.js +335 -118
  47. data/dist/controllers/idle_controller.js +316 -65
  48. data/dist/controllers/input_mask_controller.js +116 -61
  49. data/dist/controllers/intersection_controller.js +147 -103
  50. data/dist/controllers/lazy_frame_controller.js +9 -56
  51. data/dist/controllers/listbox_controller.js +413 -153
  52. data/dist/controllers/local_time_controller.js +54 -64
  53. data/dist/controllers/masonry_controller.js +70 -95
  54. data/dist/controllers/menu_controller.js +254 -167
  55. data/dist/controllers/menubar_controller.js +206 -331
  56. data/dist/controllers/meter_controller.js +159 -71
  57. data/dist/controllers/multi_select_controller.js +797 -388
  58. data/dist/controllers/navigation_menu_controller.js +156 -231
  59. data/dist/controllers/nested_form_controller.js +373 -174
  60. data/dist/controllers/network_status_controller.js +241 -45
  61. data/dist/controllers/number_input_controller.js +387 -227
  62. data/dist/controllers/optimistic_controller.js +28 -75
  63. data/dist/controllers/otp_controller.js +344 -196
  64. data/dist/controllers/overflow_indicator_controller.js +167 -93
  65. data/dist/controllers/overflow_menu_controller.js +408 -277
  66. data/dist/controllers/pagination_controller.js +342 -120
  67. data/dist/controllers/password_reveal_controller.js +424 -99
  68. data/dist/controllers/password_strength_controller.js +99 -153
  69. data/dist/controllers/persist_controller.js +252 -96
  70. data/dist/controllers/pointer_drag_controller.js +263 -164
  71. data/dist/controllers/popover_controller.js +267 -94
  72. data/dist/controllers/portal_controller.js +209 -72
  73. data/dist/controllers/preview_guard_controller.js +243 -112
  74. data/dist/controllers/progress_controller.js +132 -59
  75. data/dist/controllers/radio_group_controller.js +138 -133
  76. data/dist/controllers/range_slider_controller.js +245 -112
  77. data/dist/controllers/rating_controller.js +429 -194
  78. data/dist/controllers/read_more_controller.js +185 -47
  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 +294 -118
  83. data/dist/controllers/roving_controller.js +39 -84
  84. data/dist/controllers/scroll_area_controller.js +428 -221
  85. data/dist/controllers/scroll_restore_controller.js +0 -53
  86. data/dist/controllers/scroll_visibility_controller.js +395 -146
  87. data/dist/controllers/scrollspy_controller.js +181 -236
  88. data/dist/controllers/separator_controller.js +288 -132
  89. data/dist/controllers/sidebar_controller.js +1071 -375
  90. data/dist/controllers/skeleton_controller.js +248 -92
  91. data/dist/controllers/slider_controller.js +211 -96
  92. data/dist/controllers/smart_sticky_header_controller.js +163 -54
  93. data/dist/controllers/sortable_controller.js +13 -110
  94. data/dist/controllers/spinner_controller.js +277 -161
  95. data/dist/controllers/step_indicator_controller.js +107 -53
  96. data/dist/controllers/stepper_controller.js +171 -48
  97. data/dist/controllers/stick_to_bottom_controller.js +310 -116
  98. data/dist/controllers/sticky_observer_controller.js +214 -49
  99. data/dist/controllers/submit_once_controller.js +294 -192
  100. data/dist/controllers/switch_controller.js +107 -43
  101. data/dist/controllers/tabs_controller.js +50 -25
  102. data/dist/controllers/tags_input_controller.js +290 -149
  103. data/dist/controllers/textarea_autosize_controller.js +71 -58
  104. data/dist/controllers/theme_controller.js +148 -80
  105. data/dist/controllers/time_picker_controller.js +162 -81
  106. data/dist/controllers/toast_controller.js +365 -193
  107. data/dist/controllers/toggle_group_controller.js +194 -99
  108. data/dist/controllers/toolbar_controller.js +92 -90
  109. data/dist/controllers/tooltip_controller.js +185 -125
  110. data/dist/controllers/transition_controller.js +123 -115
  111. data/dist/controllers/tree_view_controller.js +248 -290
  112. data/dist/index.js +8649 -8572
  113. data/dist/positioning/index.js +148 -45
  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,71 +39,119 @@ 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
- current: { type: Number, default: 0 }
115
+ index: { type: Number, default: 0 }
43
116
  };
44
- static actions = ["setCurrent"];
117
+ static valueConstraints = {
118
+ index: NUMBER_BOUNDS.finite
119
+ };
120
+ static actions = ["setIndex"];
45
121
  static events = ["change"];
46
- /**
47
- * Collapses a batch of step callbacks — and a morph that swaps `current` 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 `current` 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 `current` at runtime. */
70
- currentValueChanged() {
136
+ indexValueChanged() {
71
137
  this.#repaint.schedule();
72
138
  }
73
- /**
74
- * Updates the current step from an external event (`detail.current`, 0-based)
75
- * and dispatches `change`. Out-of-range indices 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 `current` already renders is not reported as a change.
78
- */
79
- setCurrent(event) {
80
- const next = event.detail?.current;
139
+ setIndex(event) {
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 moved = clamped !== this.#clamp(this.currentValue);
84
- this.currentValue = clamped;
143
+ const previous = this.#clamp(this.#safeIndex);
144
+ const moved = clamped !== previous;
145
+ this.indexValue = clamped;
85
146
  if (!moved) return;
86
147
  this.#render();
87
148
  this.dispatch("change", {
88
- detail: { current: clamped, total: this.stepTargets.length }
149
+ detail: { index: clamped, previous, total: this.stepTargets.length }
89
150
  });
90
151
  }
91
- /**
92
- * Applies `data-state`, `aria-current`, and the progress ratio custom property.
93
- *
94
- * A pure function of the step set and `current`, so running it again writes the
95
- * same values — which is what lets the action path paint synchronously (the event
96
- * goes out after the DOM is updated) while a coalesced pass may still follow.
97
- *
98
- * @stimeoRenderRoot
99
- */
100
152
  #render() {
101
153
  const total = this.stepTargets.length;
102
- const current = this.#clamp(this.currentValue);
154
+ const current = this.#clamp(this.#safeIndex);
103
155
  this.stepTargets.forEach((step, index) => {
104
156
  step.dataset.state = index < current ? "complete" : index === current ? "current" : "upcoming";
105
157
  if (index === current) {
@@ -111,19 +163,21 @@ var StepIndicatorController = class extends Controller {
111
163
  const ratio = total > 1 ? current / (total - 1) : 0;
112
164
  this.element.style.setProperty("--stimeo--step-indicator-ratio", String(ratio));
113
165
  }
114
- /**
115
- * Constrains an index to `[0, total-1]` (or `0` when there are no steps). A
116
- * non-finite index falls back to the first step: `current` is read from markup,
117
- * so an unparsable attribute arrives as `NaN` and would otherwise propagate
118
- * into every state hook.
119
- */
120
166
  #clamp(index) {
121
167
  const last = this.stepTargets.length - 1;
122
- if (last < 0 || !Number.isFinite(index)) return 0;
168
+ if (last < 0) return 0;
123
169
  return Math.min(last, Math.max(0, Math.trunc(index)));
124
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
+ }
125
180
  };
126
181
 
127
182
  export { StepIndicatorController };
128
- //# sourceMappingURL=step_indicator_controller.js.map
129
183
  //# sourceMappingURL=step_indicator_controller.js.map
@@ -1,77 +1,193 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/stepper_controller.ts
4
- var StepperController = class extends Controller {
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
+ };
39
+
40
+ var MicrotaskCoalescer = class {
41
+ #run;
42
+ #queued = false;
43
+ #active = false;
44
+ #generation = 0;
45
+ constructor(run) {
46
+ this.#run = run;
47
+ }
48
+ activate() {
49
+ this.#active = true;
50
+ }
51
+ cancel() {
52
+ this.#active = false;
53
+ this.#queued = false;
54
+ this.#generation += 1;
55
+ }
56
+ schedule() {
57
+ if (!this.#active || this.#queued) return;
58
+ this.#queued = true;
59
+ const generation = this.#generation;
60
+ queueMicrotask(() => {
61
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
62
+ this.#queued = false;
63
+ this.#run();
64
+ });
65
+ }
66
+ };
67
+
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();
5
119
  static targets = ["step"];
6
120
  static values = {
7
121
  index: { type: Number, default: 0 },
8
122
  linear: { type: Boolean, default: false }
9
123
  };
124
+ static valueConstraints = {
125
+ index: NUMBER_BOUNDS.finite
126
+ };
10
127
  static actions = ["goto", "next", "prev"];
11
- static events = ["change"];
12
- #isConnected = false;
13
- /** Normalizes an out-of-range initial `index` and renders the initial state. */
128
+ static events = ["change", "reconcile"];
129
+ #repaint = new MorphRenderWatcher(() => this.#reconcileStep());
130
+ #shown = null;
14
131
  connect() {
15
- this.#isConnected = true;
16
- this.#normalizeAndRender();
132
+ this.#repaint.observe(this.element);
133
+ this.#shown = this.#currentStep;
134
+ this.#render();
17
135
  }
18
136
  disconnect() {
19
- this.#isConnected = false;
137
+ this.#repaint.disconnect();
20
138
  }
21
- /** Re-renders when Turbo Morph or application code changes `index` at runtime. */
22
139
  indexValueChanged() {
23
- if (!this.#isConnected) return;
24
- this.#normalizeAndRender();
140
+ this.#repaint.schedule();
141
+ }
142
+ stepTargetConnected() {
143
+ this.#repaint.schedule();
144
+ }
145
+ stepTargetDisconnected() {
146
+ this.#repaint.schedule();
25
147
  }
26
- /** Advances to the next step (ignored at the last step). */
27
148
  next() {
28
- this.#moveTo(this.#clampIndex(this.indexValue) + 1);
149
+ this.#moveTo(this.#clampIndex(this.#safeIndex) + 1);
29
150
  }
30
- /** Returns to the previous step (ignored at the first step). */
31
151
  prev() {
32
- this.#moveTo(this.#clampIndex(this.indexValue) - 1);
152
+ this.#moveTo(this.#clampIndex(this.#safeIndex) - 1);
33
153
  }
34
- /** Jumps to the step carried in the action's `index` param. */
35
154
  goto(event) {
36
- const target = Number(event.params.index);
37
- if (!Number.isFinite(target) || !Number.isInteger(target)) return;
155
+ const { index } = event.params;
156
+ const target = toFiniteNumber(index);
157
+ if (target === null || !Number.isInteger(target)) return;
38
158
  this.#moveTo(target);
39
159
  }
40
- /**
41
- * Moves the current step to `target` when allowed: in range, not a no-op, and
42
- * — under `linear` — not skipping more than one step ahead. Re-renders and
43
- * dispatches `change`.
44
- */
45
160
  #moveTo(target) {
46
161
  const total = this.stepTargets.length;
47
- if (!Number.isFinite(target) || !Number.isInteger(target)) return;
48
162
  if (target < 0 || target >= total) return;
49
- const current = this.#clampIndex(this.indexValue);
50
- if (target === current) return;
163
+ const current = this.#clampIndex(this.#safeIndex);
164
+ if (target === current && target === this.#shown) return;
51
165
  if (this.linearValue && target > current + 1) return;
52
- const previous = current;
166
+ const previous = this.#shown ?? current;
53
167
  this.indexValue = target;
54
- this.#render(target);
168
+ this.#shown = target;
169
+ this.#render();
170
+ if (target === previous) return;
55
171
  this.dispatch("change", {
56
- detail: { index: target, previous, step: this.stepTargets[target] }
172
+ detail: { index: target, previous, total, step: this.stepTargets[target] }
57
173
  });
58
174
  }
59
- /** Normalizes the public Value and reflects it without dispatching an action event. */
60
- #normalizeAndRender() {
61
- const normalized = this.#clampIndex(this.indexValue);
62
- if (!Object.is(normalized, this.indexValue)) this.indexValue = normalized;
63
- this.#render(normalized);
64
- }
65
- /**
66
- * Derives each step's `data-state` and the current button's `aria-current`.
67
- *
68
- * `aria-current="step"` is placed on the step's **first** `<button>`; the markup
69
- * contract assumes one operable button per step. If a step needs multiple
70
- * buttons, mark the navigational one first (or this would target the wrong one).
71
- *
72
- * @stimeoRenderRoot
73
- */
74
- #render(current) {
175
+ #reconcileStep() {
176
+ const previous = this.#shown;
177
+ const index = this.#currentStep;
178
+ if (index !== null) this.#shown = index;
179
+ this.#render();
180
+ if (index === null || previous === null || index === previous) return;
181
+ const steps = this.stepTargets;
182
+ this.dispatch("reconcile", {
183
+ detail: { index, previous, total: steps.length, step: steps[index] }
184
+ });
185
+ }
186
+ get #currentStep() {
187
+ return this.stepTargets.length > 0 ? this.#clampIndex(this.#safeIndex) : null;
188
+ }
189
+ #render() {
190
+ const current = this.#clampIndex(this.#safeIndex);
75
191
  this.stepTargets.forEach((step, index) => {
76
192
  step.dataset.state = index < current ? "complete" : index === current ? "current" : "upcoming";
77
193
  const button = step.querySelector("button");
@@ -83,14 +199,21 @@ var StepperController = class extends Controller {
83
199
  }
84
200
  });
85
201
  }
86
- /** Constrains an index to `[0, total-1]` (or `0` when there are no steps). */
87
202
  #clampIndex(index) {
88
203
  const last = this.stepTargets.length - 1;
89
- if (last < 0 || !Number.isFinite(index)) return 0;
204
+ if (last < 0) return 0;
90
205
  return Math.min(last, Math.max(0, Math.trunc(index)));
91
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
+ }
92
216
  };
93
217
 
94
218
  export { StepperController };
95
- //# sourceMappingURL=stepper_controller.js.map
96
219
  //# sourceMappingURL=stepper_controller.js.map