stimeo-ui 0.17.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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +106 -1
  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 +289 -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 +64 -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 +199 -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 +411 -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 +251 -94
  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 +249 -23
  67. data/dist/controllers/popover_controller.js +82 -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 +2451 -1312
  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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ab94f6bcd6de9b272ef9049a92e84b7e69af7d955dee82d24fef5dc09f61682
4
- data.tar.gz: 180f338981fd0e02a0c5dc9b30032eedfcdb0841d5734524d02d00862bbfad57
3
+ metadata.gz: 05c2871de0e4c97223dfb10803e8c4fa84abc3a0cf5e9c946d6570d88c46f065
4
+ data.tar.gz: a772dc1b21e16d21ea37af8dbb2f6a56618fa94252fe43eebcbede4ab296c2a2
5
5
  SHA512:
6
- metadata.gz: f65e42f9fdbe28e84af7ecc703f2bd92620bbe75ef197dfd478255286f23278625a1bc3c57237c3390b60c3f0d1714ffdc60ddb6584a334187e2775dbc36072a
7
- data.tar.gz: 8c832a6e93363515f8343913f2456113086812f7d6fbd63f8751b2279cecb8e3682482978cd5adee9354fd871fbae0b328d38797fdcd7a81e4a8703016bf78a8
6
+ metadata.gz: a74a9a638fadb04fcec7c72ced80825fb75215051533508d2e58c40fc64ef4eb3aa41a0b76ee85ab9697004f9861d6b6a46b75ec62c39b013d274487f7986d42
7
+ data.tar.gz: 2b0ff0e66bc0f7062c241ee01032888206b300eb74c552561c3638638a9255a19ea69b1a9555fa5e72b50ab1e8bd8479f9b37f8e79743077df6da59ee65e2409
data/CHANGELOG.md CHANGED
@@ -9,6 +9,110 @@ change between releases.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.18.0] - 2026-10-22
13
+
14
+ Minor release with no new components. IME composition, Turbo morphs, and form
15
+ submission now behave consistently across the input components, and several
16
+ contracts change, so read Changed before upgrading. The Inspector manifest moves
17
+ to schema v16; update `stimeo check` and the VS Code extension together and run
18
+ `stimeo check` before deploying.
19
+
20
+ ### Added
21
+
22
+ - time-picker: `amLabel` and `pmLabel` Values set the meridiem text and its
23
+ speech, and `hour12ZeroBased` shows and enters a twelve-hour clock's hours as
24
+ 0–11, as `午前` / `午後` time is written. The submitted value stays on the
25
+ 24-hour clock.
26
+
27
+ ### Changed
28
+
29
+ - **Breaking** input-mask: a synchronized hidden field carries a unique internal
30
+ DOM name; native FormData and native / Turbo submission restore the original
31
+ name, and removing the source submits an empty entry. Find managed fields by id
32
+ or the pairing attribute, use native FormData, and call
33
+ `prepareInputMaskForm(form)` before reading FormData from a new detached form.
34
+ input-mask clears only the raw value it wrote.
35
+ - **Breaking** otp: a pattern drop held for a composition is reported as
36
+ `reconcile` before the edit's own `change` / `complete` / `invalid`, and a
37
+ reconnect under another `pattern` drops what it rejects as `reconcile`. Use the
38
+ edit events for submission.
39
+ - **Breaking** number-input: a range or step change made during IME composition
40
+ applies when the user confirms, reported as `change`; a morph that changes
41
+ neither holds nothing.
42
+ - **Breaking** currency-input: actions and events act only on the first `display`
43
+ target, and a display already focused when the controller connects starts
44
+ formatting at its next ordinary edit or composition end. Render the initial
45
+ hidden and accessible values on the server. `stimeo check` warns about extra
46
+ displays.
47
+ - **Breaking** tabs, calendar, date-range-picker, slider, range-slider,
48
+ color-picker, multi-select, and tags-input read the writing direction from their
49
+ layout targets instead of the root; carousel gains a `pickerList` and
50
+ navigation-menu a `list` target.
51
+ - **Breaking** dirty-form and optimistic accept only the terminal event matching
52
+ the latest start's source and token, and only `success: true` succeeds. For a
53
+ custom save, call `markClean` or dispatch the paired events with `success`.
54
+ - **Breaking** local-time and relative-time read `datetime` strictly: a clock
55
+ without a zone is UTC (relative-time read it as local time), and an incomplete
56
+ or invalid value keeps the current display.
57
+ - **Breaking** presence and typing-indicator templates use `{count}`, `{name}`,
58
+ and `{names}` instead of `%{…}`.
59
+ - **Breaking** modified `Home` / `End` / `PageUp` / `PageDown` go to the browser
60
+ in widgets without a dedicated binding; pointer-drag cancels a keyboard grab
61
+ when focus leaves the handle; time-picker accepts digits typed with `Shift`.
62
+ - **Breaking** progress, meter, pointer-drag, countdown, overflow-menu, listbox,
63
+ submit-once, clipboard, announcer, and currency-input change their ownership
64
+ markers (pointer-drag: `data-stimeo--pointer-drag-tabindex-loan`, listbox:
65
+ `owns-label`); re-render persisted HTML from authored markup. progress and meter
66
+ restore the authored `aria-valuetext` when the template is cleared or the
67
+ controller disconnects.
68
+ - **Breaking** menubar, navigation-menu, menu, and context-menu treat an entry
69
+ inside an `aria-disabled="true"` ancestor as disabled (it stays reachable with
70
+ the arrow keys), and with nested entries refuse an action when any entry holding
71
+ its target is disabled. A click is judged when it reaches the component, so a
72
+ handler inside the component that disables its entry during the click keeps the
73
+ open or close it started; a menubar top it leaves natively `disabled` then has
74
+ its menu closed again, without a `close` event.
75
+ - **Breaking** pointer-drag: `reset()` returns the inline `translate` to the
76
+ origin only when `follow` is on; otherwise it cancels the gesture in flight and
77
+ leaves the author's `translate` alone.
78
+ - toggle-group writes `aria-pressed`, time-picker `aria-valuenow`, and radio-group
79
+ its arrival and release `tabindex` only when the value changes.
80
+ - context-menu: the region contract drops `aria-haspopup`; give the region a role
81
+ and name that fit its content and advertise the key with
82
+ `aria-keyshortcuts="Shift+F10"`.
83
+ - `stimeo check` reports `missing-announcer` for the `stimeo-ui/cable`
84
+ controllers (live-counter, presence, typing-indicator), and the
85
+ `stimeo:install` generator writes commented-out pins and registrations for the
86
+ opt-in `stimeo-ui/cable` and `stimeo-ui/positioning` entry points.
87
+
88
+ ### Removed
89
+
90
+ - character-counter: the `announceTextValueChanged()` callback is gone from the
91
+ shipped types; pending announcements still read the current `announceText`.
92
+
93
+ ### Fixed
94
+
95
+ - IME composition no longer sticks after a page write, a move, or a Turbo morph
96
+ in auto-submit, character-counter, combobox, command-palette, currency-input,
97
+ editable, input-mask, multi-select, number-input, otp, tags-input, and toolbar.
98
+ A morph leaves a field mid-composition alone, and a focused draft survives
99
+ morphs and reconnection.
100
+ - switch, otp, currency-input, and input-mask keep their hidden form values right
101
+ across Turbo morphs and removal.
102
+ - State events and reconciliation stay right when a `change` listener disconnects
103
+ the controller or the page edits state during a repair.
104
+ - Components release what an earlier connection registered when connected twice,
105
+ and keep authored text, ARIA, and tab stops through restores.
106
+ - relative-time follows live `datetime` changes; template substitution leaves
107
+ inherited names such as `{constructor}` unchanged.
108
+ - menubar, navigation-menu, menu, and context-menu refuse a `hidden`, natively
109
+ `disabled`, or `aria-disabled` entry on every route — an explicit element, any
110
+ event, or hover — as a click on it already was; `context-menu#activate` takes
111
+ the optional event.
112
+ - The MCP server accepts JSON-RPC batches in sessions that negotiated protocol
113
+ version 2025-03-26; other versions, and batches before `initialize`, are still
114
+ rejected.
115
+
12
116
  ## [0.17.0] - 2026-10-04
13
117
 
14
118
  ### Changed
@@ -1307,7 +1411,8 @@ Initial public alpha: 101 behavior-only, accessible Stimulus controllers driven
1307
1411
  by `data-*` attributes, shipping no CSS. Published to npm (with provenance) and
1308
1412
  RubyGems.
1309
1413
 
1310
- [Unreleased]: https://github.com/taiyaky/stimeo-ui/compare/v0.17.0...HEAD
1414
+ [Unreleased]: https://github.com/taiyaky/stimeo-ui/compare/v0.18.0...HEAD
1415
+ [0.18.0]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.18.0
1311
1416
  [0.17.0]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.17.0
1312
1417
  [0.16.0]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.16.0
1313
1418
  [0.15.0]: https://github.com/taiyaky/stimeo-ui/releases/tag/v0.15.0
data/README.md CHANGED
@@ -15,9 +15,9 @@
15
15
  owns the look entirely.
16
16
 
17
17
  - Lean by design: the **core** needs only `@hotwired/stimulus` at runtime (kept
18
- external in the build). The opt-in `stimeo-ui/positioning` module is the one
19
- exception — it uses `@floating-ui/dom` (an optional **peer dependency**; see the
20
- Peer dependencies note below).
18
+ external in the build). Two opt-in subpaths each add one optional **peer
19
+ dependency**: `stimeo-ui/positioning` (`@floating-ui/dom`) and `stimeo-ui/cable`
20
+ (`@rails/actioncable`); see the Peer dependencies note below.
21
21
  - Accessibility first: every controller follows the relevant WAI-ARIA APG pattern
22
22
  and the related WCAG 2.2 AA criteria.
23
23
  - Public controller identifiers use the `stimeo--` namespace (e.g.
@@ -36,8 +36,12 @@ bin/rails generate stimeo:install
36
36
  ```
37
37
 
38
38
  The generator vendors the prebuilt JS into `vendor/javascript/stimeo/`, pins
39
- `stimeo-ui` in `config/importmap.rb`, and registers all controllers with your
40
- Stimulus application. Then drive components from HTML alone:
39
+ `stimeo-ui` in `config/importmap.rb`, and registers the core controllers with your
40
+ Stimulus application (`app/javascript/controllers/stimeo.js`). It writes the opt-in
41
+ `stimeo-ui/positioning` and `stimeo-ui/cable` entry points into those two files
42
+ commented out; to enable one, uncomment its lines. The cable lines include the pin
43
+ for its `@rails/actioncable` peer; positioning also needs a pin for
44
+ `@floating-ui/dom`. Then drive components from HTML alone:
41
45
 
42
46
  ```erb
43
47
  <div data-controller="stimeo--dropdown">
@@ -58,9 +62,12 @@ import { Application } from "@hotwired/stimulus";
58
62
  import { registerStimeo } from "stimeo-ui";
59
63
 
60
64
  const application = Application.start();
61
- registerStimeo(application); // registers every stimeo--* controller
65
+ registerStimeo(application); // registers every controller of the core entry point
62
66
  ```
63
67
 
68
+ The opt-in `stimeo-ui/positioning` and `stimeo-ui/cable` entry points register
69
+ theirs with `registerPositioning` / `registerCable`.
70
+
64
71
  Need only a few controllers? Import them individually from
65
72
  `stimeo-ui/controllers/*` and register them under your own identifiers. Each
66
73
  file loads on its own, yet the coordination between components — which layer
@@ -73,7 +80,8 @@ and peer departure decisions; generated ARIA ids stay distinct across copies too
73
80
 
74
81
  - **Peer dependencies:** `@hotwired/stimulus` (always), `@floating-ui/dom` (only
75
82
  if you use the opt-in `stimeo-ui/positioning` module — tooltips, popovers, etc.
76
- work without it via the default flow layout).
83
+ work without it via the default flow layout), and `@rails/actioncable` (only if
84
+ you use the opt-in `stimeo-ui/cable` module).
77
85
  - **No CSS is shipped.** Style the components yourself; controllers only toggle
78
86
  ARIA state and `data-*` hooks.
79
87
 
@@ -118,8 +126,9 @@ keep the rules on everywhere else. For Biome:
118
126
  The `eslint-plugin-jsx-a11y` equivalents are
119
127
  `no-noninteractive-element-to-interactive-role`, `no-redundant-roles`,
120
128
  `prefer-tag-over-role`, `interactive-supports-focus`, and
121
- `no-noninteractive-tabindex`. These components' real accessibility is exercised
122
- with axe-core and real screen readers in this project's own test suite.
129
+ `no-noninteractive-tabindex`. This repository's tests check these components
130
+ with axe-core and with a virtual screen reader for speech order; neither stands
131
+ in for a real screen reader on your own pages.
123
132
 
124
133
  ## Composing components
125
134
 
@@ -279,7 +288,7 @@ or in `.mcp.json` (Claude Code) / `.cursor/mcp.json` (Cursor):
279
288
  It exposes four read-only tools — `stimeo_check` (validate a source string),
280
289
  `stimeo_catalog`, `stimeo_controller` (one controller's full contract,
281
290
  accessibility requirements included), and `stimeo_example` (verified example
282
- markup: the official catalog demo under [`examples/`](examples/), bundled at
291
+ markup: the controller's demo under [`examples/`](examples/), bundled at
283
292
  build time and guaranteed to pass the checker) — plus MCP resources
284
293
  (`stimeo://manifest`, `stimeo://examples/<id>`) for preloading context without
285
294
  a tool round-trip. The server reads only its bundled manifest and example
data/dist/cable/index.js CHANGED
@@ -1,6 +1,13 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
  import { createConsumer } from '@rails/actioncable';
3
3
 
4
+ function fillTemplate(template, values) {
5
+ return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
6
+ const replacement = Object.hasOwn(values, name) ? values[name] : void 0;
7
+ return replacement === void 0 ? match : String(replacement);
8
+ });
9
+ }
10
+
4
11
  function announce(message, options = {}) {
5
12
  const text = message.trim();
6
13
  if (text.length === 0) return;
@@ -10,12 +17,6 @@ function announce(message, options = {}) {
10
17
  })
11
18
  );
12
19
  }
13
- function fillTemplate(template, values) {
14
- return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
15
- const replacement = values[name];
16
- return replacement === void 0 ? match : String(replacement);
17
- });
18
- }
19
20
 
20
21
  var TOKEN_BODY = /\d[\d.,'’’    _]*\d|\d/;
21
22
  var SIGN_OPENER = /[\s([]/;
@@ -97,6 +98,9 @@ var MicrotaskCoalescer = class {
97
98
  this.#queued = false;
98
99
  this.#generation += 1;
99
100
  }
101
+ get pending() {
102
+ return this.#queued;
103
+ }
100
104
  schedule() {
101
105
  if (!this.#active || this.#queued) return;
102
106
  this.#queued = true;
@@ -138,6 +142,9 @@ var MorphRenderWatcher = class {
138
142
  schedule() {
139
143
  this.#pass.schedule();
140
144
  }
145
+ get pending() {
146
+ return this.#pass.pending;
147
+ }
141
148
  disconnect() {
142
149
  this.#listeners.dispose();
143
150
  this.#pass.cancel();
@@ -601,6 +608,7 @@ var PresenceController = class _PresenceController extends Controller {
601
608
  #pendingBeacon = null;
602
609
  #announceId = null;
603
610
  #connected = false;
611
+ #cycleListeners = new ListenerSet();
604
612
  #speakerKey = "";
605
613
  #speakerId = "";
606
614
  channelValueChanged() {
@@ -666,10 +674,10 @@ var PresenceController = class _PresenceController extends Controller {
666
674
  received: (data) => this.#onReceived(data)
667
675
  });
668
676
  this.#armHeartbeat();
669
- window.addEventListener("pagehide", this.#onPageHide);
677
+ this.#cycleListeners.add(window, "pagehide", this.#onPageHide);
670
678
  }
671
679
  #close() {
672
- window.removeEventListener("pagehide", this.#onPageHide);
680
+ this.#cycleListeners.dispose();
673
681
  this.#sendLeaveNotice(this.#releaseSpeaker());
674
682
  this.#subscription?.unsubscribe();
675
683
  this.#subscription = null;
@@ -797,7 +805,7 @@ ${this.#speakerId}`;
797
805
  #countMessage(count) {
798
806
  const templates = this.countTarget.dataset;
799
807
  const template = (count === 0 ? templates.zero : count === 1 ? templates.one : templates.other) ?? templates.other;
800
- return template ? template.replace("%{count}", String(count)) : String(count);
808
+ return template ? fillTemplate(template, { count }) : String(count);
801
809
  }
802
810
  #drawList() {
803
811
  if (!this.hasListTarget) return;
@@ -877,7 +885,6 @@ var TRANSIENT3 = new TransientHooks({
877
885
  });
878
886
  var DEFAULT_TIMEOUT = 3e3;
879
887
  var DEFAULT_THROTTLE = 2e3;
880
- var fillTokens = (template, values) => template.replace(/%\{(name|names|count)\}/g, (match, token) => values[token] ?? match);
881
888
  var TypingIndicatorController = class _TypingIndicatorController extends Controller {
882
889
  #numbers = new NumberValueReader();
883
890
  static targets = ["input", "status"];
@@ -905,6 +912,7 @@ var TypingIndicatorController = class _TypingIndicatorController extends Control
905
912
  #lastSentAt = 0;
906
913
  #announceId = null;
907
914
  #connected = false;
915
+ #listeners = new ListenerSet();
908
916
  channelValueChanged() {
909
917
  this.#follow.schedule();
910
918
  }
@@ -927,14 +935,14 @@ var TypingIndicatorController = class _TypingIndicatorController extends Control
927
935
  connect() {
928
936
  this.#connected = true;
929
937
  this.#reset();
930
- this.element.addEventListener("input", this.#onInput);
938
+ this.#listeners.add(this.element, "input", this.#onInput);
931
939
  this.#follow.activate();
932
940
  this.#open();
933
941
  }
934
942
  disconnect() {
935
943
  this.#connected = false;
936
944
  this.#follow.cancel();
937
- this.element.removeEventListener("input", this.#onInput);
945
+ this.#listeners.dispose();
938
946
  this.#close();
939
947
  this.#reset();
940
948
  }
@@ -1034,10 +1042,10 @@ var TypingIndicatorController = class _TypingIndicatorController extends Control
1034
1042
  if (names.length === 1) {
1035
1043
  const template2 = this.statusTarget.dataset.one;
1036
1044
  const name = names[0] ?? "";
1037
- return template2 ? fillTokens(template2, { name }) : `${name} is typing\u2026`;
1045
+ return template2 ? fillTemplate(template2, { name }) : `${name} is typing\u2026`;
1038
1046
  }
1039
1047
  const template = this.statusTarget.dataset.many;
1040
- return template ? fillTokens(template, { names: joined, count: String(names.length) }) : `${joined} are typing\u2026`;
1048
+ return template ? fillTemplate(template, { names: joined, count: names.length }) : `${joined} are typing\u2026`;
1041
1049
  }
1042
1050
  get #safeTimeout() {
1043
1051
  return this.#numbers.read(
@@ -26,6 +26,16 @@ function isReservedArrowChord(event, allow = []) {
26
26
  if (!event.key.startsWith("Arrow")) return false;
27
27
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
28
28
  }
29
+ function hasModifierChord(event) {
30
+ return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
31
+ }
32
+
33
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
34
+ if (element.getAttribute(name) === value) return false;
35
+ if (value === null) element.removeAttribute(name);
36
+ else element.setAttribute(name, value);
37
+ return true;
38
+ }
29
39
 
30
40
  function sharedRegistry(key, create) {
31
41
  const symbol = Symbol.for(key);
@@ -81,9 +91,8 @@ var StateRegions = class _StateRegions {
81
91
  const below = hold.linked ? stack[index - 1] : void 0;
82
92
  if (below) {
83
93
  if (region.hidden !== below.written) region.hidden = below.written;
84
- } else if (region.getAttribute("hidden") !== hold.authored) {
85
- if (hold.authored === null) region.removeAttribute("hidden");
86
- else region.setAttribute("hidden", hold.authored);
94
+ } else {
95
+ setAttributeIfChanged(region, "hidden", hold.authored);
87
96
  }
88
97
  }
89
98
  this.#mark(region, stack);
@@ -158,9 +167,7 @@ var StateRegions = class _StateRegions {
158
167
  );
159
168
  });
160
169
  for (const [name, recorded] of records) {
161
- if (region.getAttribute(name) === recorded) continue;
162
- if (recorded === null) region.removeAttribute(name);
163
- else region.setAttribute(name, recorded);
170
+ setAttributeIfChanged(region, name, recorded);
164
171
  }
165
172
  }
166
173
  #recorded(region, name) {
@@ -251,6 +258,7 @@ var AccordionController = class extends Controller {
251
258
  onKeydown(event) {
252
259
  if (event.defaultPrevented) return;
253
260
  if (isReservedArrowChord(event)) return;
261
+ if ((event.key === "Home" || event.key === "End") && hasModifierChord(event)) return;
254
262
  const current = event.currentTarget;
255
263
  if (this.triggerTargets.indexOf(current) === -1) return;
256
264
  const navigable = this.triggerTargets.filter((trigger) => trigger.closest("[hidden]") === null);