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,8 +1,5 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/meter_controller.ts
4
-
5
- // src/utils/announce.ts
6
3
  function announce(message, options = {}) {
7
4
  const text = message.trim();
8
5
  if (text.length === 0) return;
@@ -19,34 +16,59 @@ function fillTemplate(template, values) {
19
16
  });
20
17
  }
21
18
 
22
- // src/utils/coerce.ts
19
+ var NUMBER_BOUNDS = {
20
+ finite: { finite: true }};
21
+ function matchesNumberBounds(value, bounds) {
22
+ if (!Number.isFinite(value)) {
23
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
24
+ if (direction === null) return false;
25
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
26
+ }
27
+ if (bounds.min !== void 0 && value < bounds.min) return false;
28
+ if (bounds.max !== void 0 && value > bounds.max) return false;
29
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
30
+ if (bounds.integer && !Number.isInteger(value)) return false;
31
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
32
+ return true;
33
+ }
34
+
23
35
  function toFiniteNumber(raw) {
24
- if (raw === null || raw === void 0 || raw === "") return null;
25
- const value = typeof raw === "number" ? raw : Number(raw);
36
+ if (typeof raw !== "number" && typeof raw !== "string") return null;
37
+ if (typeof raw === "string" && raw.trim().length === 0) return null;
38
+ const value = Number(raw);
26
39
  return Number.isFinite(value) ? value : null;
27
40
  }
41
+ function readNumber(raw, fallback, bounds) {
42
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
43
+ }
44
+
45
+ var ListenerSet = class {
46
+ #abort = new AbortController();
47
+ add(target, type, handler, options) {
48
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
49
+ }
50
+ dispose() {
51
+ this.#abort.abort();
52
+ this.#abort = new AbortController();
53
+ }
54
+ };
28
55
 
29
- // src/utils/microtask_coalescer.ts
30
56
  var MicrotaskCoalescer = class {
31
57
  #run;
32
58
  #queued = false;
33
59
  #active = false;
34
60
  #generation = 0;
35
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
36
61
  constructor(run) {
37
62
  this.#run = run;
38
63
  }
39
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
40
64
  activate() {
41
65
  this.#active = true;
42
66
  }
43
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
44
67
  cancel() {
45
68
  this.#active = false;
46
69
  this.#queued = false;
47
70
  this.#generation += 1;
48
71
  }
49
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
50
72
  schedule() {
51
73
  if (!this.#active || this.#queued) return;
52
74
  this.#queued = true;
@@ -59,7 +81,55 @@ var MicrotaskCoalescer = class {
59
81
  }
60
82
  };
61
83
 
62
- // src/utils/range.ts
84
+ var MorphRenderWatcher = class {
85
+ #listeners = new ListenerSet();
86
+ #pass;
87
+ #element = null;
88
+ #onMorph = () => this.#pass.schedule();
89
+ constructor(run) {
90
+ this.#pass = new MicrotaskCoalescer(run);
91
+ }
92
+ observe(element) {
93
+ if (this.#element === element) return;
94
+ this.disconnect();
95
+ this.#element = element;
96
+ this.#pass.activate();
97
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
98
+ }
99
+ schedule() {
100
+ this.#pass.schedule();
101
+ }
102
+ disconnect() {
103
+ this.#listeners.dispose();
104
+ this.#pass.cancel();
105
+ this.#element = null;
106
+ }
107
+ };
108
+
109
+ var NumberValueReader = class {
110
+ #lastRejected = /* @__PURE__ */ new Map();
111
+ read(owner, name, raw, fallback, bounds) {
112
+ const resolved = readNumber(raw, fallback, bounds);
113
+ if (matchesNumberBounds(raw, bounds)) {
114
+ this.#lastRejected.delete(name);
115
+ return resolved;
116
+ }
117
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
118
+ const literal = owner.element.getAttribute(attribute);
119
+ if (literal === null) {
120
+ this.#lastRejected.delete(name);
121
+ return resolved;
122
+ }
123
+ if (this.#lastRejected.get(name) !== literal) {
124
+ this.#lastRejected.set(name, literal);
125
+ console.warn(
126
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
127
+ );
128
+ }
129
+ return resolved;
130
+ }
131
+ };
132
+
63
133
  function rangeFraction(value, min, max) {
64
134
  const span = max - min;
65
135
  if (!(span > 0)) return 0;
@@ -75,10 +145,9 @@ function rangeFraction(value, min, max) {
75
145
  return Math.min(1, Math.max(0, fraction));
76
146
  }
77
147
 
78
- // src/controllers/meter_controller.ts
79
148
  var OWNED_VALUE_TEXT = "owns-valuetext";
80
- var MeterController = class extends Controller {
81
- /** The marker above, in the namespace this controller is registered under. */
149
+ var MeterController = class _MeterController extends Controller {
150
+ #numbers = new NumberValueReader();
82
151
  get #ownedValueText() {
83
152
  return `data-${this.identifier}-${OWNED_VALUE_TEXT}`;
84
153
  }
@@ -93,114 +162,85 @@ var MeterController = class extends Controller {
93
162
  optimum: { type: Number, default: 0 },
94
163
  valueText: { type: String, default: "" }
95
164
  };
165
+ static valueConstraints = {
166
+ value: NUMBER_BOUNDS.finite,
167
+ min: NUMBER_BOUNDS.finite,
168
+ max: NUMBER_BOUNDS.finite,
169
+ low: NUMBER_BOUNDS.finite,
170
+ high: NUMBER_BOUNDS.finite,
171
+ optimum: NUMBER_BOUNDS.finite
172
+ };
96
173
  static actions = ["setValue"];
97
174
  static events = ["change"];
98
- /**
99
- * Collapses a morph that swaps several render inputs at once into one repaint.
100
- * A single update usually rewrites the whole set, and each Value would otherwise
101
- * repaint on its own.
102
- */
103
- #repaint = new MicrotaskCoalescer(() => {
175
+ #repaint = new MorphRenderWatcher(() => {
104
176
  this.#render();
105
177
  });
106
- /** The segment last announced, so only a change is read out. */
107
- #announcedState = null;
178
+ #shownState = null;
108
179
  connect() {
109
- this.#repaint.activate();
180
+ this.#repaint.observe(this.element);
110
181
  this.#render();
111
182
  }
112
- /** Closes the window in which a queued repaint may still run. */
113
183
  disconnect() {
114
- this.#repaint.cancel();
184
+ this.#repaint.disconnect();
115
185
  }
116
- /**
117
- * Updates the measured value from an action param (`amount`) or a
118
- * `detail.value` CustomEvent, syncs ARIA and `data-state`, and dispatches
119
- * `change` with the value, ratio, and computed segment.
120
- */
121
186
  setValue(event) {
122
187
  const next = toFiniteNumber(event.params?.amount ?? event.detail?.value);
123
188
  if (next === null) return;
124
189
  this.valueValue = this.#clamp(next);
190
+ const shown = this.#shownState;
125
191
  const reading = this.#render();
126
192
  this.dispatch("change", { detail: reading });
127
- if (reading.state !== this.#announcedState) {
128
- this.#announcedState = reading.state;
193
+ if (reading.state !== shown) {
129
194
  announce(
130
195
  fillTemplate(this.announceTextValue, { state: reading.state, value: reading.value })
131
196
  );
132
197
  }
133
198
  }
134
- /** Repaints when application code (or a Turbo morph) changes `value` at runtime. */
135
199
  valueValueChanged() {
136
200
  this.#repaint.schedule();
137
201
  }
138
- /** Repaints when application code (or a Turbo morph) changes `min` at runtime. */
139
202
  minValueChanged() {
140
203
  this.#repaint.schedule();
141
204
  }
142
- /** Repaints when application code (or a Turbo morph) changes `max` at runtime. */
143
205
  maxValueChanged() {
144
206
  this.#repaint.schedule();
145
207
  }
146
- /** Repaints when application code (or a Turbo morph) changes `low` at runtime. */
147
208
  lowValueChanged() {
148
209
  this.#repaint.schedule();
149
210
  }
150
- /** Repaints when application code (or a Turbo morph) changes `high` at runtime. */
151
211
  highValueChanged() {
152
212
  this.#repaint.schedule();
153
213
  }
154
- /** Repaints when application code (or a Turbo morph) changes `valueText` at runtime. */
155
214
  valueTextValueChanged() {
156
215
  this.#repaint.schedule();
157
216
  }
158
- /** Clamps `raw` into the configured `[min, max]` range. */
159
217
  #clamp(raw) {
160
- return Math.min(this.maxValue, Math.max(this.minValue, raw));
218
+ return Math.min(this.#safeMax, Math.max(this.#safeMin, raw));
161
219
  }
162
- /** Whether a threshold attribute is present (absent = no threshold). */
163
220
  #hasThreshold(name) {
164
221
  return this.element.hasAttribute(`data-${this.identifier}-${name}-value`);
165
222
  }
166
- /**
167
- * Classifies `value` into a `low`/`medium`/`high` segment. Values at or below
168
- * `low` are `low`; at or above `high` are `high`; otherwise `medium`. With
169
- * neither threshold present, everything is `medium`.
170
- */
171
223
  #stateOf(value) {
172
- if (this.#hasThreshold("low") && value <= this.lowValue) return "low";
173
- if (this.#hasThreshold("high") && value >= this.highValue) return "high";
224
+ if (this.#hasThreshold("low") && value <= this.#safeLow) return "low";
225
+ if (this.#hasThreshold("high") && value >= this.#safeHigh) return "high";
174
226
  return "medium";
175
227
  }
176
- /**
177
- * Reflects value/range onto ARIA, the segment onto `data-state`, and the ratio.
178
- * The reading is derived once and returned, so the `change` detail reports the
179
- * same numbers the DOM just received.
180
- *
181
- * @stimeoRenderRoot
182
- */
183
228
  #render() {
184
- const value = this.#clamp(this.valueValue);
229
+ const value = this.#clamp(this.#safeValue);
185
230
  const reading = {
186
231
  value,
187
- ratio: rangeFraction(value, this.minValue, this.maxValue),
232
+ ratio: rangeFraction(value, this.#safeMin, this.#safeMax),
188
233
  state: this.#stateOf(value)
189
234
  };
190
- this.element.setAttribute("aria-valuemin", String(this.minValue));
191
- this.element.setAttribute("aria-valuemax", String(this.maxValue));
235
+ this.element.setAttribute("aria-valuemin", String(this.#safeMin));
236
+ this.element.setAttribute("aria-valuemax", String(this.#safeMax));
192
237
  this.element.setAttribute("aria-valuenow", String(reading.value));
193
238
  this.element.style.setProperty("--stimeo--meter-ratio", String(reading.ratio));
194
239
  this.element.setAttribute("data-state", reading.state);
195
240
  this.#applyValueText(reading);
241
+ this.#shownState = reading.state;
196
242
  return reading;
197
243
  }
198
- /**
199
- * Sets `aria-valuetext` from the consumer-provided template, substituting
200
- * `{value}`, `{percent}`, and `{state}`. Kept i18n-neutral in the library.
201
- * With no template the attribute belongs to the consumer, so only a text this
202
- * controller wrote is taken back ({@link OWNED_VALUE_TEXT}).
203
- */
204
244
  #applyValueText({ value, ratio, state }) {
205
245
  if (this.valueTextValue.length === 0) {
206
246
  if (this.element.hasAttribute(this.#ownedValueText)) {
@@ -214,8 +254,52 @@ var MeterController = class extends Controller {
214
254
  this.element.setAttribute("aria-valuetext", text);
215
255
  this.element.setAttribute(this.#ownedValueText, "");
216
256
  }
257
+ get #safeValue() {
258
+ return this.#numbers.read(
259
+ this,
260
+ "value",
261
+ this.valueValue,
262
+ _MeterController.values.value.default,
263
+ _MeterController.valueConstraints.value
264
+ );
265
+ }
266
+ get #safeMin() {
267
+ return this.#numbers.read(
268
+ this,
269
+ "min",
270
+ this.minValue,
271
+ _MeterController.values.min.default,
272
+ _MeterController.valueConstraints.min
273
+ );
274
+ }
275
+ get #safeMax() {
276
+ return this.#numbers.read(
277
+ this,
278
+ "max",
279
+ this.maxValue,
280
+ _MeterController.values.max.default,
281
+ _MeterController.valueConstraints.max
282
+ );
283
+ }
284
+ get #safeLow() {
285
+ return this.#numbers.read(
286
+ this,
287
+ "low",
288
+ this.lowValue,
289
+ _MeterController.values.low.default,
290
+ _MeterController.valueConstraints.low
291
+ );
292
+ }
293
+ get #safeHigh() {
294
+ return this.#numbers.read(
295
+ this,
296
+ "high",
297
+ this.highValue,
298
+ _MeterController.values.high.default,
299
+ _MeterController.valueConstraints.high
300
+ );
301
+ }
217
302
  };
218
303
 
219
304
  export { MeterController };
220
- //# sourceMappingURL=meter_controller.js.map
221
305
  //# sourceMappingURL=meter_controller.js.map