stimeo-ui 0.17.0 → 0.18.1

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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +136 -2
  3. data/README.md +19 -10
  4. data/dist/cable/index.js +22 -14
  5. data/dist/controllers/accordion_controller.js +14 -6
  6. data/dist/controllers/alert_dialog_controller.js +145 -118
  7. data/dist/controllers/announcer_controller.js +113 -17
  8. data/dist/controllers/auto_submit_controller.js +185 -60
  9. data/dist/controllers/avatar_controller.js +55 -43
  10. data/dist/controllers/bulk_select_controller.js +64 -50
  11. data/dist/controllers/calendar_controller.js +55 -53
  12. data/dist/controllers/carousel_controller.js +124 -86
  13. data/dist/controllers/character_counter_controller.js +291 -95
  14. data/dist/controllers/checkbox_controller.js +6 -0
  15. data/dist/controllers/clipboard_controller.js +118 -27
  16. data/dist/controllers/collapsible_controller.js +52 -49
  17. data/dist/controllers/color_picker_controller.js +29 -7
  18. data/dist/controllers/combobox_controller.js +210 -71
  19. data/dist/controllers/command_palette_controller.js +290 -140
  20. data/dist/controllers/conditional_fields_controller.js +145 -58
  21. data/dist/controllers/confirm_controller.js +145 -118
  22. data/dist/controllers/context_menu_controller.js +138 -73
  23. data/dist/controllers/countdown_controller.js +109 -21
  24. data/dist/controllers/currency_input_controller.js +446 -72
  25. data/dist/controllers/data_grid_controller.js +134 -28
  26. data/dist/controllers/date_range_picker_controller.js +93 -72
  27. data/dist/controllers/dialog_controller.js +145 -118
  28. data/dist/controllers/direct_upload_controller.js +24 -14
  29. data/dist/controllers/dirty_form_controller.js +68 -31
  30. data/dist/controllers/dismissible_controller.js +66 -46
  31. data/dist/controllers/drawer_controller.js +145 -118
  32. data/dist/controllers/dropdown_controller.js +76 -63
  33. data/dist/controllers/editable_controller.js +200 -62
  34. data/dist/controllers/empty_state_controller.js +13 -6
  35. data/dist/controllers/file_dropzone_controller.js +55 -48
  36. data/dist/controllers/filter_controller.js +9 -2
  37. data/dist/controllers/flash_controller.js +66 -33
  38. data/dist/controllers/focus_controller.js +39 -14
  39. data/dist/controllers/form_field_controller.js +52 -43
  40. data/dist/controllers/form_validation_controller.js +18 -11
  41. data/dist/controllers/frame_loading_controller.js +55 -48
  42. data/dist/controllers/hover_card_controller.js +67 -61
  43. data/dist/controllers/idle_controller.js +10 -6
  44. data/dist/controllers/input_mask_controller.js +434 -24
  45. data/dist/controllers/intersection_controller.js +3 -0
  46. data/dist/controllers/lazy_frame_controller.js +15 -3
  47. data/dist/controllers/listbox_controller.js +260 -79
  48. data/dist/controllers/local_time_controller.js +48 -13
  49. data/dist/controllers/masonry_controller.js +11 -4
  50. data/dist/controllers/menu_controller.js +138 -82
  51. data/dist/controllers/menubar_controller.js +65 -19
  52. data/dist/controllers/meter_controller.js +270 -24
  53. data/dist/controllers/multi_select_controller.js +272 -95
  54. data/dist/controllers/navigation_menu_controller.js +67 -19
  55. data/dist/controllers/nested_form_controller.js +98 -64
  56. data/dist/controllers/network_status_controller.js +70 -52
  57. data/dist/controllers/number_input_controller.js +344 -94
  58. data/dist/controllers/optimistic_controller.js +29 -11
  59. data/dist/controllers/otp_controller.js +502 -104
  60. data/dist/controllers/overflow_indicator_controller.js +6 -0
  61. data/dist/controllers/overflow_menu_controller.js +136 -21
  62. data/dist/controllers/pagination_controller.js +47 -14
  63. data/dist/controllers/password_reveal_controller.js +52 -49
  64. data/dist/controllers/password_strength_controller.js +13 -6
  65. data/dist/controllers/persist_controller.js +48 -128
  66. data/dist/controllers/pointer_drag_controller.js +255 -24
  67. data/dist/controllers/popover_controller.js +91 -65
  68. data/dist/controllers/portal_controller.js +6 -0
  69. data/dist/controllers/preview_guard_controller.js +14 -3
  70. data/dist/controllers/progress_controller.js +271 -27
  71. data/dist/controllers/radio_group_controller.js +167 -80
  72. data/dist/controllers/range_slider_controller.js +27 -7
  73. data/dist/controllers/rating_controller.js +93 -57
  74. data/dist/controllers/read_more_controller.js +10 -6
  75. data/dist/controllers/reading_progress_controller.js +22 -5
  76. data/dist/controllers/relative_time_controller.js +75 -11
  77. data/dist/controllers/reset_on_restore_controller.js +6 -0
  78. data/dist/controllers/resizable_controller.js +55 -22
  79. data/dist/controllers/roving_controller.js +10 -4
  80. data/dist/controllers/scroll_area_controller.js +129 -88
  81. data/dist/controllers/scroll_restore_controller.js +14 -2
  82. data/dist/controllers/scroll_visibility_controller.js +93 -60
  83. data/dist/controllers/scrollspy_controller.js +11 -5
  84. data/dist/controllers/separator_controller.js +59 -43
  85. data/dist/controllers/sidebar_controller.js +150 -122
  86. data/dist/controllers/skeleton_controller.js +55 -48
  87. data/dist/controllers/slider_controller.js +27 -6
  88. data/dist/controllers/smart_sticky_header_controller.js +13 -5
  89. data/dist/controllers/sortable_controller.js +27 -14
  90. data/dist/controllers/spinner_controller.js +55 -48
  91. data/dist/controllers/step_indicator_controller.js +6 -0
  92. data/dist/controllers/stepper_controller.js +6 -0
  93. data/dist/controllers/stick_to_bottom_controller.js +19 -8
  94. data/dist/controllers/sticky_observer_controller.js +49 -43
  95. data/dist/controllers/submit_once_controller.js +145 -85
  96. data/dist/controllers/switch_controller.js +172 -28
  97. data/dist/controllers/tabs_controller.js +6 -1
  98. data/dist/controllers/tags_input_controller.js +157 -28
  99. data/dist/controllers/textarea_autosize_controller.js +40 -19
  100. data/dist/controllers/theme_controller.js +10 -4
  101. data/dist/controllers/time_picker_controller.js +192 -54
  102. data/dist/controllers/toast_controller.js +58 -27
  103. data/dist/controllers/toggle_group_controller.js +158 -56
  104. data/dist/controllers/toolbar_controller.js +150 -21
  105. data/dist/controllers/tree_view_controller.js +134 -25
  106. data/dist/index.js +2515 -1315
  107. data/dist/positioning/index.js +6 -0
  108. data/lib/generators/stimeo/install/install_generator.rb +7 -2
  109. data/lib/generators/stimeo/install/templates/stimeo.js +19 -6
  110. data/lib/stimeo/ui/version.rb +1 -1
  111. metadata +2 -2
@@ -1,5 +1,35 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
+ function sharedRegistry(key, create) {
4
+ const symbol = Symbol.for(key);
5
+ const scope = globalThis;
6
+ const existing = scope[symbol];
7
+ if (existing !== void 0) return existing;
8
+ const registry = create();
9
+ Object.defineProperty(globalThis, symbol, { value: registry });
10
+ return registry;
11
+ }
12
+
13
+ var PREFIX = "__stimeo_raw_v1_";
14
+ sharedRegistry(
15
+ "stimeo-ui.form-data-projection.registry.v1",
16
+ () => /* @__PURE__ */ new WeakMap()
17
+ );
18
+ function decodedName(key) {
19
+ if (!key.startsWith(PREFIX)) return null;
20
+ const separator = PREFIX.length + 32;
21
+ if (key[separator] !== ":") return null;
22
+ try {
23
+ const name = JSON.parse(key.slice(separator + 1));
24
+ return typeof name === "string" ? name : null;
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+ function projectedFieldName(name) {
30
+ return decodedName(name) ?? name;
31
+ }
32
+
3
33
  var ListenerSet = class {
4
34
  #abort = new AbortController();
5
35
  add(target, type, handler, options) {
@@ -45,15 +75,29 @@ var SafeTimeout = class extends TimerRegistry {
45
75
  }
46
76
  };
47
77
 
48
- function sharedRegistry(key, create) {
49
- const symbol = Symbol.for(key);
50
- const scope = globalThis;
51
- const existing = scope[symbol];
52
- if (existing !== void 0) return existing;
53
- const registry = create();
54
- Object.defineProperty(globalThis, symbol, { value: registry });
55
- return registry;
56
- }
78
+ var SubmissionOwner = class {
79
+ #synthetic = /* @__PURE__ */ Symbol("synthetic submission");
80
+ #active = null;
81
+ begin(event) {
82
+ this.#active = { source: event.target, token: this.#tokenOf(event) };
83
+ }
84
+ resolve(event) {
85
+ if (this.#active === null || this.#active.source !== event.target || this.#active.token !== this.#tokenOf(event)) {
86
+ return null;
87
+ }
88
+ this.clear();
89
+ return this.#detailOf(event)?.success === true;
90
+ }
91
+ clear() {
92
+ this.#active = null;
93
+ }
94
+ #tokenOf(event) {
95
+ return this.#detailOf(event)?.formSubmission ?? this.#synthetic;
96
+ }
97
+ #detailOf(event) {
98
+ return event.detail;
99
+ }
100
+ };
57
101
 
58
102
  var TransientHooks = class {
59
103
  #attributes;
@@ -135,13 +179,13 @@ var DirtyFormController = class extends Controller {
135
179
  static events = ["dirty", "guard"];
136
180
  #baseline = "";
137
181
  #dirty = false;
138
- #beforeunloadBound = false;
139
182
  #timeouts = new SafeTimeout();
140
183
  #listeners = new ListenerSet();
184
+ #unloadListeners = new ListenerSet();
185
+ #submission = new SubmissionOwner();
141
186
  #submitting = false;
142
187
  #submitAttempt = null;
143
188
  #submittedBaseline = null;
144
- #activeSubmission = null;
145
189
  #unregisterVisit = null;
146
190
  #onFieldChange = () => {
147
191
  this.#submitting = false;
@@ -161,26 +205,19 @@ var DirtyFormController = class extends Controller {
161
205
  this.#submitAttempt.snapshot = this.#serialize();
162
206
  };
163
207
  #onSubmitStart = (event) => {
164
- const detail = event.detail;
165
- this.#activeSubmission = detail?.formSubmission ?? event;
208
+ if (!this.#ownsSource(event.target)) return;
209
+ this.#submission.begin(event);
166
210
  this.#submittedBaseline = this.#submitAttempt?.snapshot ?? this.#serialize();
167
211
  this.#submitAttempt = null;
168
212
  this.#submitting = this.#serialize() === this.#submittedBaseline;
169
213
  };
170
214
  #onSubmitEnd = (event) => {
171
- const detail = event.detail;
172
- if (detail?.formSubmission !== void 0 && (this.#activeSubmission === null || detail.formSubmission !== this.#activeSubmission)) {
173
- return;
174
- }
215
+ const success = this.#submission.resolve(event);
216
+ if (success === null) return;
175
217
  const submittedBaseline = this.#submittedBaseline;
176
218
  this.#submitting = false;
177
219
  this.#submittedBaseline = null;
178
- this.#activeSubmission = null;
179
- if (detail?.success === false) return;
180
- if (submittedBaseline === null) {
181
- this.markClean();
182
- return;
183
- }
220
+ if (!success || submittedBaseline === null) return;
184
221
  this.#baseline = submittedBaseline;
185
222
  this.#evaluate();
186
223
  };
@@ -199,6 +236,7 @@ var DirtyFormController = class extends Controller {
199
236
  this.#listeners.add(this.element, "formdata", this.#onFormData);
200
237
  this.#listeners.add(this.element, "turbo:submit-start", this.#onSubmitStart);
201
238
  this.#listeners.add(this.element, "turbo:submit-end", this.#onSubmitEnd);
239
+ this.#unregisterVisit?.();
202
240
  this.#unregisterVisit = registerVisitParticipant(this.element.ownerDocument, {
203
241
  form: this.element,
204
242
  evaluate: (event) => this.#evaluateVisit(event)
@@ -243,14 +281,10 @@ var DirtyFormController = class extends Controller {
243
281
  return { kind: "confirm", message: this.messageValue };
244
282
  }
245
283
  #bindBeforeUnload() {
246
- if (this.#beforeunloadBound) return;
247
- window.addEventListener("beforeunload", this.#onBeforeUnload);
248
- this.#beforeunloadBound = true;
284
+ this.#unloadListeners.add(window, "beforeunload", this.#onBeforeUnload);
249
285
  }
250
286
  #unbindBeforeUnload() {
251
- if (!this.#beforeunloadBound) return;
252
- window.removeEventListener("beforeunload", this.#onBeforeUnload);
253
- this.#beforeunloadBound = false;
287
+ this.#unloadListeners.dispose();
254
288
  }
255
289
  #guardSuppressed() {
256
290
  return this.#submitting && (this.#submitAttempt === null || !this.#submitAttempt.event.defaultPrevented);
@@ -260,7 +294,10 @@ var DirtyFormController = class extends Controller {
260
294
  this.#submitting = false;
261
295
  this.#submitAttempt = null;
262
296
  this.#submittedBaseline = null;
263
- this.#activeSubmission = null;
297
+ this.#submission.clear();
298
+ }
299
+ #ownsSource(target) {
300
+ return target === this.element || target instanceof Element && target.closest("form") === this.element;
264
301
  }
265
302
  #serialize() {
266
303
  const parts = [];
@@ -280,7 +317,7 @@ var DirtyFormController = class extends Controller {
280
317
  }
281
318
  #nameOf(el) {
282
319
  if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
283
- return el.name || el.id || "";
320
+ return projectedFieldName(el.name) || el.id || "";
284
321
  }
285
322
  return null;
286
323
  }
@@ -1,15 +1,26 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
4
+ if (element.getAttribute(name) === value) return false;
5
+ if (value === null) element.removeAttribute(name);
6
+ else element.setAttribute(name, value);
7
+ return true;
8
+ }
9
+
3
10
  function sharedRegistry(key, create) {
4
11
  const symbol = Symbol.for(key);
5
12
  const scope = globalThis;
6
13
  const existing = scope[symbol];
7
14
  if (existing !== void 0) return existing;
8
- const registry = create();
9
- Object.defineProperty(globalThis, symbol, { value: registry });
10
- return registry;
15
+ const registry2 = create();
16
+ Object.defineProperty(globalThis, symbol, { value: registry2 });
17
+ return registry2;
11
18
  }
12
19
 
20
+ var registry = sharedRegistry(
21
+ "stimeo-ui.attribute-lease.registry.v1",
22
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
23
+ );
13
24
  function isAttributeValue(value) {
14
25
  return value === null || typeof value === "string";
15
26
  }
@@ -26,24 +37,46 @@ function readLeaseRecord(element, record) {
26
37
  return void 0;
27
38
  }
28
39
  }
29
- var AttributeLease = class _AttributeLease {
30
- static #registry = sharedRegistry(
31
- "stimeo-ui.attribute-lease.registry.v1",
32
- () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
33
- );
40
+ function attributeStack(element, attribute) {
41
+ const byAttribute = registry.stacks.get(element) ?? /* @__PURE__ */ new Map();
42
+ registry.stacks.set(element, byAttribute);
43
+ const known = byAttribute.get(attribute);
44
+ if (known) return known;
45
+ const found = [];
46
+ for (const record of registry.names.get(attribute) ?? []) {
47
+ const read = readLeaseRecord(element, record);
48
+ if (read) found.push({ ...read, record });
49
+ }
50
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
51
+ const stack = found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
52
+ lease: null,
53
+ record,
54
+ written,
55
+ original,
56
+ linked: linked && index > 0
57
+ }));
58
+ byAttribute.set(attribute, stack);
59
+ return stack;
60
+ }
61
+ function findHold(stack, lease, record) {
62
+ return stack.find((held) => held.lease === lease) ?? stack.find((held) => held.lease === null && held.record === record);
63
+ }
64
+ var AttributeLease = class {
34
65
  #attribute;
35
66
  #record;
36
67
  #base;
68
+ #preserveAuthor;
37
69
  #held = /* @__PURE__ */ new Set();
38
70
  constructor(attribute, owner, options = {}) {
39
71
  this.#attribute = attribute;
40
72
  this.#record = `data-${owner}-${attribute}-lease`;
41
73
  this.#base = options.base === true;
42
- const names = _AttributeLease.#registry.names;
74
+ this.#preserveAuthor = options.preserveAuthor === true;
75
+ const names = registry.names;
43
76
  names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
44
77
  }
45
78
  write(element, value) {
46
- const stack = this.#stack(element);
79
+ const stack = attributeStack(element, this.#attribute);
47
80
  const hold = this.#take(stack) ?? this.#acquire(element, stack);
48
81
  this.#held.add(element);
49
82
  const shows = !this.#covered(element, stack, hold);
@@ -53,8 +86,8 @@ var AttributeLease = class _AttributeLease {
53
86
  }
54
87
  return(element) {
55
88
  const held = this.#held.delete(element);
56
- const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
57
- const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
89
+ const known = registry.stacks.get(element)?.has(this.#attribute);
90
+ const stack = held || known || element.hasAttribute(this.#record) ? attributeStack(element, this.#attribute) : [];
58
91
  const hold = this.#take(stack);
59
92
  if (!hold) return false;
60
93
  const carries = element.getAttribute(this.#attribute) === hold.written;
@@ -72,31 +105,8 @@ var AttributeLease = class _AttributeLease {
72
105
  returnAll() {
73
106
  for (const element of Array.from(this.#held)) this.return(element);
74
107
  }
75
- #stack(element) {
76
- const stacks = _AttributeLease.#registry.stacks;
77
- const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
78
- stacks.set(element, byAttribute);
79
- const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
80
- byAttribute.set(this.#attribute, stack);
81
- return stack;
82
- }
83
- #seed(element) {
84
- const found = [];
85
- for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
86
- const read = readLeaseRecord(element, record);
87
- if (read) found.push({ ...read, record });
88
- }
89
- const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
90
- return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
91
- lease: null,
92
- record,
93
- written,
94
- original,
95
- linked: linked && index > 0
96
- }));
97
- }
98
108
  #take(stack) {
99
- const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
109
+ const hold = findHold(stack, this, this.#record);
100
110
  if (hold) hold.lease = this;
101
111
  return hold;
102
112
  }
@@ -105,7 +115,7 @@ var AttributeLease = class _AttributeLease {
105
115
  const top = stack.at(-1);
106
116
  const bottom = stack[0];
107
117
  const over = top?.written === current ? top : void 0;
108
- const from = this.#base ? bottom : over;
118
+ const from = this.#base ? this.#preserveAuthor && !stack.some((held) => held.written === current) ? void 0 : bottom : over;
109
119
  const hold = {
110
120
  lease: this,
111
121
  record: this.#record,
@@ -135,15 +145,11 @@ var AttributeLease = class _AttributeLease {
135
145
  );
136
146
  });
137
147
  for (const [name, recorded] of records) {
138
- if (element.getAttribute(name) === recorded) continue;
139
- if (recorded === null) element.removeAttribute(name);
140
- else element.setAttribute(name, recorded);
148
+ setAttributeIfChanged(element, name, recorded);
141
149
  }
142
150
  }
143
151
  #reflect(element, value) {
144
- if (element.getAttribute(this.#attribute) === value) return;
145
- if (value === null) element.removeAttribute(this.#attribute);
146
- else element.setAttribute(this.#attribute, value);
152
+ setAttributeIfChanged(element, this.#attribute, value);
147
153
  }
148
154
  };
149
155
 
@@ -231,6 +237,17 @@ function tabStopsWithin(root) {
231
237
  );
232
238
  }
233
239
 
240
+ var ListenerSet = class {
241
+ #abort = new AbortController();
242
+ add(target, type, handler, options) {
243
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
244
+ }
245
+ dispose() {
246
+ this.#abort.abort();
247
+ this.#abort = new AbortController();
248
+ }
249
+ };
250
+
234
251
  var DismissibleController = class extends Controller {
235
252
  static targets = ["root", "fallback"];
236
253
  static values = {
@@ -241,6 +258,7 @@ var DismissibleController = class extends Controller {
241
258
  static events = ["dismiss"];
242
259
  #openDefault = new AttributeLease("data-state", this.identifier);
243
260
  #connected = false;
261
+ #escapeListeners = new ListenerSet();
244
262
  connect() {
245
263
  this.#establishOpen(this.#root);
246
264
  this.#syncEscapeListener();
@@ -248,7 +266,7 @@ var DismissibleController = class extends Controller {
248
266
  }
249
267
  disconnect() {
250
268
  this.#connected = false;
251
- this.element.removeEventListener("keydown", this.#onKeydown);
269
+ this.#escapeListeners.dispose();
252
270
  }
253
271
  rootTargetConnected(root) {
254
272
  if (!this.#connected) return;
@@ -274,9 +292,9 @@ var DismissibleController = class extends Controller {
274
292
  this.#performDismiss();
275
293
  };
276
294
  #syncEscapeListener() {
277
- this.element.removeEventListener("keydown", this.#onKeydown);
295
+ this.#escapeListeners.dispose();
278
296
  if (this.closeOnEscapeValue) {
279
- this.element.addEventListener("keydown", this.#onKeydown);
297
+ this.#escapeListeners.add(this.element, "keydown", this.#onKeydown);
280
298
  }
281
299
  }
282
300
  #establishOpen(root) {
@@ -305,6 +323,8 @@ var DismissibleController = class extends Controller {
305
323
  if (document.activeElement === candidate) return;
306
324
  }
307
325
  document.body.focus();
326
+ const remaining = document.activeElement;
327
+ if (remaining instanceof HTMLElement && root.contains(remaining)) remaining.blur();
308
328
  }
309
329
  *#focusFallbacks(root) {
310
330
  const explicitFallback = this.hasFallbackTarget ? this.fallbackTarget : null;