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,7 +1,120 @@
1
1
  import { computePosition, autoUpdate, offset, flip, shift } from '@floating-ui/dom';
2
2
  import { Controller } from '@hotwired/stimulus';
3
3
 
4
- // src/positioning/index.ts
4
+ var ListenerSet = class {
5
+ #abort = new AbortController();
6
+ add(target, type, handler, options) {
7
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
8
+ }
9
+ dispose() {
10
+ this.#abort.abort();
11
+ this.#abort = new AbortController();
12
+ }
13
+ };
14
+
15
+ var MicrotaskCoalescer = class {
16
+ #run;
17
+ #queued = false;
18
+ #active = false;
19
+ #generation = 0;
20
+ constructor(run) {
21
+ this.#run = run;
22
+ }
23
+ activate() {
24
+ this.#active = true;
25
+ }
26
+ cancel() {
27
+ this.#active = false;
28
+ this.#queued = false;
29
+ this.#generation += 1;
30
+ }
31
+ get pending() {
32
+ return this.#queued;
33
+ }
34
+ schedule() {
35
+ if (!this.#active || this.#queued) return;
36
+ this.#queued = true;
37
+ const generation = this.#generation;
38
+ queueMicrotask(() => {
39
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
40
+ this.#queued = false;
41
+ this.#run();
42
+ });
43
+ }
44
+ };
45
+
46
+ var MorphRenderWatcher = class {
47
+ #listeners = new ListenerSet();
48
+ #pass;
49
+ #element = null;
50
+ #onMorph = () => this.#pass.schedule();
51
+ constructor(run) {
52
+ this.#pass = new MicrotaskCoalescer(run);
53
+ }
54
+ observe(element) {
55
+ if (this.#element === element) return;
56
+ this.disconnect();
57
+ this.#element = element;
58
+ this.#pass.activate();
59
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
60
+ }
61
+ schedule() {
62
+ this.#pass.schedule();
63
+ }
64
+ get pending() {
65
+ return this.#pass.pending;
66
+ }
67
+ disconnect() {
68
+ this.#listeners.dispose();
69
+ this.#pass.cancel();
70
+ this.#element = null;
71
+ }
72
+ };
73
+
74
+ var NUMBER_BOUNDS = {
75
+ finite: { finite: true }};
76
+ function matchesNumberBounds(value, bounds) {
77
+ if (!Number.isFinite(value)) {
78
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
79
+ if (direction === null) return false;
80
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
81
+ }
82
+ if (bounds.min !== void 0 && value < bounds.min) return false;
83
+ if (bounds.max !== void 0 && value > bounds.max) return false;
84
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
85
+ if (bounds.integer && !Number.isInteger(value)) return false;
86
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
87
+ return true;
88
+ }
89
+
90
+ function readNumber(raw, fallback, bounds) {
91
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
92
+ }
93
+
94
+ var NumberValueReader = class {
95
+ #lastRejected = /* @__PURE__ */ new Map();
96
+ read(owner, name, raw, fallback, bounds) {
97
+ const resolved = readNumber(raw, fallback, bounds);
98
+ if (matchesNumberBounds(raw, bounds)) {
99
+ this.#lastRejected.delete(name);
100
+ return resolved;
101
+ }
102
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
103
+ const literal = owner.element.getAttribute(attribute);
104
+ if (literal === null) {
105
+ this.#lastRejected.delete(name);
106
+ return resolved;
107
+ }
108
+ if (this.#lastRejected.get(name) !== literal) {
109
+ this.#lastRejected.set(name, literal);
110
+ console.warn(
111
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
112
+ );
113
+ }
114
+ return resolved;
115
+ }
116
+ };
117
+
5
118
  var PLACEMENTS = /* @__PURE__ */ new Set([
6
119
  "top",
7
120
  "top-start",
@@ -16,7 +129,8 @@ var PLACEMENTS = /* @__PURE__ */ new Set([
16
129
  "left-start",
17
130
  "left-end"
18
131
  ]);
19
- var AnchoredController = class extends Controller {
132
+ var AnchoredController = class _AnchoredController extends Controller {
133
+ #numbers = new NumberValueReader();
20
134
  static targets = ["anchor", "floating"];
21
135
  static values = {
22
136
  placement: { type: String, default: "bottom" },
@@ -27,32 +141,29 @@ var AnchoredController = class extends Controller {
27
141
  strategy: { type: String, default: "absolute" },
28
142
  active: { type: Boolean, default: true }
29
143
  };
144
+ static valueConstraints = {
145
+ offset: NUMBER_BOUNDS.finite,
146
+ padding: NUMBER_BOUNDS.finite
147
+ };
30
148
  static events = ["position"];
31
- /** `autoUpdate` cleanup while tracking; `null` when detached. */
32
149
  #stop = null;
33
- /** Identity of the live attach; `null` when detached — see {@link #onComputed}. */
34
150
  #attachId = null;
35
- /** True between connect and disconnect (Stimulus may fire value callbacks before connect). */
36
151
  #connected = false;
37
- /**
38
- * Serialized options as of the last reconcile, empty only before the first one —
39
- * a value no serialization produces, so it can never match a real key.
40
- */
152
+ #position = null;
153
+ #morphRender = new MorphRenderWatcher(() => this.#sync());
41
154
  #appliedKey = "";
42
- /** Elements the live observer holds, or `null` when detached — see {@link #sync}. */
43
155
  #appliedAnchor = null;
44
156
  #appliedFloating = null;
45
157
  connect() {
158
+ this.#morphRender.observe(this.element);
46
159
  this.#connected = true;
47
160
  this.#sync();
48
161
  }
49
162
  disconnect() {
163
+ this.#morphRender.disconnect();
50
164
  this.#connected = false;
51
165
  this.#sync();
52
166
  }
53
- // Every value change (active or an option) and every target arrival or
54
- // departure re-syncs. `#sync` is idempotent and order-independent, so no Value
55
- // has to be declared in a particular position.
56
167
  anchorTargetConnected() {
57
168
  this.#sync();
58
169
  }
@@ -86,41 +197,23 @@ var AnchoredController = class extends Controller {
86
197
  strategyValueChanged() {
87
198
  this.#sync();
88
199
  }
89
- /** Current Values mapped to the positioning engine's options. */
90
200
  get #options() {
91
201
  return {
92
- // Narrow every free-form Value to what the engine can use. A placement
93
- // outside the set would be published on the hook as a resolved side it is
94
- // not, and a non-finite distance poisons the coordinate it feeds until the
95
- // browser drops the whole declaration and leaves that axis unplaced.
96
202
  placement: PLACEMENTS.has(this.placementValue) ? this.placementValue : "bottom",
97
- offset: Number.isFinite(this.offsetValue) ? this.offsetValue : 0,
203
+ offset: this.#safeOffset,
98
204
  flip: this.flipValue,
99
205
  shift: this.shiftValue,
100
- padding: Number.isFinite(this.paddingValue) ? this.paddingValue : 0,
206
+ padding: this.#safePadding,
101
207
  strategy: this.strategyValue === "fixed" ? "fixed" : "absolute"
102
208
  };
103
209
  }
104
- /**
105
- * Reconciles the live observer with the desired state — track iff connected,
106
- * `active`, and both targets exist — re-attaching only when that state, the
107
- * options, or the elements actually changed. Stimulus fires the value-changed
108
- * callbacks on connect in declaration order and may run them before or after
109
- * `connect()`; keying on what is applied collapses that whole burst (in any
110
- * order) to a single attach, while an option or target change at runtime
111
- * re-attaches exactly once. So correctness never depends on `active` being
112
- * declared last. The elements are part of the key because the engine holds the
113
- * pair it was handed: a swap leaves the same options behind and would otherwise
114
- * keep measuring the node that just left the document.
115
- *
116
- * @stimeoRenderRoot
117
- */
118
210
  #sync() {
119
211
  const shouldTrack = this.#connected && this.activeValue && this.hasAnchorTarget && this.hasFloatingTarget;
120
212
  const anchor = shouldTrack ? this.anchorTarget : null;
121
213
  const floating = shouldTrack ? this.floatingTarget : null;
122
214
  const key = JSON.stringify(this.#options);
123
215
  if (key === this.#appliedKey && anchor === this.#appliedAnchor && floating === this.#appliedFloating) {
216
+ if (floating && this.#position) this.#reflectPosition(floating, this.#position);
124
217
  return;
125
218
  }
126
219
  this.#detach();
@@ -143,27 +236,42 @@ var AnchoredController = class extends Controller {
143
236
  this.#stop?.();
144
237
  this.#stop = null;
145
238
  this.#attachId = null;
239
+ this.#position = null;
146
240
  }
147
- /**
148
- * Reflects the resolved side onto the CSS hook and announces the placement.
149
- *
150
- * The pass carries the attach that started it and the element it positioned,
151
- * and lands only while that attach is still the live one: `autoUpdate`'s
152
- * cleanup stops further updates but cannot cancel one already computing, so a
153
- * pass superseded mid-flight would otherwise write and dispatch afterwards.
154
- * The element cannot stand in for that identity — an option change re-attaches
155
- * against the very same pair.
156
- */
157
241
  #onComputed(id, floating, result) {
158
242
  if (id !== this.#attachId) return;
159
- floating.setAttribute("data-anchored-placement", result.placement);
243
+ this.#position = result;
244
+ this.#reflectPosition(floating, result);
160
245
  this.dispatch("position", {
161
246
  detail: { placement: result.placement, x: result.x, y: result.y }
162
247
  });
163
248
  }
249
+ #reflectPosition(floating, result) {
250
+ floating.setAttribute("data-anchored-placement", result.placement);
251
+ floating.style.position = this.#options.strategy ?? "absolute";
252
+ floating.style.left = `${result.x}px`;
253
+ floating.style.top = `${result.y}px`;
254
+ }
255
+ get #safeOffset() {
256
+ return this.#numbers.read(
257
+ this,
258
+ "offset",
259
+ this.offsetValue,
260
+ _AnchoredController.values.offset.default,
261
+ _AnchoredController.valueConstraints.offset
262
+ );
263
+ }
264
+ get #safePadding() {
265
+ return this.#numbers.read(
266
+ this,
267
+ "padding",
268
+ this.paddingValue,
269
+ _AnchoredController.values.padding.default,
270
+ _AnchoredController.valueConstraints.padding
271
+ );
272
+ }
164
273
  };
165
274
 
166
- // src/positioning/index.ts
167
275
  function buildMiddleware(options) {
168
276
  const padding = options.padding ?? 0;
169
277
  const middleware = [];
@@ -202,5 +310,4 @@ function registerPositioning(application) {
202
310
  }
203
311
 
204
312
  export { AnchoredController, attachPositioning, position, positioningControllers, registerPositioning };
205
- //# sourceMappingURL=index.js.map
206
313
  //# sourceMappingURL=index.js.map
@@ -12,8 +12,10 @@ module Stimeo
12
12
  # Sets up an importmap-rails app to use Stimeo UI without npm:
13
13
  # 1. vendors the gem's prebuilt JS into vendor/javascript/stimeo/
14
14
  # 2. pins "stimeo-ui" in config/importmap.rb
15
- # 3. registers every stimeo--* controller with the app's Stimulus
16
- # application (app/javascript/controllers/stimeo.js)
15
+ # 3. registers the "stimeo-ui" entry point's controllers (registerStimeo) with the
16
+ # app's Stimulus application (app/javascript/controllers/stimeo.js); the
17
+ # positioning and cable subpaths stay opt-in: their pins and registrations
18
+ # are written commented out in config/importmap.rb and controllers/stimeo.js
17
19
  #
18
20
  # Re-running is safe: the vendor copy is refreshed in place, while pins
19
21
  # and imports are only appended when missing — so after a gem update you
@@ -60,6 +62,9 @@ module Stimeo
60
62
  pin "stimeo-ui", to: "stimeo/index.js"
61
63
  # Opt-in positioning module — also pin @floating-ui/dom if you enable it:
62
64
  # pin "stimeo-ui/positioning", to: "stimeo/positioning/index.js"
65
+ # Opt-in cable module — also pin @rails/actioncable if you enable it:
66
+ # pin "stimeo-ui/cable", to: "stimeo/cable/index.js"
67
+ # pin "@rails/actioncable", to: "actioncable.esm.js"
63
68
  RUBY
64
69
  end
65
70
  end
@@ -1,12 +1,25 @@
1
- // Registers every stimeo--* controller with the app's Stimulus application.
1
+ // Registers the controllers of the "stimeo-ui" entry point with the app's
2
+ // Stimulus application. The opt-in "stimeo-ui/positioning" and "stimeo-ui/cable"
3
+ // entry points register theirs separately (registerPositioning / registerCable):
4
+ // pin the entry point and its peer in config/importmap.rb, then uncomment its
5
+ // import and call below. An app that already owns an Action Cable consumer passes
6
+ // it to setCableConsumer (also from "stimeo-ui/cable") before registerCable, so
7
+ // both share one websocket.
2
8
  // Generated by `rails g stimeo:install`. Safe to edit.
3
9
  //
4
- // To cherry-pick instead of registering all controllers, import them
5
- // individually from "stimeo-ui/controllers/<name>" and register under your own
6
- // identifiers. Note: with a JS bundler this works as-is; with importmap-rails
7
- // each "stimeo-ui/controllers/<name>" needs its own pin (this generator only
8
- // pins the "stimeo-ui" entrypoint), so add those pins to config/importmap.rb.
10
+ // To cherry-pick instead of registering the whole entry point, import controllers
11
+ // individually from "stimeo-ui/controllers/<name>_controller" and register under
12
+ // your own identifiers. Note: with a JS bundler this works as-is; with
13
+ // importmap-rails each "stimeo-ui/controllers/<name>_controller" needs its own pin
14
+ // (this generator only pins the "stimeo-ui" entrypoint), so add those pins to
15
+ // config/importmap.rb.
9
16
  import { application } from "controllers/application";
10
17
  import { registerStimeo } from "stimeo-ui";
11
18
 
12
19
  registerStimeo(application);
20
+
21
+ // import { registerPositioning } from "stimeo-ui/positioning";
22
+ // registerPositioning(application);
23
+
24
+ // import { registerCable } from "stimeo-ui/cable";
25
+ // registerCable(application);
@@ -5,6 +5,6 @@ module Stimeo
5
5
  # Kept in lockstep with package.json (stable versions share the same
6
6
  # notation; a prerelease maps npm `-beta.N` ⇔ gem `.pre.beta.N` because
7
7
  # RubyGems forbids dashes). Bump both together.
8
- VERSION = "0.16.0"
8
+ VERSION = "0.18.0"
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stimeo-ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.0
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stimeo Labs
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-26 00:00:00.000000000 Z
11
+ date: 2026-10-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -119,7 +119,7 @@ files:
119
119
  - dist/controllers/read_more_controller.js
120
120
  - dist/controllers/reading_progress_controller.js
121
121
  - dist/controllers/relative_time_controller.js
122
- - dist/controllers/reset_before_cache_controller.js
122
+ - dist/controllers/reset_on_restore_controller.js
123
123
  - dist/controllers/resizable_controller.js
124
124
  - dist/controllers/roving_controller.js
125
125
  - dist/controllers/scroll_area_controller.js