stimeo-ui 0.7.0 → 0.8.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8e3f69e3521c7e5d2ffa5e4b948aabbb26dc9bf8429217fa6235c9cdcafb53b1
4
- data.tar.gz: d9722417ea447febdc5188eb50812a3725987f65a6920e49f98e852b5f0076ba
3
+ metadata.gz: b7dc7faa9f1a7d87af3398be60736f22e76e8e1b6ef534d0b8605e3fcdb9f4eb
4
+ data.tar.gz: 05bd47298cc4e0592f1247140c83e1cedff106c91b6a972734e5d9d3a507e3ea
5
5
  SHA512:
6
- metadata.gz: 1fe28608ac28813a4e12b448b438853d19e2acdd948fe2af5d287b615e83439a933023881a72bec2a50d579c384141be0c132310a640540b5d7eaee5554e2f28
7
- data.tar.gz: ddb5ffd05af127033055296b6a66e6e71344b89dbcc42f0c88a34bc7e88193cb2c48deb67f4f2250edd19a0bed15957914c5ee90afd137ecce1c11cfc818df5b
6
+ metadata.gz: aa22c951a7b4d5ba07c34f5f17c86227f8079d1222c40d895a1198c274c021198dc89edad6c129df4526b79001455834c88bd7ffdd46eb95dbd76166dba5975e
7
+ data.tar.gz: f95da4d63fee6006b75ec78a7dd91a73d3535fe82b456a298dd7a273e1f415df829c0fe08fc4c065d0d680d70d8434874226f30eadb5eccf4e5fc1b5ef5c99cb
data/CHANGELOG.md CHANGED
@@ -7,6 +7,67 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
  While the version is `0.x`, the public API (the `stimeo--*` data attributes) may
8
8
  change between releases.
9
9
 
10
+ ## [0.8.0] - 2026-08-22
11
+
12
+ Minor release with no new components. Eight existing ones are reworked —
13
+ auto-submit, carousel, currency-input, file-dropzone, input-mask, nested-form,
14
+ otp, and textarea-autosize — and of those, carousel, otp, and file-dropzone also
15
+ changed their markup contracts, so read Removed and Changed before upgrading. The
16
+ Inspector manifest stays on schema v12.
17
+
18
+ ### Removed
19
+
20
+ - otp: `length` — the number of `field` targets is the passcode length.
21
+ - file-dropzone: the `status` target, `dragLabel`, and the
22
+ `data-file-dropzone-slot` attributes.
23
+
24
+ ### Added
25
+
26
+ - `reconcile` on auto-submit, direct-upload, file-dropzone, flash, frame-loading,
27
+ input-mask, nested-form, otp, spinner, and submit-once — it also reports the
28
+ state a Turbo rewind discarded, carrying what that component lost
29
+ (`{ files }`, `{ ids }`, `{ forms }`, `{ removed }`, or `{}`).
30
+ - carousel: `data-state`, `inert` on inactive slides, `prefers-reduced-motion`,
31
+ and optional pickers. otp: `clear` / `onPointerDown`, and text carrying several
32
+ characters spread across the following fields. file-dropzone:
33
+ `allowDuplicates`, a `thumb` target, and the `announce*Text` wording.
34
+ - Characters an IME commits full-width are read as their half-width form in
35
+ input-mask, otp, and currency-input.
36
+ - Inspector: rules for the reworked markup — carousel's controller-owned ARIA,
37
+ the `playToggle` an `autoplay` carousel needs, the `prev` / `next` pair, a
38
+ finite positive `interval`, otp's group and field names, and the parts
39
+ file-dropzone's item template must contain.
40
+
41
+ ### Changed
42
+
43
+ - carousel: drop the `data-action` wiring and any authored `aria-pressed` —
44
+ clicks, picker keys, hover, and focus are delegated, and `aria-pressed` /
45
+ `aria-disabled` / `aria-live` / `aria-atomic` are controller-owned. Name the
46
+ toggle with `aria-label`, pair `prev` with `next`, and give an `autoplay`
47
+ carousel a `playToggle` (WCAG 2.2.2).
48
+ - otp: the root needs `role="group"` and a name, and each `field` its own.
49
+ `invalid` reports only input that filled no field.
50
+ - file-dropzone: put a `stimeo--announcer` on the page, set the `announce*Text`
51
+ wording, and use `name` / `thumb` / `remove` targets in the item template with
52
+ an authored `aria-label`. Validation runs `type` → `size` → `duplicate` →
53
+ `count`, and `change` precedes `reject`.
54
+ - input-mask: `change` fires only for a user edit; normalization and runtime
55
+ Value changes report `reconcile`. The manifest reports `tokens` as `String`
56
+ where it reported `Object`.
57
+ - textarea-autosize: `resize` no longer fires when a connect leaves the height
58
+ unchanged; width, font, and runtime `minRows` / `maxRows` changes are followed.
59
+ - currency-input keeps what was typed while typing and rounds on blur;
60
+ nested-form absorbs rows added or removed outside it; auto-submit follows a
61
+ swapped `form` target.
62
+
63
+ ### Fixed
64
+
65
+ - carousel: the play toggle stops rotation — the `focusin` before the click no
66
+ longer makes the press resume instead. otp: a full-width commit into one field
67
+ fills the following ones. currency-input: a locale with non-Latin digits
68
+ reparses its own output. nested-form: a discarded-but-visible row's remove
69
+ button no longer swallows the click.
70
+
10
71
  ## [0.7.0] - 2026-08-16
11
72
 
12
73
  Minor release reworking the form stack, separator, and scroll-area, with
@@ -634,6 +695,7 @@ Initial public alpha: 101 behavior-only, accessible Stimulus controllers driven
634
695
  by `data-*` attributes, shipping no CSS. Published to npm (with provenance) and
635
696
  RubyGems.
636
697
 
698
+ [0.8.0]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.8.0
637
699
  [0.7.0]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.7.0
638
700
  [0.6.0]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.6.0
639
701
  [0.5.0]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.5.0
@@ -2,6 +2,35 @@ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  // src/controllers/auto_submit_controller.ts
4
4
 
5
+ // src/utils/before_cache_reset.ts
6
+ var BeforeCacheReset = class _BeforeCacheReset {
7
+ /** Every subscribed instance, iterated by the one shared document listener. */
8
+ static #subscribers = /* @__PURE__ */ new Set();
9
+ /** The shared listener; installed while at least one instance is subscribed. */
10
+ static #onBeforeCache = () => {
11
+ for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
12
+ };
13
+ #rewind;
14
+ /** @param rewind - the pass that returns this controller's state to its initial form. */
15
+ constructor(rewind) {
16
+ this.#rewind = rewind;
17
+ }
18
+ /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
19
+ activate() {
20
+ const first = _BeforeCacheReset.#subscribers.size === 0;
21
+ _BeforeCacheReset.#subscribers.add(this);
22
+ if (first) {
23
+ document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
24
+ }
25
+ }
26
+ /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
27
+ deactivate() {
28
+ _BeforeCacheReset.#subscribers.delete(this);
29
+ if (_BeforeCacheReset.#subscribers.size > 0) return;
30
+ document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
31
+ }
32
+ };
33
+
5
34
  // src/utils/composition_tracker.ts
6
35
  var CompositionTracker = class {
7
36
  #observedTargets = /* @__PURE__ */ new Set();
@@ -112,14 +141,18 @@ var AutoSubmitController = class extends Controller {
112
141
  message: { type: String, default: "" }
113
142
  };
114
143
  static actions = ["submit"];
115
- static events = ["submit", "done"];
144
+ static events = ["submit", "done", "reconcile"];
116
145
  /** Debounce timer registry; one `clearAll()` in disconnect tears it down. */
117
146
  #timers = new SafeTimeout();
118
147
  /** Id of the pending debounce timer, so a new keystroke can reset it. */
119
148
  #pendingId = 0;
149
+ /** The form the listeners are attached to; target callbacks rebind it. */
150
+ #boundForm = null;
151
+ /** Rewinds the transient state hooks just before Turbo snapshots the page. */
152
+ #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
120
153
  /** Clears `aria-busy` and emits completion once Turbo finishes the submit. */
121
154
  #onSubmitEnd = () => {
122
- this.#form.removeAttribute("aria-busy");
155
+ this.#boundForm?.removeAttribute("aria-busy");
123
156
  const message = this.messageValue;
124
157
  this.dispatch("done", { detail: { message: message || void 0 } });
125
158
  if (this.announceValue && message) {
@@ -133,15 +166,23 @@ var AutoSubmitController = class extends Controller {
133
166
  }
134
167
  });
135
168
  connect() {
136
- this.#form.addEventListener("turbo:submit-end", this.#onSubmitEnd);
137
- this.#composition.observe(this.#form);
169
+ this.#beforeCache.activate();
170
+ this.#bindForm(this.#resolveForm());
138
171
  }
139
172
  disconnect() {
173
+ this.#beforeCache.deactivate();
174
+ this.#bindForm(null);
175
+ this.#composition.disconnect();
140
176
  this.#timers.clearAll();
141
177
  this.#pendingId = 0;
142
- this.#composition.disconnect();
143
- this.#form.removeAttribute("data-auto-submit-pending");
144
- this.#form.removeEventListener("turbo:submit-end", this.#onSubmitEnd);
178
+ }
179
+ /** Follows a `form` target added (or swapped in) at runtime. */
180
+ formTargetConnected() {
181
+ this.#bindForm(this.#resolveForm());
182
+ }
183
+ /** Follows a `form` target removed (or swapped out) at runtime. */
184
+ formTargetDisconnected() {
185
+ this.#bindForm(this.#resolveForm());
145
186
  }
146
187
  /**
147
188
  * Schedules a debounced submit. Wired to `input`/`change`; the `on` value is an
@@ -155,21 +196,58 @@ var AutoSubmitController = class extends Controller {
155
196
  }
156
197
  /** Schedules (and coalesces) the debounced submit for the given trigger. */
157
198
  #schedule(trigger) {
158
- this.#form.setAttribute("data-auto-submit-pending", "true");
159
- if (this.#pendingId) this.#timers.clear(this.#pendingId);
199
+ const form = this.#boundForm;
200
+ if (!form) return;
201
+ form.setAttribute("data-auto-submit-pending", "true");
202
+ this.#cancelPending();
160
203
  this.#pendingId = this.#timers.set(() => {
161
204
  this.#pendingId = 0;
162
- this.#form.removeAttribute("data-auto-submit-pending");
205
+ form.removeAttribute("data-auto-submit-pending");
163
206
  this.dispatch("submit", { detail: { trigger } });
164
- if (this.#form.checkValidity()) {
165
- this.#form.setAttribute("aria-busy", "true");
207
+ if (form.checkValidity()) {
208
+ form.setAttribute("aria-busy", "true");
166
209
  }
167
- this.#form.requestSubmit();
210
+ form.requestSubmit();
168
211
  }, this.debounceValue);
169
212
  }
170
- /** Resolves the form element (explicit `form` target, else the controller root). */
171
- get #form() {
172
- return this.hasFormTarget ? this.formTarget : this.element;
213
+ /** Cancels the pending debounced submit, if any (`clear` no-ops on unknown ids). */
214
+ #cancelPending() {
215
+ this.#timers.clear(this.#pendingId);
216
+ this.#pendingId = 0;
217
+ }
218
+ /**
219
+ * Replaces the subscribed form symmetrically. The outgoing form loses the
220
+ * `turbo:submit-end`/composition listeners, its pending hook, and any pending
221
+ * debounced submit (which described the outgoing form). An in-flight `aria-busy`
222
+ * is left for `turbo:submit-end` or the pre-cache rewind — removing it here
223
+ * would wipe a legitimately in-progress submission.
224
+ */
225
+ #bindForm(form) {
226
+ if (form === this.#boundForm) return;
227
+ const previous = this.#boundForm;
228
+ if (previous) {
229
+ this.#cancelPending();
230
+ previous.removeAttribute("data-auto-submit-pending");
231
+ previous.removeEventListener("turbo:submit-end", this.#onSubmitEnd);
232
+ this.#composition.unobserve(previous);
233
+ }
234
+ this.#boundForm = form;
235
+ if (!form) return;
236
+ form.addEventListener("turbo:submit-end", this.#onSubmitEnd);
237
+ this.#composition.observe(form);
238
+ }
239
+ /** Resolves the form: the explicit `form` target, else a `<form>` root, else null. */
240
+ #resolveForm() {
241
+ if (this.hasFormTarget) return this.formTarget;
242
+ return this.element instanceof HTMLFormElement ? this.element : null;
243
+ }
244
+ /** Returns the transient state hooks to their initial (absent) state. */
245
+ #rewindForCache() {
246
+ const inProgress = this.#pendingId !== 0 || this.#boundForm?.hasAttribute("aria-busy") === true;
247
+ this.#cancelPending();
248
+ this.#boundForm?.removeAttribute("data-auto-submit-pending");
249
+ this.#boundForm?.removeAttribute("aria-busy");
250
+ if (inProgress) this.dispatch("reconcile", { detail: {} });
173
251
  }
174
252
  /** Whether `type` is one of the whitespace-separated event types in `on`. */
175
253
  #triggers(type) {