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/portal_controller.ts
4
-
5
- // src/utils/declared_value.ts
6
3
  function parseDeclared(raw, parse, fallback) {
7
4
  try {
8
5
  return parse(raw);
@@ -22,63 +19,143 @@ function validSelector(element, raw, fallback) {
22
19
  );
23
20
  }
24
21
 
25
- // src/utils/detach_gate.ts
26
22
  var DetachGate = class _DetachGate {
27
- /** Set while a probe is queued, waiting for a reconnect to cancel it. */
28
23
  #pending = false;
29
- /**
30
- * True while a probe is queued — the last disconnect was ambiguous and no
31
- * reconnect has cancelled it yet. Read it from `connect()` to tell the
32
- * reconnect half of an in-page move from a first connect: a controller whose
33
- * initialisation restarts a measurement (a min-duration floor, an elapsed
34
- * counter) must skip it for the move, where nothing actually restarted.
35
- */
36
24
  get pending() {
37
25
  return this.#pending;
38
26
  }
39
- /**
40
- * True when the disconnect is definitely a real detach — the element left
41
- * the document, or `data-controller` no longer lists the identifier. False
42
- * means ambiguous (in-page move or observed-root exit), NOT "alive".
43
- */
44
27
  static isDetached(host) {
45
- if (!host.element.isConnected) return true;
46
- const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
47
- return !tokens.includes(host.identifier);
48
- }
49
- /**
50
- * Call from `disconnect()`: runs `teardown` synchronously on a definite
51
- * detach (fast path), otherwise defers it one microtask — a reconnect
52
- * ({@link cancel} from `connect()`) keeps the state, no reconnect runs it.
53
- * One microtask is the whole probe window: Stimulus reconnects a moved
54
- * element within the same mutation batch, before the checkpoint drains.
55
- */
28
+ return !host.element.isConnected || !_DetachGate.#listed(host);
29
+ }
56
30
  disconnected(host, teardown) {
57
- if (_DetachGate.isDetached(host)) {
31
+ const carried = _DetachGate.#carried(host);
32
+ if (!carried && _DetachGate.isDetached(host)) {
58
33
  this.#pending = false;
59
34
  teardown();
60
35
  return;
61
36
  }
62
37
  this.#pending = true;
63
- queueMicrotask(() => {
38
+ const probe = () => {
64
39
  if (!this.#pending) return;
65
40
  this.#pending = false;
66
41
  teardown();
67
- });
42
+ };
43
+ if (carried) setTimeout(probe, 0);
44
+ else queueMicrotask(probe);
45
+ }
46
+ static #carried(host) {
47
+ const element = host.element;
48
+ return !element.isConnected && element.closest("[data-turbo-permanent]") !== null && _DetachGate.#listed(host);
49
+ }
50
+ static #listed(host) {
51
+ const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
52
+ return tokens.includes(host.identifier);
68
53
  }
69
- /**
70
- * Disarms a pending probe. Call from `connect()` (the reconnect that proves
71
- * an in-page move) and from the head of any teardown path not routed through
72
- * {@link disconnected} (disabled-toggle, Escape), so an orphaned probe can
73
- * never run the teardown a second time.
74
- */
75
54
  cancel() {
76
55
  this.#pending = false;
77
56
  }
78
57
  };
79
58
 
80
- // src/controllers/portal_controller.ts
81
- var portalState = /* @__PURE__ */ new WeakMap();
59
+ var ListenerSet = class {
60
+ #abort = new AbortController();
61
+ add(target, type, handler, options) {
62
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
63
+ }
64
+ dispose() {
65
+ this.#abort.abort();
66
+ this.#abort = new AbortController();
67
+ }
68
+ };
69
+
70
+ var MicrotaskCoalescer = class {
71
+ #run;
72
+ #queued = false;
73
+ #active = false;
74
+ #generation = 0;
75
+ constructor(run) {
76
+ this.#run = run;
77
+ }
78
+ activate() {
79
+ this.#active = true;
80
+ }
81
+ cancel() {
82
+ this.#active = false;
83
+ this.#queued = false;
84
+ this.#generation += 1;
85
+ }
86
+ schedule() {
87
+ if (!this.#active || this.#queued) return;
88
+ this.#queued = true;
89
+ const generation = this.#generation;
90
+ queueMicrotask(() => {
91
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
92
+ this.#queued = false;
93
+ this.#run();
94
+ });
95
+ }
96
+ };
97
+
98
+ var MorphRenderWatcher = class {
99
+ #listeners = new ListenerSet();
100
+ #pass;
101
+ #element = null;
102
+ #onMorph = () => this.#pass.schedule();
103
+ constructor(run) {
104
+ this.#pass = new MicrotaskCoalescer(run);
105
+ }
106
+ observe(element) {
107
+ if (this.#element === element) return;
108
+ this.disconnect();
109
+ this.#element = element;
110
+ this.#pass.activate();
111
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
112
+ }
113
+ schedule() {
114
+ this.#pass.schedule();
115
+ }
116
+ disconnect() {
117
+ this.#listeners.dispose();
118
+ this.#pass.cancel();
119
+ this.#element = null;
120
+ }
121
+ };
122
+
123
+ function sharedRegistry(key, create) {
124
+ const symbol = Symbol.for(key);
125
+ const scope = globalThis;
126
+ const existing = scope[symbol];
127
+ if (existing !== void 0) return existing;
128
+ const registry = create();
129
+ Object.defineProperty(globalThis, symbol, { value: registry });
130
+ return registry;
131
+ }
132
+
133
+ var portalState = sharedRegistry(
134
+ "stimeo-ui.portal-state.registry.v1",
135
+ () => /* @__PURE__ */ new WeakMap()
136
+ );
137
+ var contentForms = sharedRegistry(
138
+ "stimeo-ui.portal-content-forms.registry.v1",
139
+ () => /* @__PURE__ */ new WeakSet()
140
+ );
141
+ var displaced = sharedRegistry(
142
+ "stimeo-ui.portal-displaced.registry.v1",
143
+ () => /* @__PURE__ */ new WeakSet()
144
+ );
145
+ function takenByPage(state) {
146
+ return !state.node.isConnected && !state.destination.contains(state.node);
147
+ }
148
+ function relocate(parent, node, before) {
149
+ const move = parent.moveBefore;
150
+ if (typeof move === "function") {
151
+ try {
152
+ move.call(parent, node, before);
153
+ return;
154
+ } catch {
155
+ }
156
+ }
157
+ parent.insertBefore(node, before);
158
+ }
82
159
  var PortalController = class extends Controller {
83
160
  static targets = ["content"];
84
161
  static values = {
@@ -87,44 +164,32 @@ var PortalController = class extends Controller {
87
164
  restore: { type: Boolean, default: true }
88
165
  };
89
166
  static events = ["mount", "unmount"];
90
- /** Decides whether a `disconnect()` is an in-page move or a real detach. */
91
167
  #gate = new DetachGate();
92
- /** The `to` declaration after validation; the default when it cannot be parsed. */
93
168
  #toSelector = "body";
94
- /**
95
- * Validates the destination declaration once, keeping only a selector the engine can
96
- * read.
97
- *
98
- * A selector reads back as an ordinary string, so a malformed one survives until it is
99
- * handed to the DOM — where it would take the whole teleport down and leave the part
100
- * silently doing nothing. Falling back to the default puts the node somewhere visible
101
- * instead, which reads as a mistake and can be traced back to the declaration.
102
- */
169
+ #connected = false;
170
+ #replace = new MorphRenderWatcher(() => this.#place());
103
171
  toValueChanged() {
104
172
  this.#toSelector = validSelector(this.element, this.toValue, "body");
173
+ this.#replace.schedule();
174
+ }
175
+ positionValueChanged() {
176
+ this.#replace.schedule();
177
+ }
178
+ contentTargetConnected() {
179
+ this.#replace.schedule();
105
180
  }
106
181
  connect() {
107
182
  this.#gate.cancel();
108
183
  const existing = portalState.get(this.element);
109
- if (existing) {
110
- existing.owner = this;
111
- return;
112
- }
113
- const node = this.hasContentTarget ? this.contentTarget : this.element;
114
- const destination = this.#destination();
115
- if (!destination || destination === node || node.contains(destination)) return;
116
- const placeholder = document.createComment("stimeo--portal");
117
- node.parentNode?.insertBefore(placeholder, node);
118
- portalState.set(this.element, { node, placeholder, owner: this });
119
- if (this.positionValue === "prepend") {
120
- destination.prepend(node);
121
- } else {
122
- destination.appendChild(node);
123
- }
124
- node.setAttribute("data-portaled", "true");
125
- this.dispatch("mount", { detail: { destination } });
184
+ if (existing) existing.owner = this;
185
+ this.#connected = true;
186
+ this.#place();
187
+ this.#replace.observe(this.element);
188
+ if (!this.#connected) this.#replace.disconnect();
126
189
  }
127
190
  disconnect() {
191
+ this.#connected = false;
192
+ this.#replace.disconnect();
128
193
  const state = portalState.get(this.element);
129
194
  if (!state) return;
130
195
  if (state.node === this.element) {
@@ -137,29 +202,97 @@ var PortalController = class extends Controller {
137
202
  if (current?.owner === this) this.#restore(current);
138
203
  });
139
204
  }
140
- /**
141
- * Returns the node to its placeholder (or removes it) and clears the bookkeeping.
142
- *
143
- * @stimeoRuntimeOnly `restore` decides whether this one unmount puts the node back or drops it.
144
- */
205
+ #place() {
206
+ let state = portalState.get(this.element);
207
+ let node = state?.node ?? null;
208
+ if (state) {
209
+ const arrival = this.#arrival(state);
210
+ const taken = takenByPage(state);
211
+ if (arrival || taken) {
212
+ if (arrival && !taken) displaced.add(state.node);
213
+ this.#restore(state);
214
+ if (!this.#connected) return;
215
+ state = void 0;
216
+ node = arrival !== null && this.element.contains(arrival) ? arrival : null;
217
+ }
218
+ }
219
+ node ??= this.#source();
220
+ if (!node) return;
221
+ const destination = this.#destination();
222
+ if (!destination || destination === node || node.contains(destination)) {
223
+ if (state) this.#unmount(state, true);
224
+ return;
225
+ }
226
+ const position = this.positionValue === "prepend" ? "prepend" : "append";
227
+ const placed = this.#placedIn(node, destination);
228
+ if (state?.destination === destination && state.position === position && placed) {
229
+ node.setAttribute("data-portaled", "true");
230
+ return;
231
+ }
232
+ if (state) {
233
+ if (!state.placeholder.isConnected && !this.#placedIn(node, state.destination)) {
234
+ state.placeholder = this.#markHome(node);
235
+ }
236
+ state.destination = destination;
237
+ state.position = position;
238
+ } else {
239
+ const placeholder = this.#markHome(node);
240
+ const held = new Set(this.contentTargets);
241
+ portalState.set(this.element, {
242
+ node,
243
+ placeholder,
244
+ owner: this,
245
+ destination,
246
+ position,
247
+ held
248
+ });
249
+ }
250
+ relocate(destination, node, position === "prepend" ? destination.firstChild : null);
251
+ node.setAttribute("data-portaled", "true");
252
+ this.dispatch("mount", { detail: { destination } });
253
+ }
254
+ #placedIn(node, destination) {
255
+ if (!destination.contains(node)) return false;
256
+ return node === this.element || !this.element.contains(node) || this.element.contains(destination);
257
+ }
258
+ #markHome(node) {
259
+ const placeholder = document.createComment("stimeo--portal");
260
+ node.parentNode?.insertBefore(placeholder, node);
261
+ return placeholder;
262
+ }
263
+ #arrival(state) {
264
+ const { node, held } = state;
265
+ return this.contentTargets.find(
266
+ (target) => !node.contains(target) && !held.has(target) && !displaced.has(target)
267
+ ) ?? null;
268
+ }
269
+ #source() {
270
+ if (this.hasContentTarget) {
271
+ contentForms.add(this.element);
272
+ return this.contentTargets.find((target) => !displaced.has(target)) ?? null;
273
+ }
274
+ return contentForms.has(this.element) ? null : this.element;
275
+ }
145
276
  #restore(state) {
277
+ this.#unmount(state, this.restoreValue && !takenByPage(state));
278
+ }
279
+ #unmount(state, home) {
146
280
  portalState.delete(this.element);
147
281
  const { node, placeholder } = state;
148
282
  node.removeAttribute("data-portaled");
149
- if (this.restoreValue && placeholder.parentNode) {
150
- placeholder.parentNode.insertBefore(node, placeholder);
283
+ const origin = placeholder.parentNode;
284
+ if (home && origin) {
285
+ relocate(origin, node, placeholder);
151
286
  } else {
152
287
  node.remove();
153
288
  }
154
289
  placeholder.remove();
155
290
  this.dispatch("unmount", { detail: {} });
156
291
  }
157
- /** Resolves the destination from the validated `to` selector. */
158
292
  #destination() {
159
293
  return document.querySelector(this.#toSelector);
160
294
  }
161
295
  };
162
296
 
163
297
  export { PortalController };
164
- //# sourceMappingURL=portal_controller.js.map
165
298
  //# sourceMappingURL=portal_controller.js.map