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/network_status_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,43 +16,217 @@ function fillTemplate(template, values) {
19
16
  });
20
17
  }
21
18
 
22
- // src/utils/safe_timeout.ts
19
+ function sharedRegistry(key, create) {
20
+ const symbol = Symbol.for(key);
21
+ const scope = globalThis;
22
+ const existing = scope[symbol];
23
+ if (existing !== void 0) return existing;
24
+ const registry = create();
25
+ Object.defineProperty(globalThis, symbol, { value: registry });
26
+ return registry;
27
+ }
28
+
29
+ function isAttributeValue(value) {
30
+ return value === null || typeof value === "string";
31
+ }
32
+ function readLeaseRecord(element, record) {
33
+ try {
34
+ const parsed = JSON.parse(element.getAttribute(record) ?? "null");
35
+ if (!Array.isArray(parsed)) return void 0;
36
+ const [original, written, ...rest] = parsed;
37
+ const linked = rest[0] !== false;
38
+ const beneath = linked ? rest : rest.slice(1);
39
+ if (!beneath.every((under) => typeof under === "string")) return void 0;
40
+ return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
41
+ } catch {
42
+ return void 0;
43
+ }
44
+ }
45
+ var AttributeLease = class _AttributeLease {
46
+ static #registry = sharedRegistry(
47
+ "stimeo-ui.attribute-lease.registry.v1",
48
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
49
+ );
50
+ #attribute;
51
+ #record;
52
+ #base;
53
+ #held = /* @__PURE__ */ new Set();
54
+ constructor(attribute, owner, options = {}) {
55
+ this.#attribute = attribute;
56
+ this.#record = `data-${owner}-${attribute}-lease`;
57
+ this.#base = options.base === true;
58
+ const names = _AttributeLease.#registry.names;
59
+ names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
60
+ }
61
+ write(element, value) {
62
+ const stack = this.#stack(element);
63
+ const hold = this.#take(stack) ?? this.#acquire(element, stack);
64
+ this.#held.add(element);
65
+ const shows = !this.#covered(element, stack, hold);
66
+ hold.written = value;
67
+ this.#mark(element, stack);
68
+ if (shows) this.#reflect(element, value);
69
+ }
70
+ return(element) {
71
+ const held = this.#held.delete(element);
72
+ const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
73
+ const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
74
+ const hold = this.#take(stack);
75
+ if (!hold) return false;
76
+ const carries = element.getAttribute(this.#attribute) === hold.written;
77
+ const owns = carries && !this.#covered(element, stack, hold);
78
+ const index = stack.indexOf(hold);
79
+ stack.splice(index, 1);
80
+ const above = stack[index];
81
+ if (above) above.linked &&= hold.linked;
82
+ this.#mark(element, stack);
83
+ if (!owns) return true;
84
+ const below = hold.linked ? stack[index - 1] : void 0;
85
+ this.#reflect(element, below ? below.written : hold.original);
86
+ return true;
87
+ }
88
+ returnAll() {
89
+ for (const element of Array.from(this.#held)) this.return(element);
90
+ }
91
+ #stack(element) {
92
+ const stacks = _AttributeLease.#registry.stacks;
93
+ const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
94
+ stacks.set(element, byAttribute);
95
+ const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
96
+ byAttribute.set(this.#attribute, stack);
97
+ return stack;
98
+ }
99
+ #seed(element) {
100
+ const found = [];
101
+ for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
102
+ const read = readLeaseRecord(element, record);
103
+ if (read) found.push({ ...read, record });
104
+ }
105
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
106
+ return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
107
+ lease: null,
108
+ record,
109
+ written,
110
+ original,
111
+ linked: linked && index > 0
112
+ }));
113
+ }
114
+ #take(stack) {
115
+ const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
116
+ if (hold) hold.lease = this;
117
+ return hold;
118
+ }
119
+ #acquire(element, stack) {
120
+ const current = element.getAttribute(this.#attribute);
121
+ const top = stack.at(-1);
122
+ const bottom = stack[0];
123
+ const over = top?.written === current ? top : void 0;
124
+ const from = this.#base ? bottom : over;
125
+ const hold = {
126
+ lease: this,
127
+ record: this.#record,
128
+ written: current,
129
+ original: from ? from.original : current,
130
+ linked: !this.#base && over !== void 0
131
+ };
132
+ if (this.#base && bottom) {
133
+ bottom.linked = true;
134
+ stack.unshift(hold);
135
+ } else stack.push(hold);
136
+ return hold;
137
+ }
138
+ #covered(element, stack, hold) {
139
+ const current = element.getAttribute(this.#attribute);
140
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
141
+ }
142
+ #mark(element, stack) {
143
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
144
+ stack.forEach((hold, index) => {
145
+ const names = stack.slice(0, index).map((under) => under.record);
146
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
147
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
148
+ records.set(
149
+ hold.record,
150
+ hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
151
+ );
152
+ });
153
+ for (const [name, recorded] of records) {
154
+ if (element.getAttribute(name) === recorded) continue;
155
+ if (recorded === null) element.removeAttribute(name);
156
+ else element.setAttribute(name, recorded);
157
+ }
158
+ }
159
+ #reflect(element, value) {
160
+ if (element.getAttribute(this.#attribute) === value) return;
161
+ if (value === null) element.removeAttribute(this.#attribute);
162
+ else element.setAttribute(this.#attribute, value);
163
+ }
164
+ };
165
+
166
+ var MAX_TIMER_DELAY_MS = 2147483647;
167
+
168
+ var NUMBER_BOUNDS = {
169
+ timer: { finite: true, min: 0, max: MAX_TIMER_DELAY_MS }};
170
+ function matchesNumberBounds(value, bounds) {
171
+ if (!Number.isFinite(value)) {
172
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
173
+ if (direction === null) return false;
174
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
175
+ }
176
+ if (bounds.min !== void 0 && value < bounds.min) return false;
177
+ if (bounds.max !== void 0 && value > bounds.max) return false;
178
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
179
+ if (bounds.integer && !Number.isInteger(value)) return false;
180
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
181
+ return true;
182
+ }
183
+
184
+ function readNumber(raw, fallback, bounds) {
185
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
186
+ }
187
+
188
+ var NumberValueReader = class {
189
+ #lastRejected = /* @__PURE__ */ new Map();
190
+ read(owner, name, raw, fallback, bounds) {
191
+ const resolved = readNumber(raw, fallback, bounds);
192
+ if (matchesNumberBounds(raw, bounds)) {
193
+ this.#lastRejected.delete(name);
194
+ return resolved;
195
+ }
196
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
197
+ const literal = owner.element.getAttribute(attribute);
198
+ if (literal === null) {
199
+ this.#lastRejected.delete(name);
200
+ return resolved;
201
+ }
202
+ if (this.#lastRejected.get(name) !== literal) {
203
+ this.#lastRejected.set(name, literal);
204
+ console.warn(
205
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
206
+ );
207
+ }
208
+ return resolved;
209
+ }
210
+ };
211
+
23
212
  var TimerRegistry = class {
24
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
25
213
  ids = /* @__PURE__ */ new Set();
26
- /**
27
- * Cancels a single tracked timer.
28
- *
29
- * No-ops if the id is unknown (already cleared, fired, or never owned by this
30
- * registry), so callers can clear defensively without guarding.
31
- */
32
214
  clear(id) {
33
215
  if (this.ids.delete(id)) {
34
216
  this.cancel(id);
35
217
  }
36
218
  }
37
- /**
38
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
39
- * to guarantee no timer outlives the element.
40
- */
41
219
  clearAll() {
42
220
  for (const id of this.ids) {
43
221
  this.cancel(id);
44
222
  }
45
223
  this.ids.clear();
46
224
  }
47
- /** Number of timers currently tracked (pending). */
48
225
  get size() {
49
226
  return this.ids.size;
50
227
  }
51
228
  };
52
229
  var SafeTimeout = class extends TimerRegistry {
53
- /**
54
- * Schedules `callback` after `delay` ms and returns the timer id.
55
- *
56
- * The id is removed from the registry automatically when the timeout fires,
57
- * so {@link TimerRegistry.size | size} reflects only still-pending timers.
58
- */
59
230
  set(callback, delay) {
60
231
  const id = this.schedule(() => {
61
232
  this.ids.delete(id);
@@ -72,43 +243,54 @@ var SafeTimeout = class extends TimerRegistry {
72
243
  }
73
244
  };
74
245
 
75
- // src/controllers/network_status_controller.ts
76
- var NetworkStatusController = class extends Controller {
246
+ var NetworkStatusController = class _NetworkStatusController extends Controller {
247
+ #numbers = new NumberValueReader();
77
248
  static targets = ["offline", "online"];
78
249
  static values = {
79
250
  announceText: { type: String, default: "" },
80
251
  announceOnlineText: { type: String, default: "" },
81
252
  onlineAutoHide: { type: Number, default: 0 }
82
253
  };
254
+ static valueConstraints = {
255
+ onlineAutoHide: NUMBER_BOUNDS.timer
256
+ };
83
257
  static events = ["change"];
84
258
  #timers = new SafeTimeout();
85
- /** Last known connectivity; guards against duplicate-state re-announcements. */
259
+ #hidden = new AttributeLease("hidden", this.identifier);
260
+ #connected = false;
86
261
  #online = true;
262
+ #recoveryShown = false;
87
263
  #handleOnline = () => this.#update(true);
88
264
  #handleOffline = () => this.#update(false);
89
265
  connect() {
90
- if (this.hasOfflineTarget) this.offlineTarget.hidden = true;
91
- if (this.hasOnlineTarget) this.onlineTarget.hidden = true;
266
+ this.#connected = true;
92
267
  this.#online = navigator.onLine;
268
+ this.#recoveryShown = false;
93
269
  this.element.setAttribute("data-state", this.#online ? "online" : "offline");
94
- if (!this.#online) this.#showOffline();
270
+ this.#paint();
95
271
  window.addEventListener("online", this.#handleOnline);
96
272
  window.addEventListener("offline", this.#handleOffline);
97
273
  }
98
274
  disconnect() {
275
+ this.#connected = false;
99
276
  window.removeEventListener("online", this.#handleOnline);
100
277
  window.removeEventListener("offline", this.#handleOffline);
101
278
  this.#timers.clearAll();
102
279
  }
103
- /**
104
- * Applies a connectivity transition, guarded against duplicate states.
105
- *
106
- * The event goes out last, so a listener reading `data-state` or a banner's
107
- * visibility sees the state the transition landed on rather than the previous one.
108
- *
109
- * @stimeoRuntimeOnly The texts word the one announcement of this transition; the state it shows
110
- * comes from the browser.
111
- */
280
+ offlineTargetConnected() {
281
+ if (this.#connected) this.#paint();
282
+ }
283
+ offlineTargetDisconnected(banner) {
284
+ if (!this.offlineTargets.includes(banner)) this.#hidden.return(banner);
285
+ if (this.#connected) this.#paint();
286
+ }
287
+ onlineTargetConnected() {
288
+ if (this.#connected) this.#paint();
289
+ }
290
+ onlineTargetDisconnected(banner) {
291
+ if (!this.onlineTargets.includes(banner)) this.#hidden.return(banner);
292
+ if (this.#connected) this.#paint();
293
+ }
112
294
  #update(online) {
113
295
  if (online === this.#online) return;
114
296
  this.#online = online;
@@ -123,30 +305,36 @@ var NetworkStatusController = class extends Controller {
123
305
  });
124
306
  this.dispatch("change", { detail: { online } });
125
307
  }
126
- /** Shows the offline banner and hides the recovery banner. */
127
308
  #showOffline() {
128
309
  this.#timers.clearAll();
129
- if (this.hasOnlineTarget) this.onlineTarget.hidden = true;
130
- if (this.hasOfflineTarget) this.offlineTarget.hidden = false;
131
- }
132
- /**
133
- * Shows the recovery banner, optionally auto-hiding it after `onlineAutoHide`.
134
- *
135
- * @stimeoRuntimeOnly `onlineAutoHide` is the delay of the one timer that hides the banner this
136
- * transition shows.
137
- */
310
+ this.#recoveryShown = false;
311
+ this.#paint();
312
+ }
138
313
  #showOnline() {
139
- if (this.hasOfflineTarget) this.offlineTarget.hidden = true;
140
- if (!this.hasOnlineTarget) return;
141
- this.onlineTarget.hidden = false;
142
- if (this.onlineAutoHideValue > 0) {
314
+ this.#recoveryShown = true;
315
+ this.#paint();
316
+ if (this.#safeOnlineAutoHide > 0) {
143
317
  this.#timers.set(() => {
144
- this.onlineTarget.hidden = true;
145
- }, this.onlineAutoHideValue);
318
+ this.#recoveryShown = false;
319
+ this.#paint();
320
+ }, this.#safeOnlineAutoHide);
146
321
  }
147
322
  }
323
+ #paint() {
324
+ if (this.hasOfflineTarget) this.#hidden.write(this.offlineTarget, this.#online ? "" : null);
325
+ if (this.hasOnlineTarget)
326
+ this.#hidden.write(this.onlineTarget, this.#recoveryShown ? null : "");
327
+ }
328
+ get #safeOnlineAutoHide() {
329
+ return this.#numbers.read(
330
+ this,
331
+ "onlineAutoHide",
332
+ this.onlineAutoHideValue,
333
+ _NetworkStatusController.values.onlineAutoHide.default,
334
+ _NetworkStatusController.valueConstraints.onlineAutoHide
335
+ );
336
+ }
148
337
  };
149
338
 
150
339
  export { NetworkStatusController };
151
- //# sourceMappingURL=network_status_controller.js.map
152
340
  //# sourceMappingURL=network_status_controller.js.map