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,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: { target: 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,25 +202,97 @@ var PortalController = class extends Controller {
137
202
  if (current?.owner === this) this.#restore(current);
138
203
  });
139
204
  }
140
- /** Returns the node to its placeholder (or removes it) and clears the bookkeeping. */
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
+ }
141
276
  #restore(state) {
277
+ this.#unmount(state, this.restoreValue && !takenByPage(state));
278
+ }
279
+ #unmount(state, home) {
142
280
  portalState.delete(this.element);
143
281
  const { node, placeholder } = state;
144
282
  node.removeAttribute("data-portaled");
145
- if (this.restoreValue && placeholder.parentNode) {
146
- placeholder.parentNode.insertBefore(node, placeholder);
283
+ const origin = placeholder.parentNode;
284
+ if (home && origin) {
285
+ relocate(origin, node, placeholder);
147
286
  } else {
148
287
  node.remove();
149
288
  }
150
289
  placeholder.remove();
151
290
  this.dispatch("unmount", { detail: {} });
152
291
  }
153
- /** Resolves the destination from the validated `to` selector. */
154
292
  #destination() {
155
293
  return document.querySelector(this.#toSelector);
156
294
  }
157
295
  };
158
296
 
159
297
  export { PortalController };
160
- //# sourceMappingURL=portal_controller.js.map
161
298
  //# sourceMappingURL=portal_controller.js.map