stimeo-ui 0.16.0 → 0.18.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 (117) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +136 -0
  3. data/README.md +27 -11
  4. data/dist/cable/index.js +384 -449
  5. data/dist/controllers/accordion_controller.js +174 -55
  6. data/dist/controllers/alert_dialog_controller.js +903 -235
  7. data/dist/controllers/announcer_controller.js +238 -205
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +414 -145
  10. data/dist/controllers/avatar_controller.js +185 -107
  11. data/dist/controllers/breadcrumb_controller.js +0 -130
  12. data/dist/controllers/bulk_select_controller.js +282 -58
  13. data/dist/controllers/calendar_controller.js +228 -260
  14. data/dist/controllers/carousel_controller.js +536 -436
  15. data/dist/controllers/character_counter_controller.js +493 -207
  16. data/dist/controllers/checkbox_controller.js +38 -74
  17. data/dist/controllers/clipboard_controller.js +261 -149
  18. data/dist/controllers/collapsible_controller.js +304 -120
  19. data/dist/controllers/color_picker_controller.js +178 -146
  20. data/dist/controllers/combobox_controller.js +522 -156
  21. data/dist/controllers/command_palette_controller.js +1156 -395
  22. data/dist/controllers/conditional_fields_controller.js +300 -171
  23. data/dist/controllers/confirm_controller.js +980 -262
  24. data/dist/controllers/context_menu_controller.js +346 -153
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +281 -158
  27. data/dist/controllers/currency_input_controller.js +575 -225
  28. data/dist/controllers/data_grid_controller.js +248 -214
  29. data/dist/controllers/date_range_picker_controller.js +338 -235
  30. data/dist/controllers/dialog_controller.js +899 -237
  31. data/dist/controllers/direct_upload_controller.js +97 -167
  32. data/dist/controllers/dirty_form_controller.js +72 -69
  33. data/dist/controllers/dismissible_controller.js +196 -24
  34. data/dist/controllers/drawer_controller.js +974 -355
  35. data/dist/controllers/dropdown_controller.js +339 -96
  36. data/dist/controllers/editable_controller.js +315 -88
  37. data/dist/controllers/empty_state_controller.js +79 -58
  38. data/dist/controllers/file_dropzone_controller.js +322 -211
  39. data/dist/controllers/filter_controller.js +77 -42
  40. data/dist/controllers/flash_controller.js +238 -347
  41. data/dist/controllers/focus_controller.js +679 -240
  42. data/dist/controllers/form_field_controller.js +156 -132
  43. data/dist/controllers/form_validation_controller.js +31 -97
  44. data/dist/controllers/frame_loading_controller.js +275 -217
  45. data/dist/controllers/highlight_controller.js +107 -75
  46. data/dist/controllers/hover_card_controller.js +327 -130
  47. data/dist/controllers/idle_controller.js +271 -101
  48. data/dist/controllers/input_mask_controller.js +483 -97
  49. data/dist/controllers/intersection_controller.js +150 -106
  50. data/dist/controllers/lazy_frame_controller.js +24 -68
  51. data/dist/controllers/listbox_controller.js +523 -240
  52. data/dist/controllers/local_time_controller.js +102 -77
  53. data/dist/controllers/masonry_controller.js +80 -96
  54. data/dist/controllers/menu_controller.js +302 -188
  55. data/dist/controllers/menubar_controller.js +232 -345
  56. data/dist/controllers/meter_controller.js +417 -87
  57. data/dist/controllers/multi_select_controller.js +785 -393
  58. data/dist/controllers/navigation_menu_controller.js +190 -250
  59. data/dist/controllers/nested_form_controller.js +396 -192
  60. data/dist/controllers/network_status_controller.js +268 -62
  61. data/dist/controllers/number_input_controller.js +610 -303
  62. data/dist/controllers/optimistic_controller.js +45 -87
  63. data/dist/controllers/otp_controller.js +658 -255
  64. data/dist/controllers/overflow_indicator_controller.js +122 -113
  65. data/dist/controllers/overflow_menu_controller.js +470 -295
  66. data/dist/controllers/pagination_controller.js +332 -123
  67. data/dist/controllers/password_reveal_controller.js +392 -121
  68. data/dist/controllers/password_strength_controller.js +111 -158
  69. data/dist/controllers/persist_controller.js +165 -111
  70. data/dist/controllers/pointer_drag_controller.js +434 -188
  71. data/dist/controllers/popover_controller.js +269 -113
  72. data/dist/controllers/portal_controller.js +215 -76
  73. data/dist/controllers/preview_guard_controller.js +243 -116
  74. data/dist/controllers/progress_controller.js +391 -78
  75. data/dist/controllers/radio_group_controller.js +278 -211
  76. data/dist/controllers/range_slider_controller.js +220 -113
  77. data/dist/controllers/rating_controller.js +458 -211
  78. data/dist/controllers/read_more_controller.js +138 -63
  79. data/dist/controllers/reading_progress_controller.js +166 -137
  80. data/dist/controllers/relative_time_controller.js +211 -112
  81. data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +103 -38
  82. data/dist/controllers/resizable_controller.js +299 -150
  83. data/dist/controllers/roving_controller.js +49 -88
  84. data/dist/controllers/scroll_area_controller.js +490 -246
  85. data/dist/controllers/scroll_restore_controller.js +14 -55
  86. data/dist/controllers/scroll_visibility_controller.js +432 -149
  87. data/dist/controllers/scrollspy_controller.js +193 -248
  88. data/dist/controllers/separator_controller.js +285 -142
  89. data/dist/controllers/sidebar_controller.js +1069 -404
  90. data/dist/controllers/skeleton_controller.js +259 -101
  91. data/dist/controllers/slider_controller.js +187 -86
  92. data/dist/controllers/smart_sticky_header_controller.js +173 -66
  93. data/dist/controllers/sortable_controller.js +26 -125
  94. data/dist/controllers/spinner_controller.js +288 -173
  95. data/dist/controllers/step_indicator_controller.js +105 -46
  96. data/dist/controllers/stepper_controller.js +120 -73
  97. data/dist/controllers/stick_to_bottom_controller.js +275 -140
  98. data/dist/controllers/sticky_observer_controller.js +220 -49
  99. data/dist/controllers/submit_once_controller.js +376 -227
  100. data/dist/controllers/switch_controller.js +201 -84
  101. data/dist/controllers/tabs_controller.js +41 -30
  102. data/dist/controllers/tags_input_controller.js +293 -173
  103. data/dist/controllers/textarea_autosize_controller.js +108 -74
  104. data/dist/controllers/theme_controller.js +140 -116
  105. data/dist/controllers/time_picker_controller.js +331 -127
  106. data/dist/controllers/toast_controller.js +244 -324
  107. data/dist/controllers/toggle_group_controller.js +252 -191
  108. data/dist/controllers/toolbar_controller.js +219 -120
  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 +283 -369
  112. data/dist/index.js +8573 -10410
  113. data/dist/positioning/index.js +154 -47
  114. data/lib/generators/stimeo/install/install_generator.rb +7 -2
  115. data/lib/generators/stimeo/install/templates/stimeo.js +19 -6
  116. data/lib/stimeo/ui/version.rb +1 -1
  117. 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,149 @@ 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
+ get pending() {
87
+ return this.#queued;
88
+ }
89
+ schedule() {
90
+ if (!this.#active || this.#queued) return;
91
+ this.#queued = true;
92
+ const generation = this.#generation;
93
+ queueMicrotask(() => {
94
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
95
+ this.#queued = false;
96
+ this.#run();
97
+ });
98
+ }
99
+ };
100
+
101
+ var MorphRenderWatcher = class {
102
+ #listeners = new ListenerSet();
103
+ #pass;
104
+ #element = null;
105
+ #onMorph = () => this.#pass.schedule();
106
+ constructor(run) {
107
+ this.#pass = new MicrotaskCoalescer(run);
108
+ }
109
+ observe(element) {
110
+ if (this.#element === element) return;
111
+ this.disconnect();
112
+ this.#element = element;
113
+ this.#pass.activate();
114
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
115
+ }
116
+ schedule() {
117
+ this.#pass.schedule();
118
+ }
119
+ get pending() {
120
+ return this.#pass.pending;
121
+ }
122
+ disconnect() {
123
+ this.#listeners.dispose();
124
+ this.#pass.cancel();
125
+ this.#element = null;
126
+ }
127
+ };
128
+
129
+ function sharedRegistry(key, create) {
130
+ const symbol = Symbol.for(key);
131
+ const scope = globalThis;
132
+ const existing = scope[symbol];
133
+ if (existing !== void 0) return existing;
134
+ const registry = create();
135
+ Object.defineProperty(globalThis, symbol, { value: registry });
136
+ return registry;
137
+ }
138
+
139
+ var portalState = sharedRegistry(
140
+ "stimeo-ui.portal-state.registry.v1",
141
+ () => /* @__PURE__ */ new WeakMap()
142
+ );
143
+ var contentForms = sharedRegistry(
144
+ "stimeo-ui.portal-content-forms.registry.v1",
145
+ () => /* @__PURE__ */ new WeakSet()
146
+ );
147
+ var displaced = sharedRegistry(
148
+ "stimeo-ui.portal-displaced.registry.v1",
149
+ () => /* @__PURE__ */ new WeakSet()
150
+ );
151
+ function takenByPage(state) {
152
+ return !state.node.isConnected && !state.destination.contains(state.node);
153
+ }
154
+ function relocate(parent, node, before) {
155
+ const move = parent.moveBefore;
156
+ if (typeof move === "function") {
157
+ try {
158
+ move.call(parent, node, before);
159
+ return;
160
+ } catch {
161
+ }
162
+ }
163
+ parent.insertBefore(node, before);
164
+ }
82
165
  var PortalController = class extends Controller {
83
166
  static targets = ["content"];
84
167
  static values = {
@@ -87,44 +170,32 @@ var PortalController = class extends Controller {
87
170
  restore: { type: Boolean, default: true }
88
171
  };
89
172
  static events = ["mount", "unmount"];
90
- /** Decides whether a `disconnect()` is an in-page move or a real detach. */
91
173
  #gate = new DetachGate();
92
- /** The `to` declaration after validation; the default when it cannot be parsed. */
93
174
  #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
- */
175
+ #connected = false;
176
+ #replace = new MorphRenderWatcher(() => this.#place());
103
177
  toValueChanged() {
104
178
  this.#toSelector = validSelector(this.element, this.toValue, "body");
179
+ this.#replace.schedule();
180
+ }
181
+ positionValueChanged() {
182
+ this.#replace.schedule();
183
+ }
184
+ contentTargetConnected() {
185
+ this.#replace.schedule();
105
186
  }
106
187
  connect() {
107
188
  this.#gate.cancel();
108
189
  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 } });
190
+ if (existing) existing.owner = this;
191
+ this.#connected = true;
192
+ this.#place();
193
+ this.#replace.observe(this.element);
194
+ if (!this.#connected) this.#replace.disconnect();
126
195
  }
127
196
  disconnect() {
197
+ this.#connected = false;
198
+ this.#replace.disconnect();
128
199
  const state = portalState.get(this.element);
129
200
  if (!state) return;
130
201
  if (state.node === this.element) {
@@ -137,29 +208,97 @@ var PortalController = class extends Controller {
137
208
  if (current?.owner === this) this.#restore(current);
138
209
  });
139
210
  }
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
- */
211
+ #place() {
212
+ let state = portalState.get(this.element);
213
+ let node = state?.node ?? null;
214
+ if (state) {
215
+ const arrival = this.#arrival(state);
216
+ const taken = takenByPage(state);
217
+ if (arrival || taken) {
218
+ if (arrival && !taken) displaced.add(state.node);
219
+ this.#restore(state);
220
+ if (!this.#connected) return;
221
+ state = void 0;
222
+ node = arrival !== null && this.element.contains(arrival) ? arrival : null;
223
+ }
224
+ }
225
+ node ??= this.#source();
226
+ if (!node) return;
227
+ const destination = this.#destination();
228
+ if (!destination || destination === node || node.contains(destination)) {
229
+ if (state) this.#unmount(state, true);
230
+ return;
231
+ }
232
+ const position = this.positionValue === "prepend" ? "prepend" : "append";
233
+ const placed = this.#placedIn(node, destination);
234
+ if (state?.destination === destination && state.position === position && placed) {
235
+ node.setAttribute("data-portaled", "true");
236
+ return;
237
+ }
238
+ if (state) {
239
+ if (!state.placeholder.isConnected && !this.#placedIn(node, state.destination)) {
240
+ state.placeholder = this.#markHome(node);
241
+ }
242
+ state.destination = destination;
243
+ state.position = position;
244
+ } else {
245
+ const placeholder = this.#markHome(node);
246
+ const held = new Set(this.contentTargets);
247
+ portalState.set(this.element, {
248
+ node,
249
+ placeholder,
250
+ owner: this,
251
+ destination,
252
+ position,
253
+ held
254
+ });
255
+ }
256
+ relocate(destination, node, position === "prepend" ? destination.firstChild : null);
257
+ node.setAttribute("data-portaled", "true");
258
+ this.dispatch("mount", { detail: { destination } });
259
+ }
260
+ #placedIn(node, destination) {
261
+ if (!destination.contains(node)) return false;
262
+ return node === this.element || !this.element.contains(node) || this.element.contains(destination);
263
+ }
264
+ #markHome(node) {
265
+ const placeholder = document.createComment("stimeo--portal");
266
+ node.parentNode?.insertBefore(placeholder, node);
267
+ return placeholder;
268
+ }
269
+ #arrival(state) {
270
+ const { node, held } = state;
271
+ return this.contentTargets.find(
272
+ (target) => !node.contains(target) && !held.has(target) && !displaced.has(target)
273
+ ) ?? null;
274
+ }
275
+ #source() {
276
+ if (this.hasContentTarget) {
277
+ contentForms.add(this.element);
278
+ return this.contentTargets.find((target) => !displaced.has(target)) ?? null;
279
+ }
280
+ return contentForms.has(this.element) ? null : this.element;
281
+ }
145
282
  #restore(state) {
283
+ this.#unmount(state, this.restoreValue && !takenByPage(state));
284
+ }
285
+ #unmount(state, home) {
146
286
  portalState.delete(this.element);
147
287
  const { node, placeholder } = state;
148
288
  node.removeAttribute("data-portaled");
149
- if (this.restoreValue && placeholder.parentNode) {
150
- placeholder.parentNode.insertBefore(node, placeholder);
289
+ const origin = placeholder.parentNode;
290
+ if (home && origin) {
291
+ relocate(origin, node, placeholder);
151
292
  } else {
152
293
  node.remove();
153
294
  }
154
295
  placeholder.remove();
155
296
  this.dispatch("unmount", { detail: {} });
156
297
  }
157
- /** Resolves the destination from the validated `to` selector. */
158
298
  #destination() {
159
299
  return document.querySelector(this.#toSelector);
160
300
  }
161
301
  };
162
302
 
163
303
  export { PortalController };
164
- //# sourceMappingURL=portal_controller.js.map
165
304
  //# sourceMappingURL=portal_controller.js.map