@wpmoo/ui 1.0.0-rc.8 → 1.0.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 (94) hide show
  1. package/ASSET_LICENSE.md +1 -1
  2. package/LICENSE +28 -0
  3. package/README.md +34 -6
  4. package/certification.json +3 -2
  5. package/dist/assets/css/moo-ui.css +634 -33
  6. package/dist/assets/css/moo-ui.min.css +10 -31
  7. package/dist/assets/css/moo.css +624 -30
  8. package/dist/assets/css/moo.min.css +10 -32
  9. package/dist/js/chart.js +23 -21
  10. package/dist/js/chart.min.js +5 -5
  11. package/dist/js/combobox.js +18 -65
  12. package/dist/js/context-menu.js +16 -68
  13. package/dist/js/datatable.js +39 -22
  14. package/dist/js/datepicker.js +4 -3
  15. package/dist/js/datepicker.min.js +4 -3
  16. package/dist/js/moo-ui.js +160 -15402
  17. package/dist/js/moo-ui.min.js +3 -23
  18. package/dist/js/sheet.js +46 -0
  19. package/dist/js/sidebar.js +5 -4
  20. package/dist/js/slider.js +12 -47
  21. package/dist/js/state.js +76 -0
  22. package/dist/release-manifest.json +6 -6
  23. package/package.json +5 -3
  24. package/scss/_components.scss +2 -2
  25. package/scss/_config.scss +15 -21
  26. package/scss/_settings.scss +0 -3
  27. package/scss/components/_accordion.scss +1 -12
  28. package/scss/components/_alert.scss +1 -12
  29. package/scss/components/_avatar.scss +1 -16
  30. package/scss/components/_badge.scss +1 -0
  31. package/scss/components/_breadcrumb.scss +1 -12
  32. package/scss/components/_button.scss +1 -29
  33. package/scss/components/_button_group.scss +1 -25
  34. package/scss/components/_card.scss +1 -29
  35. package/scss/components/_checkbox.scss +1 -38
  36. package/scss/components/_close_button.scss +1 -9
  37. package/scss/components/_collapsible.scss +1 -0
  38. package/scss/components/_combobox.scss +1 -6
  39. package/scss/components/_context-menu.scss +1 -11
  40. package/scss/components/_datatable.scss +46 -85
  41. package/scss/components/_datepicker.scss +1 -0
  42. package/scss/components/_dialog.scss +1 -14
  43. package/scss/components/_dropdown.scss +5 -51
  44. package/scss/components/_field.scss +6 -37
  45. package/scss/components/_input.scss +1 -5
  46. package/scss/components/_input_group.scss +1 -51
  47. package/scss/components/_kbd.scss +1 -0
  48. package/scss/components/_menubar.scss +1 -0
  49. package/scss/components/_navigation.scss +76 -6
  50. package/scss/components/_pagination.scss +1 -13
  51. package/scss/components/_popover.scss +1 -22
  52. package/scss/components/_progress.scss +1 -3
  53. package/scss/components/_radio_group.scss +105 -6
  54. package/scss/components/_search_trigger.scss +1 -2
  55. package/scss/components/_select.scss +1 -0
  56. package/scss/components/_separator.scss +1 -6
  57. package/scss/components/_sheet.scss +1 -17
  58. package/scss/components/_sidebar.scss +1 -2
  59. package/scss/components/_skeleton.scss +1 -8
  60. package/scss/components/_slider.scss +1 -2
  61. package/scss/components/_spinner.scss +1 -7
  62. package/scss/components/_switch.scss +1 -4
  63. package/scss/components/_table.scss +1 -2
  64. package/scss/components/_tabs.scss +1 -24
  65. package/scss/components/_textarea.scss +1 -0
  66. package/scss/components/_toast.scss +1 -4
  67. package/scss/components/_toggle_group.scss +1 -3
  68. package/scss/components/_tooltip.scss +1 -20
  69. package/scss/components/sidebar/_base.scss +0 -7
  70. package/scss/components/sidebar/_collapsed.scss +0 -6
  71. package/scss/components/sidebar/_identity.scss +0 -1
  72. package/scss/components/sidebar/_menus.scss +0 -17
  73. package/scss/foundations/_backdrop.scss +0 -26
  74. package/scss/foundations/_focus.scss +0 -56
  75. package/scss/foundations/_scope.scss +19 -0
  76. package/scss/layouts/_app.scss +5 -7
  77. package/scss/layouts/_page_grid.scss +27 -0
  78. package/scss/layouts/app/sidebar/_base.scss +0 -33
  79. package/scss/layouts/app/sidebar/_default.scss +2 -8
  80. package/scss/layouts/app/sidebar/_floating.scss +1 -9
  81. package/scss/layouts/app/sidebar/_inset.scss +5 -2
  82. package/scss/mixins/_banner.scss +9 -3
  83. package/scss/moo-core.scss +3 -6
  84. package/scss/moo-ui.scss +9 -5
  85. package/scss/settings/_bootstrap_overrides.scss +0 -8
  86. package/scss/settings/_component_variables.scss +4 -266
  87. package/scss/settings/_forms.scss +1 -38
  88. package/scss/settings/_palette.scss +0 -42
  89. package/scss/themes/_root.scss +4 -0
  90. package/scss/themes/_standalone.scss +1 -3
  91. package/scss/themes/_theme.scss +4 -0
  92. package/scss/utilities/_background_color.scss +18 -0
  93. package/scss/utilities/_border.scss +4 -0
  94. package/dist/js/theme-prepaint.js +0 -91
@@ -1,20 +1,18 @@
1
1
  /*!
2
- * Moo UI combobox.js v1.0.0-rc.8 (https://ui.wpmoo.org/)
3
- * Copyright 2026 WPMoo (https://wpmoo.org)
2
+ * Moo UI combobox.js v1.0.0 (https://wpmoo.org/)
3
+ * Copyright 2026 WPMoo Authors
4
4
  * Licensed under MIT (https://github.com/wpmoo-org/ui/blob/main/LICENSE)
5
5
  */
6
- const instances = new WeakMap();
7
- const normalize = (value) => value.trim().toLowerCase();
8
6
 
9
- export default class Combobox {
7
+ const instances = /* @__PURE__ */ new WeakMap();
8
+ const normalize = (value) => value.trim().toLowerCase();
9
+ class Combobox {
10
10
  static getInstance(element) {
11
11
  return element?.nodeType === 1 ? instances.get(element) || null : null;
12
12
  }
13
-
14
13
  static getOrCreateInstance(element, config = {}) {
15
14
  return Combobox.getInstance(element) || new Combobox(element, config);
16
15
  }
17
-
18
16
  constructor(element, config = {}) {
19
17
  if (element?.nodeType !== 1 || !element.matches(".combobox")) {
20
18
  throw new TypeError("Combobox requires a .combobox root element.");
@@ -23,7 +21,6 @@ export default class Combobox {
23
21
  if (existing) {
24
22
  return existing;
25
23
  }
26
-
27
24
  this._element = element;
28
25
  this._document = element.ownerDocument;
29
26
  this._window = this._document.defaultView;
@@ -34,7 +31,6 @@ export default class Combobox {
34
31
  if (!this._input || !this._menu || this._options.length === 0) {
35
32
  throw new TypeError("Combobox requires an input, menu, and at least one option.");
36
33
  }
37
-
38
34
  this._isMultiple = element.dataset.comboboxMultiple === "true";
39
35
  this._hidden = this._isMultiple ? null : element.querySelector('input[type="hidden"]');
40
36
  this._chipValue = element.querySelector(".combobox-value");
@@ -47,7 +43,6 @@ export default class Combobox {
47
43
  this._listeners = [];
48
44
  this._generated = [];
49
45
  this._startsOpen = this._menu.classList.contains("show");
50
-
51
46
  this._empty = this._menu.querySelector("[data-combobox-empty]");
52
47
  if (!this._empty) {
53
48
  this._empty = this._document.createElement("li");
@@ -58,7 +53,6 @@ export default class Combobox {
58
53
  this._menu.appendChild(this._empty);
59
54
  this._generated.push(this._empty);
60
55
  }
61
-
62
56
  this._liveRegion = element.querySelector("[data-combobox-live]");
63
57
  if (!this._liveRegion) {
64
58
  this._liveRegion = this._document.createElement("span");
@@ -68,12 +62,10 @@ export default class Combobox {
68
62
  element.appendChild(this._liveRegion);
69
63
  this._generated.push(this._liveRegion);
70
64
  }
71
-
72
65
  instances.set(element, this);
73
66
  this._bindEvents();
74
67
  this._toggleClear(
75
- this._input.dataset.comboboxSelected === "true" ||
76
- element.dataset.comboboxSelected === "true"
68
+ this._input.dataset.comboboxSelected === "true" || element.dataset.comboboxSelected === "true"
77
69
  );
78
70
  this._syncMultipleValue();
79
71
  this._filterOptions({ open: this._startsOpen, activate: this._startsOpen });
@@ -81,7 +73,6 @@ export default class Combobox {
81
73
  this._closeMenu(false);
82
74
  }
83
75
  }
84
-
85
76
  dispose() {
86
77
  this._listeners.forEach(({ target, type, handler, options }) => {
87
78
  target.removeEventListener(type, handler, options);
@@ -94,34 +85,29 @@ export default class Combobox {
94
85
  this._valueStore?.querySelectorAll("[data-combobox-generated]").forEach((item) => item.remove());
95
86
  instances.delete(this._element);
96
87
  }
97
-
98
88
  _listen(target, type, handler, options) {
99
89
  target?.addEventListener(type, handler, options);
100
90
  if (target) {
101
91
  this._listeners.push({ target, type, handler, options });
102
92
  }
103
93
  }
104
-
105
94
  _trigger(name, detail = {}) {
106
95
  return this._element.dispatchEvent(
107
96
  new this._window.CustomEvent(`${name}.moo.combobox`, {
108
97
  bubbles: true,
109
98
  cancelable: name === "show" || name === "hide",
110
- detail,
99
+ detail
111
100
  })
112
101
  );
113
102
  }
114
-
115
103
  _optionGroupIsHidden(option) {
116
104
  return option.closest("[data-combobox-group]")?.hidden || false;
117
105
  }
118
-
119
106
  _visibleOptions() {
120
107
  return this._options.filter(
121
108
  (option) => !option.hidden && !option.disabled && !this._optionGroupIsHidden(option)
122
109
  );
123
110
  }
124
-
125
111
  _syncGroupVisibility() {
126
112
  if (this._groups.length === 0) {
127
113
  return;
@@ -138,7 +124,6 @@ export default class Combobox {
138
124
  separator.hidden = visibleGroupCount < 2;
139
125
  });
140
126
  }
141
-
142
127
  _openMenu(emit = true) {
143
128
  if (this._menu.classList.contains("show")) {
144
129
  this._input.setAttribute("aria-expanded", "true");
@@ -153,7 +138,6 @@ export default class Combobox {
153
138
  this._trigger("shown");
154
139
  }
155
140
  }
156
-
157
141
  _closeMenu(emit = true) {
158
142
  const wasOpen = this._menu.classList.contains("show");
159
143
  if (wasOpen && emit && !this._trigger("hide")) {
@@ -170,15 +154,12 @@ export default class Combobox {
170
154
  this._trigger("hidden");
171
155
  }
172
156
  }
173
-
174
157
  _optionLabel(option) {
175
158
  return option?.querySelector(".combobox-option__label")?.textContent?.trim() || "";
176
159
  }
177
-
178
160
  _selectedOptions() {
179
161
  return this._options.filter((option) => option.getAttribute("aria-selected") === "true");
180
162
  }
181
-
182
163
  _toggleClear(selected) {
183
164
  const value = selected ? "true" : "false";
184
165
  this._element.dataset.comboboxSelected = value;
@@ -190,7 +171,6 @@ export default class Combobox {
190
171
  }
191
172
  }
192
173
  }
193
-
194
174
  _createChip(option) {
195
175
  const value = option.dataset.value || "";
196
176
  const label = this._optionLabel(option);
@@ -212,7 +192,6 @@ export default class Combobox {
212
192
  chip.appendChild(remove);
213
193
  return chip;
214
194
  }
215
-
216
195
  _syncMultipleValue() {
217
196
  if (!this._isMultiple) {
218
197
  return;
@@ -235,14 +214,12 @@ export default class Combobox {
235
214
  }
236
215
  this._toggleClear(selected.length > 0);
237
216
  }
238
-
239
217
  _emitChange() {
240
218
  const selected = this._selectedOptions();
241
219
  this._trigger("change", {
242
- values: selected.map((option) => option.dataset.value || ""),
220
+ values: selected.map((option) => option.dataset.value || "")
243
221
  });
244
222
  }
245
-
246
223
  _removeChip(value) {
247
224
  const option = this._options.find((candidate) => candidate.dataset.value === value);
248
225
  if (!option) {
@@ -252,7 +229,6 @@ export default class Combobox {
252
229
  this._syncMultipleValue();
253
230
  this._emitChange();
254
231
  }
255
-
256
232
  _clearSelection(emit = true) {
257
233
  if (this._hidden) {
258
234
  this._hidden.value = "";
@@ -267,7 +243,6 @@ export default class Combobox {
267
243
  this._emitChange();
268
244
  }
269
245
  }
270
-
271
246
  _clearStaleSelection() {
272
247
  if (this._isMultiple) {
273
248
  this._input.value = "";
@@ -280,13 +255,12 @@ export default class Combobox {
280
255
  }
281
256
  const selected = this._selectedOptions()[0] || null;
282
257
  const label = this._optionLabel(selected);
283
- if (!this._input.value || (label && normalize(this._input.value) === normalize(label))) {
258
+ if (!this._input.value || label && normalize(this._input.value) === normalize(label)) {
284
259
  return;
285
260
  }
286
261
  this._clearSelection();
287
262
  this._input.value = "";
288
263
  }
289
-
290
264
  _activeOption() {
291
265
  const activeId = this._input.getAttribute("aria-activedescendant");
292
266
  if (!activeId) {
@@ -294,28 +268,22 @@ export default class Combobox {
294
268
  }
295
269
  return this._visibleOptions().find((option) => option.id === activeId) || null;
296
270
  }
297
-
298
271
  _selectedOption() {
299
272
  if (this._isMultiple) {
300
273
  return null;
301
274
  }
302
- return (
303
- this._visibleOptions().find(
304
- (option) => option.getAttribute("aria-selected") === "true"
305
- ) || null
306
- );
275
+ return this._visibleOptions().find(
276
+ (option) => option.getAttribute("aria-selected") === "true"
277
+ ) || null;
307
278
  }
308
-
309
279
  _preferredActiveOption() {
310
280
  return this._selectedOption() || this._visibleOptions()[0] || null;
311
281
  }
312
-
313
282
  _activatePreferredOptionIfNeeded() {
314
283
  if (!this._activeOption()) {
315
284
  this._setActiveOption(this._preferredActiveOption());
316
285
  }
317
286
  }
318
-
319
287
  _setActiveOption(option, { scroll = true } = {}) {
320
288
  this._options.forEach((candidate) => {
321
289
  const active = candidate === option;
@@ -341,19 +309,12 @@ export default class Combobox {
341
309
  }
342
310
  }
343
311
  }
344
-
345
312
  _activateOptionFromPointer(option, event) {
346
- if (
347
- event.pointerType === "touch" ||
348
- option.disabled ||
349
- option.getAttribute("aria-disabled") === "true" ||
350
- option === this._activeOption()
351
- ) {
313
+ if (event.pointerType === "touch" || option.disabled || option.getAttribute("aria-disabled") === "true" || option === this._activeOption()) {
352
314
  return;
353
315
  }
354
316
  this._setActiveOption(option, { scroll: false });
355
317
  }
356
-
357
318
  _restoreSelectedActiveOption() {
358
319
  if (!this._menu.classList.contains("show")) {
359
320
  return;
@@ -363,7 +324,6 @@ export default class Combobox {
363
324
  this._setActiveOption(selected, { scroll: false });
364
325
  }
365
326
  }
366
-
367
327
  _chooseOption(option) {
368
328
  if (!option || option.disabled) {
369
329
  return;
@@ -391,7 +351,6 @@ export default class Combobox {
391
351
  this._setActiveOption(option);
392
352
  this._emitChange();
393
353
  }
394
-
395
354
  _filterOptions({ open = true, activate = true, resetActive = false } = {}) {
396
355
  if (open) {
397
356
  this._openMenu();
@@ -413,7 +372,6 @@ export default class Combobox {
413
372
  this._activatePreferredOptionIfNeeded();
414
373
  }
415
374
  }
416
-
417
375
  _bindEvents() {
418
376
  this._listen(this._input, "focus", () => {
419
377
  this._openMenu();
@@ -469,25 +427,17 @@ export default class Combobox {
469
427
  }
470
428
  });
471
429
  }
472
-
473
430
  _handleRootClick(event) {
474
431
  const target = event.target;
475
- const trigger = target instanceof this._window.Element
476
- ? target.closest("[data-combobox-chip-remove]")
477
- : null;
432
+ const trigger = target instanceof this._window.Element ? target.closest("[data-combobox-chip-remove]") : null;
478
433
  if (trigger) {
479
434
  event.preventDefault();
480
435
  this._removeChip(trigger.dataset.value || "");
481
436
  this._input.focus();
482
- } else if (
483
- this._isMultiple &&
484
- target instanceof this._window.Element &&
485
- target.closest(".combobox-chips")
486
- ) {
437
+ } else if (this._isMultiple && target instanceof this._window.Element && target.closest(".combobox-chips")) {
487
438
  this._input.focus();
488
439
  }
489
440
  }
490
-
491
441
  _handleKeydown(event) {
492
442
  const available = this._visibleOptions();
493
443
  const current = available.findIndex(
@@ -528,3 +478,6 @@ export default class Combobox {
528
478
  }
529
479
  }
530
480
  }
481
+ export {
482
+ Combobox as default
483
+ };
@@ -1,19 +1,17 @@
1
1
  /*!
2
- * Moo UI context-menu.js v1.0.0-rc.8 (https://ui.wpmoo.org/)
3
- * Copyright 2026 WPMoo (https://wpmoo.org)
2
+ * Moo UI context-menu.js v1.0.0 (https://wpmoo.org/)
3
+ * Copyright 2026 WPMoo Authors
4
4
  * Licensed under MIT (https://github.com/wpmoo-org/ui/blob/main/LICENSE)
5
5
  */
6
- const instances = new WeakMap();
7
6
 
7
+ const instances = /* @__PURE__ */ new WeakMap();
8
8
  function pointerRect(event) {
9
9
  const { clientX: x, clientY: y } = event;
10
10
  return { top: y, left: x, right: x, bottom: y, width: 0, height: 0, x, y };
11
11
  }
12
-
13
12
  function isDisabledTrigger(trigger) {
14
13
  return Boolean(trigger?.disabled || trigger?.getAttribute?.("aria-disabled") === "true");
15
14
  }
16
-
17
15
  function menuItemFromEvent(event) {
18
16
  const target = event.target;
19
17
  const item = target.closest?.(".dropdown-item");
@@ -22,16 +20,13 @@ function menuItemFromEvent(event) {
22
20
  }
23
21
  return target.closest?.("li")?.querySelector(":scope > .dropdown-item") || null;
24
22
  }
25
-
26
- export default class ContextMenu {
23
+ class ContextMenu {
27
24
  static getInstance(element) {
28
25
  return element?.nodeType === 1 ? instances.get(element) || null : null;
29
26
  }
30
-
31
27
  static getOrCreateInstance(element, config = {}) {
32
28
  return ContextMenu.getInstance(element) || new ContextMenu(element, config);
33
29
  }
34
-
35
30
  constructor(element, config = {}) {
36
31
  if (element?.nodeType !== 1 || !element.matches(".context-menu")) {
37
32
  throw new TypeError("ContextMenu requires a .context-menu root element.");
@@ -40,7 +35,6 @@ export default class ContextMenu {
40
35
  if (existing) {
41
36
  return existing;
42
37
  }
43
-
44
38
  this._element = element;
45
39
  this._document = element.ownerDocument;
46
40
  this._window = this._document.defaultView;
@@ -52,12 +46,10 @@ export default class ContextMenu {
52
46
  if (!this._fallback) {
53
47
  throw new TypeError("ContextMenu requires an explicit fallback trigger.");
54
48
  }
55
-
56
49
  const Dropdown = this._bootstrap("Dropdown");
57
50
  if (!Dropdown) {
58
51
  throw new TypeError("ContextMenu requires bootstrap.Dropdown (bootstrap.bundle.js).");
59
52
  }
60
-
61
53
  this._config = { ...config };
62
54
  this._listeners = [];
63
55
  this._invoker = null;
@@ -65,17 +57,15 @@ export default class ContextMenu {
65
57
  this._openMethod = null;
66
58
  this._reference = {
67
59
  getBoundingClientRect: () => this._currentRect(),
68
- contextElement: this._fallback,
60
+ contextElement: this._fallback
69
61
  };
70
62
  this._dropdown = new Dropdown(this._fallback, {
71
63
  reference: this._reference,
72
- autoClose: "outside",
64
+ autoClose: "outside"
73
65
  });
74
-
75
66
  instances.set(element, this);
76
67
  this._bindEvents();
77
68
  }
78
-
79
69
  dispose() {
80
70
  this._listeners.forEach(({ target, type, handler, options }) => {
81
71
  target.removeEventListener(type, handler, options);
@@ -87,56 +77,46 @@ export default class ContextMenu {
87
77
  this._pointerRect = null;
88
78
  instances.delete(this._element);
89
79
  }
90
-
91
80
  show(anchor = this._fallback) {
92
81
  this._open(anchor, null);
93
82
  }
94
-
95
83
  hide() {
96
84
  this._dropdown.hide();
97
85
  }
98
-
99
86
  _listen(target, type, handler, options) {
100
87
  target.addEventListener(type, handler, options);
101
88
  this._listeners.push({ target, type, handler, options });
102
89
  }
103
-
104
90
  _bootstrap(name) {
105
91
  return this._window.bootstrap?.[name] || null;
106
92
  }
107
-
108
93
  _currentRect() {
109
94
  if (this._pointerRect) {
110
95
  return this._pointerRect;
111
96
  }
112
97
  return (this._invoker || this._fallback).getBoundingClientRect();
113
98
  }
114
-
115
99
  _trigger(name, cancelable = false) {
116
100
  return this._element.dispatchEvent(
117
101
  new this._window.CustomEvent(`${name}.moo.context-menu`, {
118
102
  bubbles: true,
119
- cancelable,
103
+ cancelable
120
104
  })
121
105
  );
122
106
  }
123
-
124
107
  _triggers() {
125
108
  return Array.from(this._element.querySelectorAll(":scope > .context-menu-trigger"));
126
109
  }
127
-
128
110
  _syncExpanded(expanded) {
129
111
  this._triggers().forEach((trigger) => {
130
112
  trigger.setAttribute("aria-expanded", String(expanded));
131
113
  });
132
114
  }
133
-
134
115
  _menuItems() {
135
116
  return Array.from(this._menu.querySelectorAll(".dropdown-item")).filter(
136
117
  (item) => !isDisabledTrigger(item) && !item.classList.contains("disabled")
137
118
  );
138
119
  }
139
-
140
120
  _open(trigger, pointerEvent, openMethod) {
141
121
  if (isDisabledTrigger(trigger)) {
142
122
  return;
@@ -149,7 +129,6 @@ export default class ContextMenu {
149
129
  this._openMethod = openMethod;
150
130
  this._dropdown.show();
151
131
  }
152
-
153
132
  _handleContextMenu(event) {
154
133
  const trigger = event.target.closest(".context-menu-trigger");
155
134
  if (!trigger || !this._element.contains(trigger)) {
@@ -158,7 +137,6 @@ export default class ContextMenu {
158
137
  event.preventDefault();
159
138
  this._open(trigger, event, "pointer");
160
139
  }
161
-
162
140
  _handleKeydown(event) {
163
141
  const trigger = event.target.closest(".context-menu-trigger");
164
142
  if (!trigger || !this._element.contains(trigger)) {
@@ -172,13 +150,11 @@ export default class ContextMenu {
172
150
  event.preventDefault();
173
151
  this._open(trigger, null, "keyboard");
174
152
  }
175
-
176
153
  _handleFallbackClick() {
177
154
  this._invoker = this._fallback;
178
155
  this._pointerRect = null;
179
156
  this._openMethod = "pointer";
180
157
  }
181
-
182
158
  _handleFallbackKeydown(event) {
183
159
  if (event.key !== " " && event.key !== "Spacebar") {
184
160
  return;
@@ -191,38 +167,28 @@ export default class ContextMenu {
191
167
  }
192
168
  this._open(this._fallback, null, "fallback-keyboard");
193
169
  }
194
-
195
170
  _handleMenuClick(event) {
196
171
  const item = menuItemFromEvent(event);
197
172
  if (!item) {
198
173
  return;
199
174
  }
200
175
  const persistentItem = item.matches(".dropdown-item-check, [data-context-menu-persistent]");
201
- const disabledItem =
202
- item.matches('.dropdown-item.disabled, .dropdown-item[aria-disabled="true"], .dropdown-item:disabled') ||
203
- isDisabledTrigger(item);
176
+ const disabledItem = item.matches('.dropdown-item.disabled, .dropdown-item[aria-disabled="true"], .dropdown-item:disabled') || isDisabledTrigger(item);
204
177
  if (disabledItem) {
205
178
  event.preventDefault();
206
179
  event.stopPropagation();
207
180
  return;
208
181
  }
209
- // Radio-grouped toggle buttons reuse the dropdown_toggle_item() button
210
- // Bootstrap's own roving arrow-key focus already picks up (it only ever
211
- // matches real .dropdown-item elements); Moo adds only the missing
212
- // mutual-exclusivity Bootstrap's independent button-toggle plugin does
213
- // not provide.
214
182
  const radioButton = item.matches("[data-context-menu-radio-group]") ? item : null;
215
183
  if (radioButton) {
216
184
  event.preventDefault();
217
185
  event.stopPropagation();
218
186
  const group = radioButton.dataset.contextMenuRadioGroup;
219
- this._menu
220
- .querySelectorAll(`[data-context-menu-radio-group="${group}"]`)
221
- .forEach((button) => {
222
- const checked = button === radioButton;
223
- button.classList.toggle("active", checked);
224
- button.setAttribute("aria-pressed", String(checked));
225
- });
187
+ this._menu.querySelectorAll(`[data-context-menu-radio-group="${group}"]`).forEach((button) => {
188
+ const checked = button === radioButton;
189
+ button.classList.toggle("active", checked);
190
+ button.setAttribute("aria-pressed", String(checked));
191
+ });
226
192
  return;
227
193
  }
228
194
  if (persistentItem) {
@@ -230,7 +196,6 @@ export default class ContextMenu {
230
196
  }
231
197
  this._dropdown.hide();
232
198
  }
233
-
234
199
  _bindEvents() {
235
200
  this._listen(this._element, "contextmenu", (event) => this._handleContextMenu(event));
236
201
  this._listen(this._element, "keydown", (event) => this._handleKeydown(event));
@@ -244,12 +209,6 @@ export default class ContextMenu {
244
209
  });
245
210
  this._listen(this._fallback, "shown.bs.dropdown", () => {
246
211
  this._syncExpanded(true);
247
- // Bootstrap's own show() never focuses an item either -- only its
248
- // Up/Down-key handler does, since a pointer-driven open (right-click,
249
- // or a click on the fallback trigger) should let :hover carry the
250
- // highlight instead of a focus ring competing with it. A keyboard-
251
- // driven open (Shift+F10/ContextMenu key) still lands on the first
252
- // item, matching that same native arrow-key-open convention.
253
212
  if (this._openMethod === "fallback-keyboard") {
254
213
  this._fallback.focus();
255
214
  } else if (this._openMethod === "keyboard") {
@@ -269,22 +228,8 @@ export default class ContextMenu {
269
228
  const invoker = this._invoker;
270
229
  this._pointerRect = null;
271
230
  this._invoker = null;
272
- // A native Bootstrap click on the fallback trigger bypasses _open()
273
- // entirely (Bootstrap's own capture-phase click-to-toggle handler
274
- // calls show() directly), so _handleFallbackClick's bubble-phase
275
- // "click" listener always runs too late to set _openMethod before
276
- // shown.bs.dropdown reads it. Resetting it here, back to the neutral
277
- // pointer default, is what keeps that path correct rather than
278
- // inheriting whatever a prior keyboard-triggered open last set.
279
231
  this._openMethod = null;
280
232
  this._trigger("hidden");
281
- // Bootstrap's own dataApiKeydownHandler (js/src/dropdown.js) intercepts
282
- // Escape in the document capture phase and hardcodes
283
- // `getToggleButton.focus()` (our fallback trigger) after hide().
284
- // Nothing downstream of a capture-phase stopPropagation() can observe
285
- // that keydown to redirect focus, so correct it in a microtask, which
286
- // still runs before the next paint: if focus landed on the fallback
287
- // but a different trigger invoked this menu, return focus there.
288
233
  queueMicrotask(() => {
289
234
  if (invoker && invoker !== this._fallback && this._document.activeElement === this._fallback) {
290
235
  invoker.focus();
@@ -293,3 +238,6 @@ export default class ContextMenu {
293
238
  });
294
239
  }
295
240
  }
241
+ export {
242
+ ContextMenu as default
243
+ };
@@ -1,14 +1,15 @@
1
1
  /*!
2
- * Moo UI datatable.js v1.0.0-rc.8 (https://ui.wpmoo.org/)
3
- * Copyright 2026 WPMoo (https://wpmoo.org)
2
+ * Moo UI datatable.js v1.0.0 (https://wpmoo.org/)
3
+ * Copyright 2026 WPMoo Authors
4
4
  * Licensed under MIT (https://github.com/wpmoo-org/ui/blob/main/LICENSE)
5
5
  */
6
+
6
7
  var __defProp = Object.defineProperty;
7
8
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
8
9
 
9
10
  // src/js/theme-owner.js
10
11
  var OWNER_SELECTOR = '.moo-ui[data-bs-theme="light"], .moo-ui[data-bs-theme="dark"]';
11
- var PORTAL_SELECTOR = "[data-moo-overlay-portal-host]";
12
+ var PORTAL_SELECTOR = "[data-moo-overlay-host]";
12
13
  function documentFor(node) {
13
14
  if (node?.nodeType === 9) return node;
14
15
  if (node?.ownerDocument) return node.ownerDocument;
@@ -137,12 +138,12 @@ var _DataTable = class _DataTable {
137
138
  // That keeps the menu out of the scroll wrapper's layout math, so Safari
138
139
  // cannot clip it at the rounded frame edge or shift the visible table
139
140
  // slice while Popper positions it against the viewport.
140
- // Table and card views both wrap row actions in .table-row-actions, but
141
- // .table-responsive only contains the table view; card triggers must be
142
- // selected separately so both views get the same fixed Popper config.
141
+ // Public Dropdown compositions need the same behavior as the built-in
142
+ // row actions. Scope by row/card ownership so toolbar and sort menus keep
143
+ // their own lifecycle.
143
144
  _rowActionTriggers() {
144
145
  return this._element.querySelectorAll(
145
- '.table-responsive .table-row-actions [data-bs-toggle="dropdown"], [data-datatable-card] .table-row-actions [data-bs-toggle="dropdown"]'
146
+ 'tr[data-datatable-row] .dropdown [data-bs-toggle="dropdown"], [data-datatable-card] .dropdown [data-bs-toggle="dropdown"]'
146
147
  );
147
148
  }
148
149
  _initRowActionDropdowns() {
@@ -226,7 +227,7 @@ var _DataTable = class _DataTable {
226
227
  });
227
228
  }
228
229
  _rowActionMenuForTrigger(trigger) {
229
- if (!trigger?.closest?.(".table-row-actions")) {
230
+ if (!trigger?.closest?.("tr[data-datatable-row], [data-datatable-card]")) {
230
231
  return null;
231
232
  }
232
233
  return trigger.closest(".dropdown")?.querySelector(":scope > .dropdown-menu") || null;
@@ -391,38 +392,54 @@ var _DataTable = class _DataTable {
391
392
  active.blur();
392
393
  }
393
394
  }
394
- // responsive_mode="toggle" renders both the table and the card list and
395
- // lets the reader pick between them (Odoo's List/Kanban switcher, not a
396
- // developer-chosen breakpoint), so the choice belongs to the reader across
397
- // visits, not just the current render.
395
+ // Auto mode remembers independent narrow/wide choices; toggle mode keeps
396
+ // its existing viewport-independent preference.
398
397
  _initViewToggle() {
399
398
  const toggles = Array.from(this._element.querySelectorAll(".datatable-view-toggle"));
400
399
  if (!toggles.length) {
401
400
  return;
402
401
  }
403
402
  const inputs = toggles.flatMap((toggle) => Array.from(toggle.querySelectorAll("input")));
404
- const storageKey = `moo-datatable-view:${this._element.id}`;
403
+ const breakpoint = this._window.getComputedStyle(this._element).getPropertyValue("--moo-datatable-responsive-breakpoint").trim();
404
+ const auto = this._element.hasAttribute("data-datatable-responsive-breakpoint");
405
+ const media = auto && breakpoint ? this._window.matchMedia(`(min-width: ${breakpoint})`) : null;
406
+ const preferences = /* @__PURE__ */ new Map();
407
+ const storageKey = /* @__PURE__ */ __name(() => `moo-datatable-view:${this._element.id}${media ? media.matches ? ":wide" : ":narrow" : ""}`, "storageKey");
405
408
  const setView = /* @__PURE__ */ __name((value, { persist = false } = {}) => {
409
+ if (this._element.dataset.datatableView !== value) {
410
+ const Dropdown = this._bootstrap("Dropdown");
411
+ [...this._rowActionTriggers(), ...this._sortTriggers()].forEach((trigger) => {
412
+ Dropdown?.getInstance(trigger)?.hide();
413
+ });
414
+ }
406
415
  this._element.dataset.datatableView = value;
407
416
  inputs.forEach((input) => {
408
417
  input.checked = input.value === value;
409
418
  });
410
419
  if (persist) {
420
+ preferences.set(storageKey(), value);
411
421
  try {
412
- this._window.localStorage.setItem(storageKey, value);
422
+ this._window.localStorage.setItem(storageKey(), value);
413
423
  } catch {
414
424
  }
415
425
  }
416
426
  }, "setView");
417
- let stored = null;
418
- try {
419
- stored = this._window.localStorage.getItem(storageKey);
420
- } catch {
421
- stored = null;
422
- }
423
- if (stored === "table" || stored === "cards") {
424
- setView(stored);
427
+ const restoreView = /* @__PURE__ */ __name(() => {
428
+ let stored = preferences.get(storageKey());
429
+ try {
430
+ stored ?? (stored = this._window.localStorage.getItem(storageKey()));
431
+ } catch {
432
+ }
433
+ setView(stored === "table" || stored === "cards" ? stored : media ? media.matches ? "table" : "cards" : this._element.dataset.datatableView);
434
+ }, "restoreView");
435
+ if (!auto || media) {
436
+ restoreView();
437
+ } else {
438
+ inputs.forEach((input) => {
439
+ input.checked = false;
440
+ });
425
441
  }
442
+ if (media) this._listen(media, "change", restoreView);
426
443
  toggles.forEach((toggle) => {
427
444
  this._listen(toggle, "change", (event) => {
428
445
  const value = event.target.value;