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
@@ -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
 
@@ -174,6 +180,9 @@ var MicrotaskCoalescer = class {
174
180
  this.#queued = false;
175
181
  this.#generation += 1;
176
182
  }
183
+ get pending() {
184
+ return this.#queued;
185
+ }
177
186
  schedule() {
178
187
  if (!this.#active || this.#queued) return;
179
188
  this.#queued = true;
@@ -204,6 +213,9 @@ var MorphRenderWatcher = class {
204
213
  schedule() {
205
214
  this.#pass.schedule();
206
215
  }
216
+ get pending() {
217
+ return this.#pass.pending;
218
+ }
207
219
  disconnect() {
208
220
  this.#listeners.dispose();
209
221
  this.#pass.cancel();
@@ -1,5 +1,12 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
+ function fillTemplate(template, values) {
4
+ return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
5
+ const replacement = Object.hasOwn(values, name) ? values[name] : void 0;
6
+ return replacement === void 0 ? match : String(replacement);
7
+ });
8
+ }
9
+
3
10
  function announce(message, options = {}) {
4
11
  const text = message.trim();
5
12
  if (text.length === 0) return;
@@ -9,11 +16,12 @@ function announce(message, options = {}) {
9
16
  })
10
17
  );
11
18
  }
12
- function fillTemplate(template, values) {
13
- return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
14
- const replacement = values[name];
15
- return replacement === void 0 ? match : String(replacement);
16
- });
19
+
20
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
21
+ if (element.getAttribute(name) === value) return false;
22
+ if (value === null) element.removeAttribute(name);
23
+ else element.setAttribute(name, value);
24
+ return true;
17
25
  }
18
26
 
19
27
  function sharedRegistry(key, create) {
@@ -21,11 +29,15 @@ function sharedRegistry(key, create) {
21
29
  const scope = globalThis;
22
30
  const existing = scope[symbol];
23
31
  if (existing !== void 0) return existing;
24
- const registry = create();
25
- Object.defineProperty(globalThis, symbol, { value: registry });
26
- return registry;
32
+ const registry2 = create();
33
+ Object.defineProperty(globalThis, symbol, { value: registry2 });
34
+ return registry2;
27
35
  }
28
36
 
37
+ var registry = sharedRegistry(
38
+ "stimeo-ui.attribute-lease.registry.v1",
39
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
40
+ );
29
41
  function isAttributeValue(value) {
30
42
  return value === null || typeof value === "string";
31
43
  }
@@ -42,24 +54,46 @@ function readLeaseRecord(element, record) {
42
54
  return void 0;
43
55
  }
44
56
  }
45
- var AttributeLease = class _AttributeLease {
46
- static #registry = sharedRegistry(
47
- "stimeo-ui.attribute-lease.registry.v1",
48
- () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
49
- );
57
+ function attributeStack(element, attribute) {
58
+ const byAttribute = registry.stacks.get(element) ?? /* @__PURE__ */ new Map();
59
+ registry.stacks.set(element, byAttribute);
60
+ const known = byAttribute.get(attribute);
61
+ if (known) return known;
62
+ const found = [];
63
+ for (const record of registry.names.get(attribute) ?? []) {
64
+ const read = readLeaseRecord(element, record);
65
+ if (read) found.push({ ...read, record });
66
+ }
67
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
68
+ const stack = found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
69
+ lease: null,
70
+ record,
71
+ written,
72
+ original,
73
+ linked: linked && index > 0
74
+ }));
75
+ byAttribute.set(attribute, stack);
76
+ return stack;
77
+ }
78
+ function findHold(stack, lease, record) {
79
+ return stack.find((held) => held.lease === lease) ?? stack.find((held) => held.lease === null && held.record === record);
80
+ }
81
+ var AttributeLease = class {
50
82
  #attribute;
51
83
  #record;
52
84
  #base;
85
+ #preserveAuthor;
53
86
  #held = /* @__PURE__ */ new Set();
54
87
  constructor(attribute, owner, options = {}) {
55
88
  this.#attribute = attribute;
56
89
  this.#record = `data-${owner}-${attribute}-lease`;
57
90
  this.#base = options.base === true;
58
- const names = _AttributeLease.#registry.names;
91
+ this.#preserveAuthor = options.preserveAuthor === true;
92
+ const names = registry.names;
59
93
  names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
60
94
  }
61
95
  write(element, value) {
62
- const stack = this.#stack(element);
96
+ const stack = attributeStack(element, this.#attribute);
63
97
  const hold = this.#take(stack) ?? this.#acquire(element, stack);
64
98
  this.#held.add(element);
65
99
  const shows = !this.#covered(element, stack, hold);
@@ -69,8 +103,8 @@ var AttributeLease = class _AttributeLease {
69
103
  }
70
104
  return(element) {
71
105
  const held = this.#held.delete(element);
72
- const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
73
- const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
106
+ const known = registry.stacks.get(element)?.has(this.#attribute);
107
+ const stack = held || known || element.hasAttribute(this.#record) ? attributeStack(element, this.#attribute) : [];
74
108
  const hold = this.#take(stack);
75
109
  if (!hold) return false;
76
110
  const carries = element.getAttribute(this.#attribute) === hold.written;
@@ -88,31 +122,8 @@ var AttributeLease = class _AttributeLease {
88
122
  returnAll() {
89
123
  for (const element of Array.from(this.#held)) this.return(element);
90
124
  }
91
- #stack(element) {
92
- const stacks = _AttributeLease.#registry.stacks;
93
- const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
94
- stacks.set(element, byAttribute);
95
- const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
96
- byAttribute.set(this.#attribute, stack);
97
- return stack;
98
- }
99
- #seed(element) {
100
- const found = [];
101
- for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
102
- const read = readLeaseRecord(element, record);
103
- if (read) found.push({ ...read, record });
104
- }
105
- const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
106
- return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
107
- lease: null,
108
- record,
109
- written,
110
- original,
111
- linked: linked && index > 0
112
- }));
113
- }
114
125
  #take(stack) {
115
- const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
126
+ const hold = findHold(stack, this, this.#record);
116
127
  if (hold) hold.lease = this;
117
128
  return hold;
118
129
  }
@@ -121,7 +132,7 @@ var AttributeLease = class _AttributeLease {
121
132
  const top = stack.at(-1);
122
133
  const bottom = stack[0];
123
134
  const over = top?.written === current ? top : void 0;
124
- const from = this.#base ? bottom : over;
135
+ const from = this.#base ? this.#preserveAuthor && !stack.some((held) => held.written === current) ? void 0 : bottom : over;
125
136
  const hold = {
126
137
  lease: this,
127
138
  record: this.#record,
@@ -151,15 +162,11 @@ var AttributeLease = class _AttributeLease {
151
162
  );
152
163
  });
153
164
  for (const [name, recorded] of records) {
154
- if (element.getAttribute(name) === recorded) continue;
155
- if (recorded === null) element.removeAttribute(name);
156
- else element.setAttribute(name, recorded);
165
+ setAttributeIfChanged(element, name, recorded);
157
166
  }
158
167
  }
159
168
  #reflect(element, value) {
160
- if (element.getAttribute(this.#attribute) === value) return;
161
- if (value === null) element.removeAttribute(this.#attribute);
162
- else element.setAttribute(this.#attribute, value);
169
+ setAttributeIfChanged(element, this.#attribute, value);
163
170
  }
164
171
  };
165
172
 
@@ -190,6 +197,9 @@ var MicrotaskCoalescer = class {
190
197
  this.#queued = false;
191
198
  this.#generation += 1;
192
199
  }
200
+ get pending() {
201
+ return this.#queued;
202
+ }
193
203
  schedule() {
194
204
  if (!this.#active || this.#queued) return;
195
205
  this.#queued = true;
@@ -220,6 +230,9 @@ var MorphRenderWatcher = class {
220
230
  schedule() {
221
231
  this.#pass.schedule();
222
232
  }
233
+ get pending() {
234
+ return this.#pass.pending;
235
+ }
223
236
  disconnect() {
224
237
  this.#listeners.dispose();
225
238
  this.#pass.cancel();
@@ -298,14 +311,15 @@ var BulkSelectController = class _BulkSelectController extends Controller {
298
311
  this.#reportChange(this.#recompute());
299
312
  }
300
313
  };
314
+ #listeners = new ListenerSet();
301
315
  connect() {
302
316
  this.#allPagesMode = this.element.dataset.allPages === "true";
303
317
  this.#reconcile.observe(this.element);
304
- this.element.addEventListener("change", this.#onChange);
318
+ this.#listeners.add(this.element, "change", this.#onChange);
305
319
  this.#recompute();
306
320
  }
307
321
  disconnect() {
308
- this.element.removeEventListener("change", this.#onChange);
322
+ this.#listeners.dispose();
309
323
  this.#reconcile.disconnect();
310
324
  }
311
325
  itemTargetConnected() {
@@ -31,10 +31,17 @@ function logicalArrowKey(key, element) {
31
31
  if (!isRtl(element)) return key;
32
32
  return key === "ArrowRight" ? "ArrowLeft" : "ArrowRight";
33
33
  }
34
+ function gridDirectionElement(grid, root) {
35
+ const table = grid.closest("table");
36
+ return table !== null && root.contains(table) ? table : grid;
37
+ }
34
38
  function isReservedArrowChord(event, allow = []) {
35
39
  if (!event.key.startsWith("Arrow")) return false;
36
40
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
37
41
  }
42
+ function hasModifierChord(event) {
43
+ return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
44
+ }
38
45
 
39
46
  var formatters = /* @__PURE__ */ new WeakMap();
40
47
  function intlFormatter(factory, locale, options, fallbackLocale) {
@@ -96,6 +103,30 @@ function parseISOMonthString(monthStr) {
96
103
  function toISOMonthString(date) {
97
104
  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
98
105
  }
106
+ function monthGridDays(year, month, weekStart) {
107
+ const first = new Date(year, month - 1, 1);
108
+ const offset = (first.getDay() - weekStart + 7) % 7;
109
+ const current = new Date(first);
110
+ current.setDate(first.getDate() - offset);
111
+ const days = [];
112
+ for (let i = 0; i < 42; i++) {
113
+ days.push(new Date(current));
114
+ current.setDate(current.getDate() + 1);
115
+ }
116
+ return days;
117
+ }
118
+ function dateInMonthClamped(date, year, month) {
119
+ const target = new Date(year, month - 1, 1);
120
+ const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
121
+ target.setDate(Math.min(date.getDate(), lastDay));
122
+ return target;
123
+ }
124
+ function shiftMonthClamped(date, delta) {
125
+ return dateInMonthClamped(date, date.getFullYear(), date.getMonth() + 1 + delta);
126
+ }
127
+ function shiftYearClamped(date, delta) {
128
+ return dateInMonthClamped(date, date.getFullYear() + delta, date.getMonth() + 1);
129
+ }
99
130
  var MONTH_LABEL_OPTIONS = { month: "long", year: "numeric" };
100
131
  function monthLabelFormatter(locale) {
101
132
  return intlFormatter(Intl.DateTimeFormat, locale, MONTH_LABEL_OPTIONS, "en");
@@ -130,6 +161,9 @@ var MicrotaskCoalescer = class {
130
161
  this.#queued = false;
131
162
  this.#generation += 1;
132
163
  }
164
+ get pending() {
165
+ return this.#queued;
166
+ }
133
167
  schedule() {
134
168
  if (!this.#active || this.#queued) return;
135
169
  this.#queued = true;
@@ -171,6 +205,9 @@ var MorphRenderWatcher = class {
171
205
  schedule() {
172
206
  this.#pass.schedule();
173
207
  }
208
+ get pending() {
209
+ return this.#pass.pending;
210
+ }
174
211
  disconnect() {
175
212
  this.#listeners.dispose();
176
213
  this.#pass.cancel();
@@ -253,7 +290,7 @@ var CalendarController = class _CalendarController extends Controller {
253
290
  #monthMoves = 0;
254
291
  #paintedDates = /* @__PURE__ */ new WeakMap();
255
292
  #opened = false;
256
- #movedByUser = false;
293
+ #ownMove = false;
257
294
  #repaint = new MorphRenderWatcher(() => {
258
295
  this.#repaintKeepingFocus();
259
296
  });
@@ -319,6 +356,9 @@ var CalendarController = class _CalendarController extends Controller {
319
356
  onKeydown(event) {
320
357
  if (event.defaultPrevented) return;
321
358
  if (isReservedArrowChord(event)) return;
359
+ if ((event.key === "Home" || event.key === "End") && hasModifierChord(event)) return;
360
+ if ((event.key === "PageUp" || event.key === "PageDown") && (event.ctrlKey || event.altKey || event.metaKey))
361
+ return;
322
362
  const dayElement = event.target?.closest(
323
363
  targetSelector(this.identifier, "day")
324
364
  );
@@ -327,7 +367,8 @@ var CalendarController = class _CalendarController extends Controller {
327
367
  if (!date) return;
328
368
  let handled = true;
329
369
  let nextDate = new Date(date);
330
- switch (logicalArrowKey(event.key, this.element)) {
370
+ const directionElement = this.hasGridTarget ? gridDirectionElement(this.gridTarget, this.element) : this.element;
371
+ switch (logicalArrowKey(event.key, directionElement)) {
331
372
  case "ArrowLeft":
332
373
  nextDate.setDate(nextDate.getDate() - 1);
333
374
  break;
@@ -342,16 +383,16 @@ var CalendarController = class _CalendarController extends Controller {
342
383
  break;
343
384
  case "PageUp":
344
385
  if (event.shiftKey) {
345
- nextDate = this.#calculateShiftedYearDate(date, -1);
386
+ nextDate = shiftYearClamped(date, -1);
346
387
  } else {
347
- nextDate = this.#calculateShiftedMonthDate(date, -1);
388
+ nextDate = shiftMonthClamped(date, -1);
348
389
  }
349
390
  break;
350
391
  case "PageDown":
351
392
  if (event.shiftKey) {
352
- nextDate = this.#calculateShiftedYearDate(date, 1);
393
+ nextDate = shiftYearClamped(date, 1);
353
394
  } else {
354
- nextDate = this.#calculateShiftedMonthDate(date, 1);
395
+ nextDate = shiftMonthClamped(date, 1);
355
396
  }
356
397
  break;
357
398
  case "Home":
@@ -400,7 +441,7 @@ var CalendarController = class _CalendarController extends Controller {
400
441
  const locale = resolveLocale(this.element, this.localeValue);
401
442
  this.labelTarget.textContent = monthLabelFormatter(locale).format(monthStart);
402
443
  }
403
- const days = this.#calculateGridDays(year, month);
444
+ const days = monthGridDays(year, month, this.#safeWeekStart);
404
445
  const dayElements = this.dayTargets;
405
446
  const consumerDisabled = /* @__PURE__ */ new Map();
406
447
  for (const el of dayElements) {
@@ -513,8 +554,8 @@ var CalendarController = class _CalendarController extends Controller {
513
554
  return true;
514
555
  }
515
556
  #mirrorFields(painted, selection, reconciled) {
516
- const byUser = this.#movedByUser;
517
- this.#movedByUser = false;
557
+ const ownMove = this.#ownMove;
558
+ this.#ownMove = false;
518
559
  const moved = [];
519
560
  if (this.hasFieldTarget && this.#writeField(this.fieldTarget, selection) && !reconciled) {
520
561
  moved.push([this.fieldTarget, selection, this.#fieldWrites.get(this.fieldTarget) ?? 0]);
@@ -526,7 +567,7 @@ var CalendarController = class _CalendarController extends Controller {
526
567
  this.#fieldWrites.get(this.monthFieldTarget) ?? 0
527
568
  ]);
528
569
  }
529
- return byUser ? moved : [];
570
+ return ownMove ? moved : [];
530
571
  }
531
572
  #selectDay(dayElement, reason) {
532
573
  if (dayElement.getAttribute("aria-disabled") === "true") return;
@@ -535,7 +576,7 @@ var CalendarController = class _CalendarController extends Controller {
535
576
  this.selectedValue = dateStr;
536
577
  const selected = parseISODateString(dateStr);
537
578
  if (selected) this.focusedDate = selected;
538
- this.#movedByUser = true;
579
+ this.#ownMove = true;
539
580
  const changed = dateStr !== this.#published;
540
581
  if (changed) this.#selectionMoves += 1;
541
582
  const move = this.#selectionMoves;
@@ -547,7 +588,7 @@ var CalendarController = class _CalendarController extends Controller {
547
588
  #focusAndNavigateToDate(date) {
548
589
  this.focusedDate = date;
549
590
  const targetMonthStr = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
550
- this.#movedByUser = true;
591
+ this.#ownMove = true;
551
592
  if (targetMonthStr !== this.#monthRequest) {
552
593
  this.monthValue = targetMonthStr;
553
594
  if (targetMonthStr !== this.#paintedMonth) return;
@@ -573,7 +614,7 @@ var CalendarController = class _CalendarController extends Controller {
573
614
  #shiftMonth(delta) {
574
615
  const monthInfo = parseISOMonthString(this.#monthRequest);
575
616
  if (!monthInfo) return;
576
- this.#movedByUser = true;
617
+ this.#ownMove = true;
577
618
  const nextMonthDate = new Date(monthInfo.year, monthInfo.month - 1 + delta, 1);
578
619
  this.monthValue = `${nextMonthDate.getFullYear()}-${String(nextMonthDate.getMonth() + 1).padStart(2, "0")}`;
579
620
  if (this.monthValue === this.#paintedMonth) {
@@ -581,28 +622,6 @@ var CalendarController = class _CalendarController extends Controller {
581
622
  this.#repaintKeepingFocus();
582
623
  }
583
624
  }
584
- #calculateShiftedMonthDate(baseDate, delta) {
585
- const targetDate = new Date(baseDate.getFullYear(), baseDate.getMonth() + delta, 1);
586
- const lastDayInTarget = new Date(
587
- targetDate.getFullYear(),
588
- targetDate.getMonth() + 1,
589
- 0
590
- ).getDate();
591
- const clampedDay = Math.min(baseDate.getDate(), lastDayInTarget);
592
- targetDate.setDate(clampedDay);
593
- return targetDate;
594
- }
595
- #calculateShiftedYearDate(baseDate, delta) {
596
- const targetDate = new Date(baseDate.getFullYear() + delta, baseDate.getMonth(), 1);
597
- const lastDayInTarget = new Date(
598
- targetDate.getFullYear(),
599
- targetDate.getMonth() + 1,
600
- 0
601
- ).getDate();
602
- const clampedDay = Math.min(baseDate.getDate(), lastDayInTarget);
603
- targetDate.setDate(clampedDay);
604
- return targetDate;
605
- }
606
625
  #getStartOfWeekDate(date) {
607
626
  const currentDay = date.getDay();
608
627
  const shift = (currentDay - this.#safeWeekStart + 7) % 7;
@@ -640,27 +659,10 @@ var CalendarController = class _CalendarController extends Controller {
640
659
  if (today.getFullYear() === monthInfo.year && today.getMonth() === monthInfo.month - 1) {
641
660
  this.focusedDate = today;
642
661
  } else {
643
- const targetDate = new Date(monthInfo.year, monthInfo.month - 1, 1);
644
- const lastDayInTarget = new Date(monthInfo.year, monthInfo.month, 0).getDate();
645
- const clampedDay = Math.min(this.focusedDate.getDate(), lastDayInTarget);
646
- targetDate.setDate(clampedDay);
647
- this.focusedDate = targetDate;
662
+ this.focusedDate = dateInMonthClamped(this.focusedDate, monthInfo.year, monthInfo.month);
648
663
  }
649
664
  }
650
665
  }
651
- #calculateGridDays(year, month) {
652
- const firstDay = new Date(year, month - 1, 1);
653
- const dayOfWeek = firstDay.getDay();
654
- const offset = (dayOfWeek - this.#safeWeekStart + 7) % 7;
655
- const days = [];
656
- const current = new Date(firstDay);
657
- current.setDate(firstDay.getDate() - offset);
658
- for (let i = 0; i < 42; i++) {
659
- days.push(new Date(current));
660
- current.setDate(current.getDate() + 1);
661
- }
662
- return days;
663
- }
664
666
  get #safeWeekStart() {
665
667
  return this.#numbers.read(
666
668
  this,