@zeyos/zx 2.2.1 → 3.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 (117) hide show
  1. package/CHANGELOG.md +197 -0
  2. package/DESIGN-SYSTEM.md +136 -0
  3. package/MIGRATION.md +155 -4
  4. package/README.md +51 -14
  5. package/dist/types/compat/map/data.d.ts +3 -2
  6. package/dist/types/components/account-menu/account-menu.d.ts +144 -0
  7. package/dist/types/components/app-rail/app-rail.d.ts +250 -0
  8. package/dist/types/components/app-sidebar/app-sidebar.d.ts +169 -0
  9. package/dist/types/components/avatar/avatar.d.ts +100 -0
  10. package/dist/types/components/chart/chart.d.ts +321 -0
  11. package/dist/types/components/date-picker/month-picker.d.ts +6 -0
  12. package/dist/types/components/dialog/dialog.d.ts +9 -0
  13. package/dist/types/components/dock/dock.d.ts +495 -0
  14. package/dist/types/components/dropdown/dropdown.d.ts +2 -2
  15. package/dist/types/components/field/field.d.ts +14 -0
  16. package/dist/types/components/field-upload/field-upload.d.ts +37 -4
  17. package/dist/types/components/form/form.d.ts +25 -3
  18. package/dist/types/components/grid/grid.d.ts +172 -0
  19. package/dist/types/components/launcher/launcher.d.ts +300 -0
  20. package/dist/types/components/layout/layout.d.ts +4 -4
  21. package/dist/types/components/menu-button/menu-button.d.ts +10 -2
  22. package/dist/types/components/modal/modal.d.ts +25 -0
  23. package/dist/types/components/multi-value-editor/multi-value-editor.d.ts +74 -9
  24. package/dist/types/components/questionnaire/items.d.ts +327 -0
  25. package/dist/types/components/questionnaire/questionnaire.d.ts +449 -0
  26. package/dist/types/components/select/select.d.ts +36 -1
  27. package/dist/types/components/sheet/sheet-stack.d.ts +167 -0
  28. package/dist/types/components/sheet/sheet.d.ts +294 -0
  29. package/dist/types/components/split-view/split-view.d.ts +2 -21
  30. package/dist/types/components/tabbox/tabbox.d.ts +12 -3
  31. package/dist/types/components/table/table.d.ts +219 -12
  32. package/dist/types/components/tooltip/tooltip.d.ts +2 -2
  33. package/dist/types/components/value-list/value-list.d.ts +24 -3
  34. package/dist/types/core/dom.d.ts +114 -0
  35. package/dist/types/core/drag-axis.d.ts +111 -0
  36. package/dist/types/core/position.d.ts +4 -2
  37. package/dist/types/index.d.ts +12 -2
  38. package/dist/zx-compat.esm.js +943 -311
  39. package/dist/zx-compat.esm.min.js +1 -1
  40. package/dist/zx-compat.global.js +7893 -2252
  41. package/dist/zx-compat.global.min.js +3 -3
  42. package/dist/zx-zeyos.esm.js +826 -114
  43. package/dist/zx-zeyos.esm.min.js +1 -1
  44. package/dist/zx.css +1720 -243
  45. package/dist/zx.esm.js +8227 -2599
  46. package/dist/zx.esm.min.js +3 -3
  47. package/dist/zx.global.js +8227 -2599
  48. package/dist/zx.global.min.js +3 -3
  49. package/dist/zx.min.css +1 -1
  50. package/docs/api.json +2597 -255
  51. package/docs/llms.md +475 -37
  52. package/docs/llms.txt +21 -9
  53. package/package.json +4 -2
  54. package/src/compat/globals.js +3 -1
  55. package/src/compat/map/data.js +31 -9
  56. package/src/components/account-menu/account-menu.css +70 -0
  57. package/src/components/account-menu/account-menu.js +208 -0
  58. package/src/components/app-rail/app-rail.css +107 -0
  59. package/src/components/app-rail/app-rail.js +668 -0
  60. package/src/components/app-sidebar/app-sidebar.css +68 -0
  61. package/src/components/app-sidebar/app-sidebar.js +388 -0
  62. package/src/components/avatar/avatar.css +61 -0
  63. package/src/components/avatar/avatar.js +222 -0
  64. package/src/components/button/button.css +0 -1
  65. package/src/components/chart/chart.css +61 -0
  66. package/src/components/chart/chart.js +518 -0
  67. package/src/components/date-picker/date-range-picker.css +7 -0
  68. package/src/components/date-picker/month-picker.js +9 -0
  69. package/src/components/dialog/dialog.js +11 -4
  70. package/src/components/dock/dock.css +228 -0
  71. package/src/components/dock/dock.js +977 -0
  72. package/src/components/dropdown/dropdown.js +1 -1
  73. package/src/components/field/field.js +2 -0
  74. package/src/components/field-upload/field-upload.js +9 -4
  75. package/src/components/fieldset/fieldset.css +7 -0
  76. package/src/components/form/form.css +7 -0
  77. package/src/components/form/form.js +6 -3
  78. package/src/components/grid/grid.css +6 -0
  79. package/src/components/grid/grid.js +213 -0
  80. package/src/components/launcher/launcher.css +100 -0
  81. package/src/components/launcher/launcher.js +557 -0
  82. package/src/components/layout/layout.js +2 -2
  83. package/src/components/menu-button/menu-button.js +27 -2
  84. package/src/components/modal/modal.css +5 -1
  85. package/src/components/modal/modal.js +37 -2
  86. package/src/components/multi-value-editor/multi-value-editor.css +99 -15
  87. package/src/components/multi-value-editor/multi-value-editor.js +237 -19
  88. package/src/components/navigation-bar/navigation-bar.css +7 -0
  89. package/src/components/questionnaire/items.js +370 -0
  90. package/src/components/questionnaire/questionnaire.css +387 -0
  91. package/src/components/questionnaire/questionnaire.js +1033 -0
  92. package/src/components/search/search.css +10 -2
  93. package/src/components/select/select.css +6 -1
  94. package/src/components/select/select.js +99 -11
  95. package/src/components/sheet/sheet-stack.js +304 -0
  96. package/src/components/sheet/sheet.css +351 -0
  97. package/src/components/sheet/sheet.js +551 -0
  98. package/src/components/split-view/split-view.js +76 -161
  99. package/src/components/stepper/stepper.css +12 -2
  100. package/src/components/tabbox/tabbox.css +13 -3
  101. package/src/components/tabbox/tabbox.js +34 -10
  102. package/src/components/table/table.css +43 -0
  103. package/src/components/table/table.js +523 -45
  104. package/src/components/tooltip/tooltip.js +5 -2
  105. package/src/components/value-list/value-list.js +6 -3
  106. package/src/core/dom.js +302 -0
  107. package/src/core/drag-axis.js +203 -0
  108. package/src/core/position.js +68 -22
  109. package/src/index.js +19 -2
  110. package/styles/tokens/dark.css +15 -13
  111. package/styles/tokens/global.css +17 -5
  112. package/styles/tokens/semantic.css +14 -8
  113. package/styles/tokens/themes.css +72 -0
  114. package/styles/zx.css +11 -1
  115. package/dist/types/components/permission/permission.d.ts +0 -93
  116. package/src/components/permission/permission.css +0 -47
  117. package/src/components/permission/permission.js +0 -198
@@ -347,7 +347,7 @@ function parse(source) {
347
347
  }
348
348
  function installGlobals(host = globalThis) {
349
349
  installElementStorage(host);
350
- host.__ = parse;
350
+ if (host.__ === void 0) host.__ = parse;
351
351
  const stringPrototype = host.String?.prototype;
352
352
  if (stringPrototype && typeof stringPrototype.htmlSpecialChars !== "function") {
353
353
  Object.defineProperty(stringPrototype, "htmlSpecialChars", {
@@ -2838,6 +2838,14 @@ var MonthPicker = class extends DatePicker {
2838
2838
  static cssName = "month-picker";
2839
2839
  /** @type {MonthPickerOptions} */
2840
2840
  static defaults = { value: null, min: null, max: null };
2841
+ /**
2842
+ * Creates a month picker with its narrower month-based option contract.
2843
+ * @param {Element|string|null} target Existing element, selector, or null.
2844
+ * @param {MonthPickerOptions} [options={}] Month-picker options.
2845
+ */
2846
+ constructor(target, options = {}) {
2847
+ super(target, options);
2848
+ }
2841
2849
  /** @returns {HTMLElement} */
2842
2850
  render() {
2843
2851
  const root = (
@@ -3057,7 +3065,13 @@ var PLACEMENTS = /* @__PURE__ */ new Set([
3057
3065
  "top-start",
3058
3066
  "top-end",
3059
3067
  "bottom",
3060
- "top"
3068
+ "top",
3069
+ "right-start",
3070
+ "right-end",
3071
+ "left-start",
3072
+ "left-end",
3073
+ "right",
3074
+ "left"
3061
3075
  ]);
3062
3076
  function position(anchor, floating, options = {}) {
3063
3077
  if (!(anchor instanceof Element) || !(floating instanceof HTMLElement)) {
@@ -3085,13 +3099,15 @@ function position(anchor, floating, options = {}) {
3085
3099
  floating.style.inset = "auto";
3086
3100
  floating.style.margin = "0";
3087
3101
  floating.style.setProperty("position-anchor", anchorName);
3088
- floating.style.setProperty("position-area", positionArea(placement));
3089
- floating.style.setProperty("inset-area", positionArea(placement));
3102
+ const area = positionArea(placement, getComputedStyle(anchor).direction === "rtl");
3103
+ floating.style.setProperty("position-area", area);
3104
+ floating.style.setProperty("inset-area", area);
3090
3105
  floating.style.setProperty(
3091
3106
  "position-try-fallbacks",
3092
3107
  flip ? "flip-block, flip-inline, flip-block flip-inline" : "none"
3093
3108
  );
3094
- floating.style.setProperty(placement.startsWith("top") ? "margin-block-end" : "margin-block-start", `${offset}px`);
3109
+ const margin = placement.startsWith("top") ? "margin-bottom" : placement.startsWith("bottom") ? "margin-top" : placement.startsWith("left") ? "margin-right" : "margin-left";
3110
+ floating.style.setProperty(margin, `${offset}px`);
3095
3111
  update = () => {
3096
3112
  if (matchWidth) floating.style.minWidth = `${anchor.getBoundingClientRect().width}px`;
3097
3113
  };
@@ -3135,14 +3151,20 @@ function position(anchor, floating, options = {}) {
3135
3151
  }
3136
3152
  };
3137
3153
  }
3138
- function positionArea(placement) {
3154
+ function positionArea(placement, rtl) {
3139
3155
  return {
3140
- "bottom-start": "block-end span-inline-end",
3141
- "bottom-end": "block-end span-inline-start",
3142
- "top-start": "block-start span-inline-end",
3143
- "top-end": "block-start span-inline-start",
3144
- bottom: "block-end",
3145
- top: "block-start"
3156
+ "bottom-start": rtl ? "bottom span-left" : "bottom span-right",
3157
+ "bottom-end": rtl ? "bottom span-right" : "bottom span-left",
3158
+ "top-start": rtl ? "top span-left" : "top span-right",
3159
+ "top-end": rtl ? "top span-right" : "top span-left",
3160
+ "right-start": "right span-bottom",
3161
+ "right-end": "right span-top",
3162
+ "left-start": "left span-bottom",
3163
+ "left-end": "left span-top",
3164
+ bottom: "bottom",
3165
+ top: "top",
3166
+ right: "right",
3167
+ left: "left"
3146
3168
  }[placement];
3147
3169
  }
3148
3170
  function updateFallback(anchor, floating, { placement, offset, flip, matchWidth }) {
@@ -3152,26 +3174,54 @@ function updateFallback(anchor, floating, { placement, offset, flip, matchWidth
3152
3174
  const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
3153
3175
  const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
3154
3176
  let actualPlacement = placement;
3155
- const prefersBottom = placement.startsWith("bottom");
3177
+ const side = placementSide(placement);
3156
3178
  const bottomY = anchorRect.bottom + offset;
3157
3179
  const topY = anchorRect.top - offset - floatingRect.height;
3180
+ const rightX = anchorRect.right + offset;
3181
+ const leftX = anchorRect.left - offset - floatingRect.width;
3158
3182
  if (flip) {
3159
- if (prefersBottom && bottomY + floatingRect.height > viewportHeight && topY >= 0) {
3160
- actualPlacement = /** @type {typeof placement} */
3161
- placement.replace("bottom", "top");
3162
- } else if (!prefersBottom && topY < 0 && bottomY + floatingRect.height <= viewportHeight) {
3163
- actualPlacement = /** @type {typeof placement} */
3164
- placement.replace("top", "bottom");
3183
+ if (side === "bottom" && bottomY + floatingRect.height > viewportHeight && topY >= 0) {
3184
+ actualPlacement = flipPlacement(placement, "bottom", "top");
3185
+ } else if (side === "top" && topY < 0 && bottomY + floatingRect.height <= viewportHeight) {
3186
+ actualPlacement = flipPlacement(placement, "top", "bottom");
3187
+ } else if (side === "right" && rightX + floatingRect.width > viewportWidth && leftX >= 0) {
3188
+ actualPlacement = flipPlacement(placement, "right", "left");
3189
+ } else if (side === "left" && leftX < 0 && rightX + floatingRect.width <= viewportWidth) {
3190
+ actualPlacement = flipPlacement(placement, "left", "right");
3165
3191
  }
3166
3192
  }
3193
+ const actualSide = placementSide(actualPlacement);
3167
3194
  let left;
3168
- if (actualPlacement.endsWith("-end")) left = anchorRect.right - floatingRect.width;
3169
- else if (actualPlacement.endsWith("-start")) left = anchorRect.left;
3170
- else left = anchorRect.left + (anchorRect.width - floatingRect.width) / 2;
3171
- const top = actualPlacement.startsWith("bottom") ? bottomY : topY;
3195
+ let top;
3196
+ if (actualSide === "bottom" || actualSide === "top") {
3197
+ const rtl = getComputedStyle(anchor).direction === "rtl";
3198
+ if (actualPlacement.endsWith("-end")) {
3199
+ left = rtl ? anchorRect.left : anchorRect.right - floatingRect.width;
3200
+ } else if (actualPlacement.endsWith("-start")) {
3201
+ left = rtl ? anchorRect.right - floatingRect.width : anchorRect.left;
3202
+ } else left = anchorRect.left + (anchorRect.width - floatingRect.width) / 2;
3203
+ top = actualSide === "bottom" ? bottomY : topY;
3204
+ } else {
3205
+ left = actualSide === "right" ? rightX : leftX;
3206
+ if (actualPlacement.endsWith("-end")) top = anchorRect.bottom - floatingRect.height;
3207
+ else if (actualPlacement.endsWith("-start")) top = anchorRect.top;
3208
+ else top = anchorRect.top + (anchorRect.height - floatingRect.height) / 2;
3209
+ }
3172
3210
  floating.style.left = `${Math.round(clamp(left, 0, Math.max(0, viewportWidth - floatingRect.width)))}px`;
3173
3211
  floating.style.top = `${Math.round(clamp(top, 0, Math.max(0, viewportHeight - floatingRect.height)))}px`;
3174
3212
  }
3213
+ function placementSide(placement) {
3214
+ return (
3215
+ /** @type {'top'|'bottom'|'left'|'right'} */
3216
+ placement.split("-")[0]
3217
+ );
3218
+ }
3219
+ function flipPlacement(placement, from, to) {
3220
+ return (
3221
+ /** @type {Placement} */
3222
+ placement.replace(from, to)
3223
+ );
3224
+ }
3175
3225
  function showPopover(element) {
3176
3226
  if (typeof element.showPopover !== "function") return;
3177
3227
  try {
@@ -4004,31 +4054,31 @@ var PLACEHOLDER_TOKENS = {
4004
4054
  "%Y": "YYYY",
4005
4055
  "%y": "YY"
4006
4056
  };
4007
- function parseRangeText(text3, format, separator) {
4057
+ function parseRangeText(text3, format2, separator) {
4008
4058
  const source = String(text3 ?? "").trim();
4009
4059
  if (source === "") return { start: null, end: null };
4010
4060
  const raw2 = String(separator ?? " \u2013 ");
4011
4061
  const core = raw2.trim();
4012
- const strict = core !== "" && String(format).includes(core) && /^\s|\s$/.test(raw2);
4062
+ const strict = core !== "" && String(format2).includes(core) && /^\s|\s$/.test(raw2);
4013
4063
  const lead = strict ? "\\s+" : "\\s*";
4014
4064
  const tail = strict ? "(?:\\s+|$)" : "\\s*";
4015
4065
  const parts = core === "" ? [source] : source.split(new RegExp(`${lead}${escapeRegExp(core)}${tail}`));
4016
4066
  if (parts.length > 2) return null;
4017
- const first = parseDate(parts[0].trim(), format);
4067
+ const first = parseDate(parts[0].trim(), format2);
4018
4068
  if (!first) return null;
4019
4069
  if (parts.length === 1) return { start: first, end: new Date(first.getTime()) };
4020
4070
  if (parts[1].trim() === "") return { start: first, end: null };
4021
- const second = parseDate(parts[1].trim(), format);
4071
+ const second = parseDate(parts[1].trim(), format2);
4022
4072
  if (!second) return null;
4023
4073
  return normalizeRange(first, second);
4024
4074
  }
4025
- function formatRangeText(range, format, separator) {
4075
+ function formatRangeText(range, format2, separator) {
4026
4076
  const start = range?.start ?? null;
4027
4077
  const end = range?.end ?? null;
4028
4078
  if (!start && !end) return "";
4029
- if (!end) return formatDate(start, format);
4030
- if (!start) return formatDate(end, format);
4031
- return `${formatDate(start, format)}${separator}${formatDate(end, format)}`;
4079
+ if (!end) return formatDate(start, format2);
4080
+ if (!start) return formatDate(end, format2);
4081
+ return `${formatDate(start, format2)}${separator}${formatDate(end, format2)}`;
4032
4082
  }
4033
4083
  var DateRangeBox = class extends Component {
4034
4084
  static cssName = "date-range-box";
@@ -4686,12 +4736,12 @@ var DateTimeBox = function DateTimeBox2(target, options = {}) {
4686
4736
  function validDate4(value) {
4687
4737
  return value instanceof Date && !Number.isNaN(value.getTime()) ? new Date(value.getTime()) : null;
4688
4738
  }
4689
- function effectiveFormat(format, time) {
4690
- const value = String(format || "%d.%m.%Y");
4739
+ function effectiveFormat(format2, time) {
4740
+ const value = String(format2 || "%d.%m.%Y");
4691
4741
  return time && !/%[HM]/.test(value) ? `${value} %H:%M` : value;
4692
4742
  }
4693
- function placeholderFromFormat(format) {
4694
- return format.replace(/%[demYyHMS]/g, (token) => ({
4743
+ function placeholderFromFormat(format2) {
4744
+ return format2.replace(/%[demYyHMS]/g, (token) => ({
4695
4745
  "%d": "DD",
4696
4746
  "%e": "D",
4697
4747
  "%m": "MM",
@@ -5118,18 +5168,34 @@ function registerFieldUploadAdapter() {
5118
5168
  }
5119
5169
 
5120
5170
  // src/components/multi-value-editor/multi-value-editor.js
5171
+ var REORDER = Object.freeze({
5172
+ both: { drag: true, buttons: true },
5173
+ buttons: { drag: false, buttons: true },
5174
+ drag: { drag: true, buttons: false },
5175
+ none: { drag: false, buttons: false }
5176
+ });
5177
+ var PARTS = Object.freeze({
5178
+ value: ".zx-multi-value-editor__value",
5179
+ handle: ".zx-multi-value-editor__handle",
5180
+ up: '[data-action="up"]',
5181
+ down: '[data-action="down"]'
5182
+ });
5121
5183
  var MultiValueEditor = class extends Component {
5122
5184
  static cssName = "multi-value-editor";
5123
5185
  /** @type {MultiValueEditorOptions} */
5124
- static defaults = { values: [], options: null, addLabel: "Add value" };
5186
+ static defaults = { values: [], options: null, addLabel: "Add value", reorder: "both" };
5125
5187
  /** @returns {HTMLElement} */
5126
5188
  render() {
5127
5189
  const root = (
5128
5190
  /** @type {HTMLElement} */
5129
5191
  this.el ?? h("div")
5130
5192
  );
5193
+ this.el = root;
5131
5194
  this._values = Array.isArray(this.options.values) ? this.options.values.map(String) : [];
5132
5195
  this._ownedNodes = [];
5196
+ this._initialState = root.getAttribute("data-state");
5197
+ this._dragIndex = -1;
5198
+ this._armed = null;
5133
5199
  this._rows = h("div", { class: "zx-multi-value-editor__rows" });
5134
5200
  this._add = h("button", {
5135
5201
  class: "zx-btn zx-multi-value-editor__add",
@@ -5140,6 +5206,17 @@ var MultiValueEditor = class extends Component {
5140
5206
  root.append(this._rows, this._add);
5141
5207
  this.listen(root, "click", (event) => this._handleClick(event));
5142
5208
  this.listen(this._rows, "change", (event) => this._handleChange(event));
5209
+ this.listen(this._rows, "keydown", (event) => this._handleKeydown(event));
5210
+ if (this._reorder().drag) {
5211
+ this.listen(this._rows, "pointerdown", (event) => this._handlePointerDown(event));
5212
+ this.listen(this._rows, "dragstart", (event) => this._handleDragStart(event));
5213
+ this.listen(this._rows, "dragover", (event) => this._handleDragOver(event));
5214
+ this.listen(this._rows, "dragleave", (event) => this._handleDragLeave(event));
5215
+ this.listen(this._rows, "drop", (event) => this._handleDrop(event));
5216
+ this.listen(this._rows, "dragend", () => this._finishDrag());
5217
+ this.listen(document, "pointerup", () => this._disarm());
5218
+ this.listen(document, "pointercancel", () => this._disarm());
5219
+ }
5143
5220
  this._renderRows();
5144
5221
  return root;
5145
5222
  }
@@ -5162,16 +5239,27 @@ var MultiValueEditor = class extends Component {
5162
5239
  if (!silent) this.emit("change", { values: this.getValues() });
5163
5240
  return this;
5164
5241
  }
5165
- /** @param {number} [focusIndex=-1] @returns {void} */
5166
- _renderRows(focusIndex = -1) {
5242
+ /** @returns {{drag: boolean, buttons: boolean}} */
5243
+ _reorder() {
5244
+ return REORDER[this.options.reorder] ?? REORDER.both;
5245
+ }
5246
+ /**
5247
+ * @param {number} [focusIndex=-1] Row to focus after the render, or -1 for none.
5248
+ * @param {keyof PARTS} [focusPart='value'] Which control of that row takes focus.
5249
+ * @returns {void}
5250
+ */
5251
+ _renderRows(focusIndex = -1, focusPart = "value") {
5167
5252
  const choices = normalizeOptions2(this.options.options);
5168
5253
  const useSelect = this.options.options !== null;
5254
+ const { drag, buttons } = this._reorder();
5255
+ const last = this._values.length - 1;
5169
5256
  const rows = this._values.map((value, index) => {
5257
+ const entries = !useSelect || choices.some(([option]) => option === value) ? choices : [[value, value], ...choices];
5170
5258
  const control = useSelect ? h("select", {
5171
5259
  class: "zx-multi-value-editor__value",
5172
5260
  ariaLabel: `Value ${index + 1}`,
5173
5261
  dataset: { index }
5174
- }, choices.map(([optionValue, label]) => h("option", {
5262
+ }, entries.map(([optionValue, label]) => h("option", {
5175
5263
  value: optionValue,
5176
5264
  selected: optionValue === value
5177
5265
  }, label))) : h("input", {
@@ -5184,40 +5272,52 @@ var MultiValueEditor = class extends Component {
5184
5272
  return h(
5185
5273
  "div",
5186
5274
  { class: "zx-multi-value-editor__row", dataset: { row: index } },
5275
+ drag ? h("button", {
5276
+ class: "zx-multi-value-editor__handle",
5277
+ type: "button",
5278
+ ariaLabel: `Reorder value ${index + 1}. Use the arrow keys to move it.`,
5279
+ dataset: { action: "grab", index }
5280
+ }, icon("drag", { size: 12 })) : null,
5187
5281
  control,
5188
5282
  h(
5189
5283
  "div",
5190
5284
  { class: "zx-multi-value-editor__controls" },
5191
- h("button", {
5285
+ buttons ? h("button", {
5192
5286
  class: "zx-btn",
5193
5287
  type: "button",
5194
- disabled: index === 0,
5288
+ // `aria-disabled` rather than `disabled`: the buttons re-render on every move, and a
5289
+ // control that disables itself under the reader's finger drops focus to the document.
5290
+ ariaDisabled: String(index === 0),
5195
5291
  ariaLabel: `Move value ${index + 1} up`,
5196
5292
  dataset: { action: "up", index }
5197
- }, "\u2191"),
5198
- h("button", {
5293
+ }, icon("chevron-up", { size: 12 })) : null,
5294
+ buttons ? h("button", {
5199
5295
  class: "zx-btn",
5200
5296
  type: "button",
5201
- disabled: index === this._values.length - 1,
5297
+ ariaDisabled: String(index === last),
5202
5298
  ariaLabel: `Move value ${index + 1} down`,
5203
5299
  dataset: { action: "down", index }
5204
- }, "\u2193"),
5300
+ }, icon("chevron-down", { size: 12 })) : null,
5205
5301
  h("button", {
5206
5302
  class: "zx-btn",
5207
5303
  type: "button",
5208
5304
  ariaLabel: `Remove value ${index + 1}`,
5209
5305
  dataset: { action: "remove", index }
5210
- }, "\xD7")
5306
+ }, icon("x", { size: 12 }))
5211
5307
  )
5212
5308
  );
5213
5309
  });
5214
5310
  this._rows.replaceChildren(...rows);
5215
- if (focusIndex >= 0) rows[focusIndex]?.querySelector(".zx-multi-value-editor__value")?.focus();
5311
+ if (focusIndex >= 0) {
5312
+ const row = rows[Math.min(focusIndex, rows.length - 1)];
5313
+ row?.querySelector(PARTS[focusPart] ?? PARTS.value)?.focus();
5314
+ }
5216
5315
  }
5217
5316
  /** @param {MouseEvent} event @returns {void} */
5218
5317
  _handleClick(event) {
5219
5318
  const button2 = event.target instanceof Element ? event.target.closest("[data-action]") : null;
5220
5319
  if (!button2 || !this.el.contains(button2)) return;
5320
+ if (button2.getAttribute("aria-disabled") === "true") return;
5221
5321
  const action = button2.dataset.action;
5222
5322
  const index = Number(button2.dataset.index);
5223
5323
  if (action === "add") {
@@ -5230,28 +5330,156 @@ var MultiValueEditor = class extends Component {
5230
5330
  this._renderRows(Math.min(index, this._values.length - 1));
5231
5331
  this._emitChange();
5232
5332
  } else if ((action === "up" || action === "down") && Number.isInteger(index)) {
5233
- const target = index + (action === "up" ? -1 : 1);
5234
- if (target < 0 || target >= this._values.length) return;
5235
- [this._values[index], this._values[target]] = [this._values[target], this._values[index]];
5236
- this._renderRows(target);
5237
- this._emitChange();
5333
+ this._move(index, index + (action === "up" ? -1 : 1), action);
5238
5334
  }
5239
5335
  }
5240
5336
  /** @param {Event} event @returns {void} */
5241
5337
  _handleChange(event) {
5242
5338
  const control = event.target instanceof Element ? event.target.closest("[data-index]") : null;
5243
- if (!control) return;
5339
+ if (!control || !("value" in control)) return;
5244
5340
  const index = Number(control.dataset.index);
5245
5341
  if (!Number.isInteger(index) || index < 0 || index >= this._values.length) return;
5246
5342
  this._values[index] = control.value;
5247
5343
  this._emitChange();
5248
5344
  }
5345
+ /**
5346
+ * The handle is the keyboard equivalent of dragging it, so `reorder: 'drag'` is still operable
5347
+ * without a pointer.
5348
+ * @param {KeyboardEvent} event
5349
+ * @returns {void}
5350
+ */
5351
+ _handleKeydown(event) {
5352
+ const handle = event.target instanceof Element ? event.target.closest(".zx-multi-value-editor__handle") : null;
5353
+ if (!handle) return;
5354
+ const index = Number(handle.dataset.index);
5355
+ const target = event.key === "ArrowUp" ? index - 1 : event.key === "ArrowDown" ? index + 1 : event.key === "Home" ? 0 : event.key === "End" ? this._values.length - 1 : -1;
5356
+ if (target < 0 || target >= this._values.length) return;
5357
+ event.preventDefault();
5358
+ this._move(index, target, "handle");
5359
+ }
5360
+ /**
5361
+ * Arms the row for dragging. Only the handle does this, so text selection inside a row's input
5362
+ * keeps working and a stray drag cannot reorder anything.
5363
+ * @param {PointerEvent} event
5364
+ * @returns {void}
5365
+ */
5366
+ _handlePointerDown(event) {
5367
+ const handle = event.target instanceof Element ? event.target.closest(".zx-multi-value-editor__handle") : null;
5368
+ this._disarm();
5369
+ if (!handle || handle.disabled) return;
5370
+ const row = (
5371
+ /** @type {HTMLElement|null} */
5372
+ handle.closest(".zx-multi-value-editor__row")
5373
+ );
5374
+ if (!row) return;
5375
+ row.draggable = true;
5376
+ this._armed = row;
5377
+ }
5378
+ /** @returns {void} */
5379
+ _disarm() {
5380
+ if (this._armed) this._armed.draggable = false;
5381
+ this._armed = null;
5382
+ }
5383
+ /** @param {DragEvent} event @returns {void} */
5384
+ _handleDragStart(event) {
5385
+ const row = event.target instanceof Element ? event.target.closest(".zx-multi-value-editor__row") : null;
5386
+ if (!row || row !== this._armed) {
5387
+ event.preventDefault();
5388
+ return;
5389
+ }
5390
+ this._dragIndex = Number(row.dataset.row);
5391
+ row.dataset.dragging = "true";
5392
+ this.el.setAttribute("data-state", "dragging");
5393
+ event.dataTransfer?.setData("text/plain", String(this._dragIndex));
5394
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
5395
+ }
5396
+ /** @param {DragEvent} event @returns {void} */
5397
+ _handleDragOver(event) {
5398
+ if (this._dragIndex < 0) return;
5399
+ event.preventDefault();
5400
+ if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
5401
+ this._showDrop(this._dropTarget(event.clientY));
5402
+ }
5403
+ /** @param {DragEvent} event @returns {void} */
5404
+ _handleDragLeave(event) {
5405
+ const to = event.relatedTarget;
5406
+ if (to instanceof Node && this._rows.contains(to)) return;
5407
+ this._clearDrop();
5408
+ }
5409
+ /** @param {DragEvent} event @returns {void} */
5410
+ _handleDrop(event) {
5411
+ if (this._dragIndex < 0) return;
5412
+ event.preventDefault();
5413
+ const from = this._dragIndex;
5414
+ const slot = this._dropTarget(event.clientY);
5415
+ this._finishDrag();
5416
+ this._move(from, slot > from ? slot - 1 : slot, "handle");
5417
+ }
5418
+ /** @returns {void} */
5419
+ _finishDrag() {
5420
+ this._dragIndex = -1;
5421
+ this._disarm();
5422
+ this._clearDrop();
5423
+ for (const row of this._rows.children) delete /** @type {HTMLElement} */
5424
+ row.dataset.dragging;
5425
+ if (this._initialState === null) this.el.removeAttribute("data-state");
5426
+ else this.el.setAttribute("data-state", this._initialState);
5427
+ }
5428
+ /**
5429
+ * Insertion slot (0…length) for a pointer at `clientY`.
5430
+ * @param {number} clientY Pointer position.
5431
+ * @returns {number}
5432
+ */
5433
+ _dropTarget(clientY) {
5434
+ const rows = [...this._rows.children];
5435
+ for (const [index, row] of rows.entries()) {
5436
+ const box = row.getBoundingClientRect();
5437
+ if (clientY < box.top + box.height / 2) return index;
5438
+ }
5439
+ return rows.length;
5440
+ }
5441
+ /** @param {number} slot Insertion slot to mark. @returns {void} */
5442
+ _showDrop(slot) {
5443
+ const rows = [...this._rows.children];
5444
+ const inert = slot === this._dragIndex || slot === this._dragIndex + 1;
5445
+ for (const [index, row] of rows.entries()) {
5446
+ const cell = (
5447
+ /** @type {HTMLElement} */
5448
+ row
5449
+ );
5450
+ if (!inert && index === slot) cell.dataset.drop = "before";
5451
+ else if (!inert && slot === rows.length && index === rows.length - 1) cell.dataset.drop = "after";
5452
+ else delete cell.dataset.drop;
5453
+ }
5454
+ }
5455
+ /** @returns {void} */
5456
+ _clearDrop() {
5457
+ for (const row of this._rows.children) delete /** @type {HTMLElement} */
5458
+ row.dataset.drop;
5459
+ }
5460
+ /**
5461
+ * @param {number} from Index to move.
5462
+ * @param {number} to Destination index.
5463
+ * @param {keyof PARTS} part Control of the moved row that takes focus.
5464
+ * @returns {boolean} Whether anything moved.
5465
+ */
5466
+ _move(from, to, part) {
5467
+ const size = this._values.length;
5468
+ if (from === to || from < 0 || to < 0 || from >= size || to >= size) return false;
5469
+ const [value] = this._values.splice(from, 1);
5470
+ this._values.splice(to, 0, value);
5471
+ this._renderRows(to, part);
5472
+ this._emitChange();
5473
+ return true;
5474
+ }
5249
5475
  /** @returns {void} */
5250
5476
  _emitChange() {
5251
5477
  this.emit("change", { values: this.getValues() });
5252
5478
  }
5253
5479
  /** @returns {void} */
5254
5480
  destroy() {
5481
+ if (this._initialState === null) this.el?.removeAttribute("data-state");
5482
+ else this.el?.setAttribute("data-state", this._initialState);
5255
5483
  for (const node of this._ownedNodes ?? []) node.remove();
5256
5484
  super.destroy();
5257
5485
  }
@@ -6276,6 +6504,7 @@ var Select = class _Select extends Component {
6276
6504
  /** @type {SelectOptions} */
6277
6505
  static defaults = {
6278
6506
  items: [],
6507
+ fixedItems: [],
6279
6508
  valueKey: "ID",
6280
6509
  labelKey: "name",
6281
6510
  renderItem: null,
@@ -6295,6 +6524,8 @@ var Select = class _Select extends Component {
6295
6524
  render() {
6296
6525
  this._items = [];
6297
6526
  this._visibleItems = [];
6527
+ this._fixedItems = [];
6528
+ this._visibleFixed = [];
6298
6529
  this._selected = null;
6299
6530
  this._optionEntries = [];
6300
6531
  this._activeIndex = -1;
@@ -6378,6 +6609,8 @@ var Select = class _Select extends Component {
6378
6609
  root.replaceChildren(control, panel);
6379
6610
  this._items = Array.isArray(this.options.items) ? this.options.items.slice() : [];
6380
6611
  this._visibleItems = this._items.slice();
6612
+ this._fixedItems = Array.isArray(this.options.fixedItems) ? this.options.fixedItems.slice() : [];
6613
+ this._visibleFixed = this._fixedItems.slice();
6381
6614
  this._disabled = Boolean(this.options.disabled);
6382
6615
  this._handleTypeahead = typeahead(
6383
6616
  () => this._optionEntries.filter((entry) => !entry.clear).map((entry) => entry.element),
@@ -6454,7 +6687,22 @@ var Select = class _Select extends Component {
6454
6687
  const selectedValue = this.value;
6455
6688
  this._items = Array.isArray(items) ? items.slice() : [];
6456
6689
  this._visibleItems = this._items.slice();
6457
- this._selected = selectedValue == null ? null : this._items.find((item) => Object.is(this._itemValue(item), selectedValue)) ?? null;
6690
+ this._selected = selectedValue == null ? null : this._findItem(selectedValue);
6691
+ this._syncInputValue();
6692
+ this._renderOptions();
6693
+ this._position?.update();
6694
+ return this;
6695
+ }
6696
+ /**
6697
+ * Replaces the pinned choices and preserves selection by ID when possible.
6698
+ * @param {SelectItem[]} items New fixed items.
6699
+ * @returns {this}
6700
+ */
6701
+ setFixedItems(items) {
6702
+ const selectedValue = this.value;
6703
+ this._fixedItems = Array.isArray(items) ? items.slice() : [];
6704
+ this._visibleFixed = this._fixedItems.slice();
6705
+ this._selected = selectedValue == null ? null : this._findItem(selectedValue);
6458
6706
  this._syncInputValue();
6459
6707
  this._renderOptions();
6460
6708
  this._position?.update();
@@ -6508,6 +6756,7 @@ var Select = class _Select extends Component {
6508
6756
  this.refs.input.removeAttribute("aria-activedescendant");
6509
6757
  this._activeIndex = -1;
6510
6758
  this._visibleItems = this._items.slice();
6759
+ this._visibleFixed = this._fixedItems.slice();
6511
6760
  this._syncInputValue();
6512
6761
  this.emit("close", {});
6513
6762
  return this;
@@ -6563,6 +6812,38 @@ var Select = class _Select extends Component {
6563
6812
  select.set(requestedValue, { silent: true });
6564
6813
  return select;
6565
6814
  }
6815
+ /**
6816
+ * Creates the record-permission picker: the fixed Private and Public choices above the groups a
6817
+ * record can be shared with. The value is the tri-state ZeyOS stores — `false` for private,
6818
+ * `true` for public, or a group ID — so the control binds straight to the record field.
6819
+ *
6820
+ * @param {Element|string|null} target Component target.
6821
+ * @param {SelectOptions & {groups?: SelectItem[]|((query: string) => Promise<SelectItem[]>|SelectItem[])}} [options={}]
6822
+ * Select overrides other than the fixed choices; `groups` holds the shareable groups, either as
6823
+ * an array (filtered locally) or as a loader function (queried remotely).
6824
+ * @returns {Select}
6825
+ */
6826
+ static permission(target, options = {}) {
6827
+ const { groups = [], value = true, valueKey = "ID", labelKey = "name", ...rest } = options;
6828
+ const loader = typeof groups === "function" ? groups : null;
6829
+ const shareable = loader ? [] : groups;
6830
+ const choices = [{ ID: false, name: "Private" }, { ID: true, name: "Public" }];
6831
+ const select = new _Select(target, {
6832
+ filter: loader ?? "local",
6833
+ ...rest,
6834
+ value: null,
6835
+ items: shareable,
6836
+ fixedItems: [],
6837
+ // The two choices carry their own ID and label, so a caller's group readers stay untouched.
6838
+ valueKey: (item) => choices.includes(item) ? item.ID : readItem(item, valueKey),
6839
+ labelKey: (item) => choices.includes(item) ? item.name : String(readItem(item, labelKey) ?? "")
6840
+ });
6841
+ choices[0].name = select._message("permission.private", "Private");
6842
+ choices[1].name = select._message("permission.public", "Public");
6843
+ select.setFixedItems(choices);
6844
+ select.set(value, { silent: true });
6845
+ return select;
6846
+ }
6566
6847
  /** Cleans up the panel, pending async work, and enhanced target contents. @returns {void} */
6567
6848
  destroy() {
6568
6849
  this._cancelQuery();
@@ -6633,8 +6914,8 @@ var Select = class _Select extends Component {
6633
6914
  );
6634
6915
  if (!this._open) this.open();
6635
6916
  if (this.options.filter === "local") {
6636
- const keys = this.options.searchKeys ?? [this.options.labelKey];
6637
- this._visibleItems = matchItems(this._items, query, keys);
6917
+ this._visibleFixed = this._matchFixed(query);
6918
+ this._visibleItems = matchItems(this._items, query, this._searchKeys());
6638
6919
  this._renderOptions();
6639
6920
  this._setActive(this._optionEntries.length > 0 ? 0 : -1);
6640
6921
  this._position?.update();
@@ -6689,6 +6970,10 @@ var Select = class _Select extends Component {
6689
6970
  if (this.options.clearable) {
6690
6971
  this._appendOption(null, true, this.options.placeholder || this._message("select.none", "No selection"));
6691
6972
  }
6973
+ for (const item of this._visibleFixed) this._appendOption(item, false);
6974
+ if (this._visibleFixed.length > 0 && this._visibleItems.length > 0 && !this.options.groupKey) {
6975
+ list.append(h("div", { class: "zx-select__separator", role: "presentation" }));
6976
+ }
6692
6977
  for (const item of this._visibleItems) {
6693
6978
  if (this.options.groupKey) {
6694
6979
  const group = this._read(item, this.options.groupKey);
@@ -6702,7 +6987,7 @@ var Select = class _Select extends Component {
6702
6987
  }
6703
6988
  this._appendOption(item, false);
6704
6989
  }
6705
- if (this._visibleItems.length === 0 && !this._loading) {
6990
+ if (this._visibleItems.length === 0 && this._visibleFixed.length === 0 && !this._loading) {
6706
6991
  list.append(h("div", {
6707
6992
  class: "zx-select__empty",
6708
6993
  role: "status"
@@ -6742,6 +7027,7 @@ var Select = class _Select extends Component {
6742
7027
  if (this._queryTimer !== null) clearTimeout(this._queryTimer);
6743
7028
  this._requestSequence += 1;
6744
7029
  const sequence = this._requestSequence;
7030
+ this._visibleFixed = this._matchFixed(query);
6745
7031
  if (query.length < Math.max(0, Number(this.options.minQuery) || 0)) {
6746
7032
  this._setLoading(false);
6747
7033
  this._visibleItems = [];
@@ -6826,13 +7112,14 @@ var Select = class _Select extends Component {
6826
7112
  }
6827
7113
  /** @param {unknown} candidate @returns {SelectItem|null} */
6828
7114
  _findItem(candidate) {
6829
- const direct = this._items.find((item) => item === candidate);
7115
+ const pool = this._fixedItems.length === 0 ? this._items : [...this._fixedItems, ...this._items];
7116
+ const direct = pool.find((item) => item === candidate);
6830
7117
  if (direct !== void 0) return direct;
6831
7118
  const value = candidate !== null && typeof candidate === "object" ? this._itemValue(
6832
7119
  /** @type {SelectItem} */
6833
7120
  candidate
6834
7121
  ) : candidate;
6835
- return this._items.find((item) => Object.is(this._itemValue(item), value)) ?? null;
7122
+ return pool.find((item) => Object.is(this._itemValue(item), value)) ?? null;
6836
7123
  }
6837
7124
  /** @param {SelectItem|null} item @returns {unknown} */
6838
7125
  _itemValue(item) {
@@ -6845,9 +7132,20 @@ var Select = class _Select extends Component {
6845
7132
  }
6846
7133
  /** @param {SelectItem|null} item @param {SelectValueReader|SelectLabelReader} reader @returns {unknown} */
6847
7134
  _read(item, reader) {
6848
- if (typeof reader === "function") return reader(item);
6849
- if (item === null || typeof item !== "object") return item;
6850
- return item[reader];
7135
+ return readItem(item, reader);
7136
+ }
7137
+ /** @returns {Array<SelectValueReader|SelectLabelReader>} */
7138
+ _searchKeys() {
7139
+ return this.options.searchKeys ?? [this.options.labelKey];
7140
+ }
7141
+ /**
7142
+ * Narrows the pinned choices. They are always matched here and never handed to a filter
7143
+ * function: a fixed choice belongs to the control, not to whatever the remote source returns.
7144
+ * @param {string} query Search text.
7145
+ * @returns {SelectItem[]}
7146
+ */
7147
+ _matchFixed(query) {
7148
+ return matchItems(this._fixedItems, query, this._searchKeys());
6851
7149
  }
6852
7150
  /** @returns {number} */
6853
7151
  _selectedEntryIndex() {
@@ -6870,6 +7168,11 @@ var Select = class _Select extends Component {
6870
7168
  return message === key ? fallback : message;
6871
7169
  }
6872
7170
  };
7171
+ function readItem(item, reader) {
7172
+ if (typeof reader === "function") return reader(item);
7173
+ if (item === null || typeof item !== "object") return item;
7174
+ return item[reader];
7175
+ }
6873
7176
  function isPrintable(event) {
6874
7177
  return event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
6875
7178
  }
@@ -8010,9 +8313,9 @@ var TagPicker = class extends Component {
8010
8313
  function normalizeLabel(value) {
8011
8314
  return String(value ?? "").trim().toLocaleLowerCase();
8012
8315
  }
8013
- function sameSelection(a, b, read2) {
8316
+ function sameSelection(a, b, read) {
8014
8317
  if (a.length !== b.length) return false;
8015
- return a.every((item, index) => Object.is(read2(item), read2(b[index])));
8318
+ return a.every((item, index) => Object.is(read(item), read(b[index])));
8016
8319
  }
8017
8320
 
8018
8321
  // src/components/tag-picker/field-adapter.js
@@ -8982,6 +9285,82 @@ function isFileList(value) {
8982
9285
  return typeof FileList === "function" && value instanceof FileList;
8983
9286
  }
8984
9287
 
9288
+ // src/core/format.js
9289
+ var MIN_DIGITS = 0;
9290
+ var MAX_DIGITS = 20;
9291
+ var DEFAULT_MAX_DIGITS = 3;
9292
+ var numberFormatters = /* @__PURE__ */ new Map();
9293
+ function formatNumber(value, options = {}) {
9294
+ const number = toFiniteNumber(value);
9295
+ if (number === null) return "";
9296
+ const { locale = getLanguage(), decimals, minDecimals, maxDecimals, group = true } = options;
9297
+ return format(locale, digitOptions({ decimals, minDecimals, maxDecimals, group }), number);
9298
+ }
9299
+ function formatCurrency(value, currency, options = {}) {
9300
+ const number = toFiniteNumber(value);
9301
+ if (number === null) return "";
9302
+ const { locale = getLanguage(), decimals } = options;
9303
+ const code = currencyCode(currency);
9304
+ if (code === null) return formatNumber(number, { locale, decimals });
9305
+ const digits = digitOptions({ decimals, group: true });
9306
+ return format(locale, { ...digits, style: "currency", currency: code }, number);
9307
+ }
9308
+ function formatPercent(value, options = {}) {
9309
+ const number = toFiniteNumber(value);
9310
+ if (number === null) return "";
9311
+ const { locale = getLanguage(), decimals = 0 } = options;
9312
+ return format(locale, { ...digitOptions({ decimals, group: true }), style: "percent" }, number);
9313
+ }
9314
+ function format(locale, options, value) {
9315
+ const key = `${locale}\0${JSON.stringify(options)}`;
9316
+ let formatter = numberFormatters.get(key);
9317
+ if (formatter === void 0) {
9318
+ formatter = createNumberFormat(locale, options);
9319
+ numberFormatters.set(key, formatter);
9320
+ }
9321
+ return formatter.format(value);
9322
+ }
9323
+ function createNumberFormat(locale, options) {
9324
+ try {
9325
+ return new Intl.NumberFormat(locale, options);
9326
+ } catch {
9327
+ try {
9328
+ return new Intl.NumberFormat(void 0, options);
9329
+ } catch {
9330
+ return new Intl.NumberFormat();
9331
+ }
9332
+ }
9333
+ }
9334
+ function digitOptions({ decimals, minDecimals, maxDecimals, group = true }) {
9335
+ const result = { useGrouping: group !== false };
9336
+ const min = digitCount(minDecimals ?? decimals);
9337
+ const max = digitCount(maxDecimals ?? decimals);
9338
+ if (min !== null) result.minimumFractionDigits = min;
9339
+ if (max !== null) {
9340
+ result.maximumFractionDigits = max;
9341
+ if (min !== null && min > max) result.minimumFractionDigits = max;
9342
+ } else if (min !== null) {
9343
+ result.maximumFractionDigits = Math.max(min, DEFAULT_MAX_DIGITS);
9344
+ }
9345
+ return result;
9346
+ }
9347
+ function digitCount(value) {
9348
+ if (value == null) return null;
9349
+ const number = Number(value);
9350
+ if (!Number.isFinite(number)) return null;
9351
+ return Math.min(MAX_DIGITS, Math.max(MIN_DIGITS, Math.round(number)));
9352
+ }
9353
+ function toFiniteNumber(value) {
9354
+ if (value === null || value === void 0 || value === "") return null;
9355
+ const number = typeof value === "number" ? value : Number(value);
9356
+ return Number.isFinite(number) ? number : null;
9357
+ }
9358
+ function currencyCode(value) {
9359
+ if (typeof value !== "string") return null;
9360
+ const code = value.trim().toUpperCase();
9361
+ return /^[A-Z]{3}$/.test(code) ? code : null;
9362
+ }
9363
+
8985
9364
  // src/components/check-button/check-button.js
8986
9365
  var CheckButton = class extends Component {
8987
9366
  static cssName = "check-button";
@@ -9142,6 +9521,7 @@ var Modal = class extends Component {
9142
9521
  if (!this.options.closable || cancelEvent.defaultPrevented) event.preventDefault();
9143
9522
  });
9144
9523
  this.listen(this.el, "close", () => {
9524
+ if (!this._isRealClose()) return;
9145
9525
  this.el.dataset.state = "closed";
9146
9526
  delete this.el.dataset.zxOverlayOrder;
9147
9527
  const result = this.#hasPendingResult ? this.#pendingResult : this.el.returnValue || void 0;
@@ -9155,6 +9535,37 @@ var Modal = class extends Component {
9155
9535
  this.close();
9156
9536
  });
9157
9537
  }
9538
+ /**
9539
+ * Element the owned dialog is appended to. Overridden by subclasses that live inside a host
9540
+ * rather than at the document level.
9541
+ *
9542
+ * Called from `render()`, which the base constructor runs before any subclass field
9543
+ * initializers, so an override may read `this.options` but must not touch instance state.
9544
+ * @returns {Element}
9545
+ */
9546
+ mountTarget() {
9547
+ return document.body;
9548
+ }
9549
+ /**
9550
+ * Puts the dialog on screen. Overridden by subclasses that open non-modally.
9551
+ * @returns {void}
9552
+ */
9553
+ _show() {
9554
+ this.el.showModal();
9555
+ }
9556
+ /**
9557
+ * Whether an incoming `close` event is a real dismissal.
9558
+ *
9559
+ * A dialog cannot move between the top layer and the page flow while it is open, so a subclass
9560
+ * that re-hosts itself has to close and immediately reopen. `close` is dispatched in a queued
9561
+ * task, not synchronously, so that stale event arrives once the dialog is already open again —
9562
+ * late enough to clobber `data-state`, fire a `close` nobody asked for, and trip
9563
+ * `destroyOnClose`. Returning false from an override drops it at the source instead.
9564
+ * @returns {boolean}
9565
+ */
9566
+ _isRealClose() {
9567
+ return true;
9568
+ }
9158
9569
  /**
9159
9570
  * Creates the owned native dialog.
9160
9571
  * @returns {HTMLDialogElement}
@@ -9167,7 +9578,7 @@ var Modal = class extends Component {
9167
9578
  }, h("div", { class: "zx-modal__content", ref: "content" }))
9168
9579
  );
9169
9580
  dialog.dataset.state = "closed";
9170
- document.body.append(dialog);
9581
+ this.mountTarget().append(dialog);
9171
9582
  return dialog;
9172
9583
  }
9173
9584
  /**
@@ -9180,7 +9591,7 @@ var Modal = class extends Component {
9180
9591
  this.el.dataset.zxOverlayOrder = String(nextOverlayOrder());
9181
9592
  this.el.returnValue = "";
9182
9593
  try {
9183
- this.el.showModal();
9594
+ this._show();
9184
9595
  } catch (error) {
9185
9596
  delete this.el.dataset.zxOverlayOrder;
9186
9597
  throw error;
@@ -9295,7 +9706,7 @@ var Dialog = class _Dialog extends Modal {
9295
9706
  */
9296
9707
  constructor(_target = null, options = {}) {
9297
9708
  super(null, options);
9298
- this.#setSize(this.options.size);
9709
+ this._applySize(this.options.size);
9299
9710
  this.#baseTitle = String(this.options.title ?? "");
9300
9711
  this.#applyTitle(this.#baseTitle);
9301
9712
  this.#baseButtons = copyButtons(this.options.buttons);
@@ -9345,7 +9756,7 @@ var Dialog = class _Dialog extends Modal {
9345
9756
  )
9346
9757
  );
9347
9758
  dialog.dataset.state = "closed";
9348
- document.body.append(dialog);
9759
+ this.mountTarget().append(dialog);
9349
9760
  return dialog;
9350
9761
  }
9351
9762
  /**
@@ -9567,8 +9978,15 @@ var Dialog = class _Dialog extends Modal {
9567
9978
  dialog.open();
9568
9979
  });
9569
9980
  }
9570
- /** @param {'sm'|'md'|'lg'|number} size @returns {void} */
9571
- #setSize(size) {
9981
+ /**
9982
+ * Applies the dialog width. Subclasses that size themselves on a different axis override this.
9983
+ *
9984
+ * Called from the constructor before any subclass field initializers have run, so an override
9985
+ * may only touch the DOM — reading an uninitialized private field here throws.
9986
+ * @param {'sm'|'md'|'lg'|number} size Preset or pixel width.
9987
+ * @returns {void}
9988
+ */
9989
+ _applySize(size) {
9572
9990
  const presets = { sm: 400, md: 600, lg: 840 };
9573
9991
  const width = typeof size === "number" && Number.isFinite(size) ? size : presets[size] ?? presets.md;
9574
9992
  this.el.style.inlineSize = `${Math.max(0, width)}px`;
@@ -9996,6 +10414,10 @@ var Tabbox = class extends Component {
9996
10414
  if (!tab || !tablist.contains(tab)) return;
9997
10415
  const record = this._recordForTab(tab);
9998
10416
  if (!record || record.definition.disabled) return;
10417
+ if (event.target.closest?.(".zx-tabbox__close")) {
10418
+ this._closeTab(record);
10419
+ return;
10420
+ }
9999
10421
  this._setFocused(record);
10000
10422
  this.openTab(record.definition.name);
10001
10423
  });
@@ -10045,8 +10467,9 @@ var Tabbox = class extends Component {
10045
10467
  children.push(title, badge);
10046
10468
  if (normalized.closable) {
10047
10469
  children.push(h("span", {
10048
- class: "zx-tabbox__close",
10049
- ariaHidden: "true"
10470
+ class: "zx-tabbox__close zx-icon-btn",
10471
+ ariaHidden: "true",
10472
+ title: "Close"
10050
10473
  }, icon("x", { size: 14 })));
10051
10474
  }
10052
10475
  const tab = (
@@ -10220,13 +10643,23 @@ var Tabbox = class extends Component {
10220
10643
  }
10221
10644
  if (event.key === "Delete" && current.definition.closable) {
10222
10645
  event.preventDefault();
10223
- const index = this._tabs.indexOf(current);
10224
- if (!this._removeTab(current.definition.name, true)) return;
10225
- const remaining = this._tabs[Math.min(index, this._tabs.length - 1)] ?? this._tabs[index - 1];
10226
- const focusTarget = remaining && !remaining.definition.disabled ? remaining : this._active;
10227
- if (focusTarget) this._setFocused(focusTarget);
10646
+ this._closeTab(current);
10228
10647
  }
10229
10648
  }
10649
+ /**
10650
+ * Closes a tab on a user gesture, keeping roving focus inside the tablist.
10651
+ * @param {TabRecord} record Tab to close.
10652
+ * @returns {void}
10653
+ * @fires Tabbox#close
10654
+ */
10655
+ _closeTab(record) {
10656
+ const index = this._tabs.indexOf(record);
10657
+ const held = record.tab.contains(document.activeElement);
10658
+ if (!this._removeTab(record.definition.name, true)) return;
10659
+ const remaining = this._tabs[Math.min(index, this._tabs.length - 1)] ?? this._tabs[index - 1];
10660
+ const focusTarget = remaining && !remaining.definition.disabled ? remaining : this._active;
10661
+ if (focusTarget) this._setFocused(focusTarget, held);
10662
+ }
10230
10663
  /** @param {string} name @param {boolean} emitClose @returns {boolean} */
10231
10664
  _removeTab(name, emitClose) {
10232
10665
  const record = this._find(name);
@@ -10373,7 +10806,8 @@ var Table = class extends Component {
10373
10806
  emptyText: null,
10374
10807
  rowClass: null,
10375
10808
  zebra: true,
10376
- editMode: false
10809
+ editMode: false,
10810
+ hierarchy: false
10377
10811
  };
10378
10812
  /**
10379
10813
  * Creates a table around a target or creates a new root when target is null.
@@ -10410,8 +10844,17 @@ var Table = class extends Component {
10410
10844
  this._rovingKey = null;
10411
10845
  this._rovingCell = null;
10412
10846
  this._rovingMatched = false;
10847
+ this._hierarchy = normalizeHierarchy(this.options.hierarchy, this._columns);
10848
+ this._expanded = initialExpanded(this._hierarchy?.expanded);
10413
10849
  if (this._sort && this.options.sortMode === "local") this._sortData();
10850
+ if (this._hierarchy?.expanded === true) {
10851
+ for (const id of hierarchyBranchIds(this._data, this.options.rowId, this._hierarchy.parentId)) {
10852
+ this._expanded.add(id);
10853
+ }
10854
+ }
10855
+ this._reconcileExpanded();
10414
10856
  this._table = h("table", { class: "zx-table__table" });
10857
+ if (this._hierarchy) this._table.setAttribute("role", "treegrid");
10415
10858
  this._colgroup = h("colgroup", { class: "zx-table__columns" });
10416
10859
  this._thead = h("thead", { class: "zx-table__head" });
10417
10860
  this._tbody = h("tbody", { class: "zx-table__body" });
@@ -10480,6 +10923,7 @@ var Table = class extends Component {
10480
10923
  assertRows(rows);
10481
10924
  this._data = [...rows];
10482
10925
  if (this._sort && this.options.sortMode === "local") this._sortData();
10926
+ this._reconcileExpanded();
10483
10927
  const selectionChanged = this._pruneSelection();
10484
10928
  this._renderBody();
10485
10929
  if (selectionChanged) this._emitSelectionChange();
@@ -10497,6 +10941,7 @@ var Table = class extends Component {
10497
10941
  assertRows(rows);
10498
10942
  this._data.push(...rows);
10499
10943
  if (this._sort && this.options.sortMode === "local") this._sortData();
10944
+ this._reconcileExpanded();
10500
10945
  this._renderBody();
10501
10946
  this._emitDataChange();
10502
10947
  return this;
@@ -10516,6 +10961,7 @@ var Table = class extends Component {
10516
10961
  this._data[index] = row;
10517
10962
  if (wasSelected) this._selected.add(this._idFor(row));
10518
10963
  if (this._sort && this.options.sortMode === "local") this._sortData();
10964
+ this._reconcileExpanded();
10519
10965
  this._renderBody();
10520
10966
  if (wasSelected && !Object.is(id, this._idFor(row))) this._emitSelectionChange();
10521
10967
  this._emitDataChange();
@@ -10533,6 +10979,7 @@ var Table = class extends Component {
10533
10979
  const index = this._data.findIndex((candidate) => Object.is(this._idFor(candidate), id));
10534
10980
  if (index < 0) return this;
10535
10981
  this._data.splice(index, 1);
10982
+ this._reconcileExpanded();
10536
10983
  const selectionChanged = this._selected.delete(id);
10537
10984
  if (Object.is(this._selectionAnchorId, id)) this._selectionAnchorId = null;
10538
10985
  this._renderBody();
@@ -10548,7 +10995,10 @@ var Table = class extends Component {
10548
10995
  getRow(id) {
10549
10996
  return this._data.find((row) => Object.is(this._idFor(row), id)) ?? null;
10550
10997
  }
10551
- /** @returns {TableRow[]} Shallow copy of current rows in display order. */
10998
+ /**
10999
+ * Returns a shallow copy of the current rows in display order.
11000
+ * @returns {TableRow[]} Current table rows.
11001
+ */
10552
11002
  getData() {
10553
11003
  return [...this._data];
10554
11004
  }
@@ -10562,6 +11012,7 @@ var Table = class extends Component {
10562
11012
  empty() {
10563
11013
  const selectionChanged = this._selected.size > 0;
10564
11014
  this._data = [];
11015
+ this._expanded.clear();
10565
11016
  this._selected.clear();
10566
11017
  this._selectionAnchorId = null;
10567
11018
  this._renderBody();
@@ -10596,7 +11047,10 @@ var Table = class extends Component {
10596
11047
  if (!options.silent) this.emit("sort", { id, dir: direction });
10597
11048
  return this;
10598
11049
  }
10599
- /** @returns {TableRow[]} Selected rows in current display order. */
11050
+ /**
11051
+ * Returns selected rows in current display order.
11052
+ * @returns {TableRow[]} Selected rows.
11053
+ */
10600
11054
  getSelection() {
10601
11055
  return this._data.filter((row) => this._selected.has(this._idFor(row)));
10602
11056
  }
@@ -10635,6 +11089,75 @@ var Table = class extends Component {
10635
11089
  this._emitSelectionChange();
10636
11090
  return this;
10637
11091
  }
11092
+ /**
11093
+ * Toggles one hierarchy branch. A leaf or an id absent from the table is a no-op.
11094
+ * @param {unknown} id Row id.
11095
+ * @param {{silent?: boolean}} [options={}] Event behavior.
11096
+ * @returns {this}
11097
+ * @fires Table#rowtoggle
11098
+ */
11099
+ toggleRow(id, options = {}) {
11100
+ return this._expanded.has(id) ? this.collapseRow(id, options) : this.expandRow(id, options);
11101
+ }
11102
+ /**
11103
+ * Expands one hierarchy branch.
11104
+ * @param {unknown} id Row id.
11105
+ * @param {{silent?: boolean}} [options={}] Event behavior.
11106
+ * @returns {this}
11107
+ * @fires Table#rowtoggle
11108
+ */
11109
+ expandRow(id, options = {}) {
11110
+ return this._setRowExpanded(id, true, options);
11111
+ }
11112
+ /**
11113
+ * Collapses one hierarchy branch without discarding its descendants or their expansion state.
11114
+ * @param {unknown} id Row id.
11115
+ * @param {{silent?: boolean}} [options={}] Event behavior.
11116
+ * @returns {this}
11117
+ * @fires Table#rowtoggle
11118
+ */
11119
+ collapseRow(id, options = {}) {
11120
+ return this._setRowExpanded(id, false, options);
11121
+ }
11122
+ /**
11123
+ * Expands every row that currently has children.
11124
+ * @param {{silent?: boolean}} [options={}] Event behavior. When not silent, one `rowtoggle`
11125
+ * event is emitted per branch whose state changed.
11126
+ * @returns {this}
11127
+ */
11128
+ expandAll(options = {}) {
11129
+ if (!this._hierarchy) return this;
11130
+ const branchIds = hierarchyBranchIds(this._data, this.options.rowId, this._hierarchy.parentId);
11131
+ const changed = branchIds.filter((id) => !this._expanded.has(id));
11132
+ for (const id of changed) this._expanded.add(id);
11133
+ if (changed.length) this._renderBody();
11134
+ if (!options.silent) {
11135
+ for (const id of changed) this._emitRowToggle(id, true);
11136
+ }
11137
+ return this;
11138
+ }
11139
+ /**
11140
+ * Collapses every hierarchy branch.
11141
+ * @param {{silent?: boolean}} [options={}] Event behavior.
11142
+ * @returns {this}
11143
+ */
11144
+ collapseAll(options = {}) {
11145
+ if (!this._hierarchy || this._expanded.size === 0) return this;
11146
+ const changed = [...this._expanded];
11147
+ this._expanded.clear();
11148
+ this._renderBody();
11149
+ if (!options.silent) {
11150
+ for (const id of changed) this._emitRowToggle(id, false);
11151
+ }
11152
+ return this;
11153
+ }
11154
+ /**
11155
+ * Returns expanded branch ids in current flat-data order.
11156
+ * @returns {unknown[]} Expanded branch ids.
11157
+ */
11158
+ getExpanded() {
11159
+ return this._data.map((row) => this._idFor(row)).filter((id) => this._expanded.has(id));
11160
+ }
10638
11161
  /**
10639
11162
  * Opens the editor for a cell. No-op unless the table runs with `editMode` and the column
10640
11163
  * resolves to an editable type for that row. In `'row'` mode every editable cell of the row is
@@ -10742,11 +11265,17 @@ var Table = class extends Component {
10742
11265
  this._abortEdit({ focus: true });
10743
11266
  return this;
10744
11267
  }
10745
- /** @returns {boolean} Whether a cell or row is currently open for editing. */
11268
+ /**
11269
+ * Reports whether a cell or row is currently open for editing.
11270
+ * @returns {boolean} Whether an edit is open.
11271
+ */
10746
11272
  isEditing() {
10747
11273
  return this._edit !== null;
10748
11274
  }
10749
- /** @returns {{id: unknown, columnId: string}|null} Row id and focused column id, or null. */
11275
+ /**
11276
+ * Returns the row id and focused column id for the open edit.
11277
+ * @returns {{id: unknown, columnId: string}|null} Editing location, or null.
11278
+ */
10750
11279
  getEditing() {
10751
11280
  return this._edit ? { id: this._edit.id, columnId: this._edit.columnId } : null;
10752
11281
  }
@@ -10755,7 +11284,31 @@ var Table = class extends Component {
10755
11284
  * @returns {TableRow[]}
10756
11285
  */
10757
11286
  _visibleData() {
10758
- return this._rendered === Infinity ? this._data : this._data.slice(0, this._rendered);
11287
+ return this._visibleEntries().map((entry) => entry.row);
11288
+ }
11289
+ /** @returns {TableHierarchyEntry[]} Rows visible after hierarchy expansion, before growing. */
11290
+ _projectedData() {
11291
+ if (!this._hierarchy) {
11292
+ return this._data.map((row, index) => ({
11293
+ row,
11294
+ id: this._idFor(row),
11295
+ index,
11296
+ depth: 0,
11297
+ hasChildren: false,
11298
+ parentId: null
11299
+ }));
11300
+ }
11301
+ return projectHierarchyRows(
11302
+ this._data,
11303
+ this.options.rowId,
11304
+ this._hierarchy.parentId,
11305
+ this._expanded
11306
+ );
11307
+ }
11308
+ /** @returns {TableHierarchyEntry[]} Rows that currently have DOM entries. */
11309
+ _visibleEntries() {
11310
+ const projected = this._projectedData();
11311
+ return this._rendered === Infinity ? projected : projected.slice(0, this._rendered);
10759
11312
  }
10760
11313
  /**
10761
11314
  * Renders the next batch of rows.
@@ -10767,10 +11320,11 @@ var Table = class extends Component {
10767
11320
  if (!this._growStep) return this;
10768
11321
  const step = Number(count) > 0 ? Math.trunc(Number(count)) : this._growStep;
10769
11322
  const before = this._rendered;
10770
- this._rendered = Math.min(this._data.length, this._rendered + step);
11323
+ const total = this._projectedData().length;
11324
+ this._rendered = Math.min(total, this._rendered + step);
10771
11325
  if (this._rendered === before) return this;
10772
11326
  this._renderBody();
10773
- this.emit("grow", { rendered: this._rendered, total: this._data.length });
11327
+ this.emit("grow", { rendered: this._rendered, total });
10774
11328
  return this;
10775
11329
  }
10776
11330
  /**
@@ -10779,14 +11333,14 @@ var Table = class extends Component {
10779
11333
  * @fires Table#grow
10780
11334
  */
10781
11335
  showAll() {
10782
- return this.growBy(this._data.length);
11336
+ return this.growBy(this._projectedData().length);
10783
11337
  }
10784
11338
  /**
10785
11339
  * How many rows are currently rendered.
10786
11340
  * @returns {number}
10787
11341
  */
10788
11342
  getRenderedCount() {
10789
- return Math.min(this._rendered, this._data.length);
11343
+ return Math.min(this._rendered, this._projectedData().length);
10790
11344
  }
10791
11345
  /**
10792
11346
  * Keeps the "show more" control in step with what is rendered.
@@ -10798,7 +11352,7 @@ var Table = class extends Component {
10798
11352
  */
10799
11353
  _syncGrowing() {
10800
11354
  if (!this._growStep) return;
10801
- const remaining = this._data.length - this.getRenderedCount();
11355
+ const remaining = this._projectedData().length - this.getRenderedCount();
10802
11356
  if (remaining <= 0) {
10803
11357
  this._more?.remove();
10804
11358
  this._more = null;
@@ -10869,10 +11423,11 @@ var Table = class extends Component {
10869
11423
  * @returns {void}
10870
11424
  */
10871
11425
  _applyStackedRoles(stacked) {
10872
- const parts = [[this._table, "table"], [this._thead, "rowgroup"], [this._tbody, "rowgroup"]];
11426
+ const parts = [[this._table, this._hierarchy ? "treegrid" : "table"], [this._thead, "rowgroup"], [this._tbody, "rowgroup"]];
10873
11427
  for (const [element, role] of parts) {
10874
11428
  if (!element) continue;
10875
11429
  if (stacked) element.setAttribute("role", role);
11430
+ else if (element === this._table && this._hierarchy) element.setAttribute("role", "treegrid");
10876
11431
  else element.removeAttribute("role");
10877
11432
  }
10878
11433
  for (const [selector, role] of [["tr", "row"], ["td", "cell"], ["th", "columnheader"]]) {
@@ -10949,7 +11504,8 @@ var Table = class extends Component {
10949
11504
  this._headers = /* @__PURE__ */ new Map();
10950
11505
  for (const column of this._columns) {
10951
11506
  const th = h("th", { scope: "col" });
10952
- if (column.align) th.dataset.align = column.align;
11507
+ const align = column.align ?? (isNumericColumn(column) ? "end" : null);
11508
+ if (align) th.dataset.align = align;
10953
11509
  if (column.headerTitle) th.title = column.headerTitle;
10954
11510
  if (column.sortable) {
10955
11511
  const button2 = h("button", {
@@ -10978,6 +11534,8 @@ var Table = class extends Component {
10978
11534
  this._rowElements = /* @__PURE__ */ new Map();
10979
11535
  this._rovingCell = null;
10980
11536
  this._rovingMatched = false;
11537
+ const projected = this._projectedData();
11538
+ this._hierarchyMeta = new Map(projected.map((entry) => [entry.id, entry]));
10981
11539
  if (this._data.length === 0) {
10982
11540
  fragment.append(h(
10983
11541
  "tr",
@@ -10988,10 +11546,12 @@ var Table = class extends Component {
10988
11546
  }, this._emptyContent())
10989
11547
  ));
10990
11548
  } else {
10991
- this._visibleData().forEach((row, index) => fragment.append(this._createRow(row, index)));
11549
+ this._visibleEntries().forEach((entry, visibleIndex) => {
11550
+ fragment.append(this._createRow(entry.row, entry.index, { ...entry, visibleIndex }));
11551
+ });
10992
11552
  }
10993
11553
  this._tbody.replaceChildren(fragment);
10994
- if (this._growStep) this._table.setAttribute("aria-rowcount", String(this._data.length + 1));
11554
+ if (this._growStep) this._table.setAttribute("aria-rowcount", String(projected.length + 1));
10995
11555
  this._syncSelectAll();
10996
11556
  this._syncGrowing();
10997
11557
  if (this._stacked) this._applyStackedRoles(true);
@@ -11010,18 +11570,27 @@ var Table = class extends Component {
11010
11570
  if (empty == null) return this.msg("table.empty");
11011
11571
  return typeof empty === "function" ? empty() : empty;
11012
11572
  }
11013
- /** @param {TableRow} row @param {number} index @returns {HTMLTableRowElement} */
11014
- _createRow(row, index) {
11573
+ /**
11574
+ * @param {TableRow} row
11575
+ * @param {number} index
11576
+ * @param {TableHierarchyEntry & {visibleIndex: number}} tree
11577
+ * @returns {HTMLTableRowElement}
11578
+ */
11579
+ _createRow(row, index, tree) {
11015
11580
  const id = this._idFor(row);
11016
11581
  const tr = (
11017
11582
  /** @type {HTMLTableRowElement} */
11018
11583
  h("tr", { dataset: { row: "" } })
11019
11584
  );
11020
- if (this._growStep) tr.setAttribute("aria-rowindex", String(index + 2));
11585
+ if (this._growStep) tr.setAttribute("aria-rowindex", String(tree.visibleIndex + 2));
11021
11586
  const rowClass = typeof this.options.rowClass === "function" ? this.options.rowClass(row) : "";
11022
11587
  if (rowClass) tr.classList.add(...String(rowClass).split(/\s+/).filter(Boolean));
11023
11588
  if (this.options.selectable !== false) tr.setAttribute("aria-selected", String(this._selected.has(id)));
11024
- this._rowMeta.set(tr, { row, id, index });
11589
+ if (this._hierarchy) {
11590
+ tr.setAttribute("aria-level", String(tree.depth + 1));
11591
+ if (tree.hasChildren) tr.setAttribute("aria-expanded", String(this._expanded.has(id)));
11592
+ }
11593
+ this._rowMeta.set(tr, { row, id, index, visibleIndex: tree.visibleIndex });
11025
11594
  const elements = this._rowElements.get(id) ?? [];
11026
11595
  elements.push(tr);
11027
11596
  this._rowElements.set(id, elements);
@@ -11039,7 +11608,8 @@ var Table = class extends Component {
11039
11608
  /** @type {HTMLTableCellElement} */
11040
11609
  h("td")
11041
11610
  );
11042
- if (column.align) cell.dataset.align = column.align;
11611
+ const align = column.align ?? (isNumericColumn(column) ? "end" : null);
11612
+ if (align) cell.dataset.align = align;
11043
11613
  cell.dataset.label = column.label;
11044
11614
  if (column.popin === false) cell.dataset.popin = "false";
11045
11615
  if (this._editMode !== false) {
@@ -11050,7 +11620,7 @@ var Table = class extends Component {
11050
11620
  this._offerRovingCell(cell, id, column.id);
11051
11621
  }
11052
11622
  }
11053
- this._renderCellContent(cell, column, row, index);
11623
+ this._renderCellContent(cell, column, row, index, tree);
11054
11624
  tr.append(cell);
11055
11625
  });
11056
11626
  return tr;
@@ -11061,13 +11631,33 @@ var Table = class extends Component {
11061
11631
  * @param {TableColumn} column Column definition.
11062
11632
  * @param {TableRow} row Row data.
11063
11633
  * @param {number} index Row index.
11634
+ * @param {TableHierarchyEntry} [tree] Hierarchy metadata for the row.
11064
11635
  * @returns {void}
11065
11636
  */
11066
- _renderCellContent(cell, column, row, index) {
11067
- const value = typeof column.render === "function" ? column.render(row, index) : row?.[column.id];
11068
- if (value && typeof value === "object" && typeof value.nodeType === "number") cell.replaceChildren(value);
11069
- else if (value != null) cell.replaceChildren(document.createTextNode(String(value)));
11070
- else cell.replaceChildren();
11637
+ _renderCellContent(cell, column, row, index, tree = this._hierarchyMeta?.get(this._idFor(row))) {
11638
+ const value = formatTableCell(column, row, index);
11639
+ const content = h("span", { class: "zx-table__cell-content" });
11640
+ if (value && typeof value === "object" && typeof value.nodeType === "number") content.append(value);
11641
+ else if (value != null) content.append(document.createTextNode(String(value)));
11642
+ if (!this._hierarchy || column.id !== this._hierarchy.column || !tree) {
11643
+ cell.replaceChildren(...content.childNodes);
11644
+ return;
11645
+ }
11646
+ const branch = h("span", { class: "zx-table__tree-cell" });
11647
+ branch.style.setProperty("--zx-table-depth", String(tree.depth));
11648
+ if (tree.hasChildren) {
11649
+ const expanded = this._expanded.has(tree.id);
11650
+ branch.append(h("button", {
11651
+ class: "zx-table__tree-toggle",
11652
+ type: "button",
11653
+ ariaExpanded: String(expanded),
11654
+ ariaLabel: `${expanded ? "Collapse" : "Expand"} row ${index + 1}`
11655
+ }, icon(expanded ? "chevron-down" : "chevron-right", { size: 14 })));
11656
+ } else {
11657
+ branch.append(h("span", { class: "zx-table__tree-spacer", ariaHidden: "true" }));
11658
+ }
11659
+ branch.append(content);
11660
+ cell.replaceChildren(branch);
11071
11661
  }
11072
11662
  /** @param {MouseEvent} event @returns {void} */
11073
11663
  _handleBodyClick(event) {
@@ -11080,7 +11670,15 @@ var Table = class extends Component {
11080
11670
  if (!rowElement || !this._tbody.contains(rowElement)) return;
11081
11671
  const meta = this._rowMeta.get(rowElement);
11082
11672
  if (!meta) return;
11083
- if (this._edit) this.commitEdit();
11673
+ if (this._edit) {
11674
+ this.commitEdit();
11675
+ if (this._edit) return;
11676
+ }
11677
+ const disclosure = target.closest(".zx-table__tree-toggle");
11678
+ if (disclosure) {
11679
+ this.toggleRow(meta.id);
11680
+ return;
11681
+ }
11084
11682
  const checkbox = target.closest(".zx-table__row-checkbox");
11085
11683
  if (checkbox && this.options.selectable === "multi") {
11086
11684
  this._toggleMultiSelection(
@@ -11176,7 +11774,15 @@ var Table = class extends Component {
11176
11774
  return entry;
11177
11775
  }
11178
11776
  if (type === "number") {
11179
- const field = new NumberField(null, { ...props, value: value ?? null });
11777
+ const locale = resolveColumnOption(column.locale, row);
11778
+ const decimals = resolveColumnDecimals(column, row);
11779
+ const suffix = column.type === "currency" ? resolveColumnOption(column.currency, row) : column.type === "unit" ? resolveColumnOption(column.unit, row) : null;
11780
+ const inferred = {
11781
+ ...locale != null && !Object.prototype.hasOwnProperty.call(props, "locale") ? { locale } : {},
11782
+ ...decimals != null && !Object.prototype.hasOwnProperty.call(props, "precision") ? { precision: decimals } : {},
11783
+ ...suffix != null && !Object.prototype.hasOwnProperty.call(props, "unit") ? { unit: String(suffix) } : {}
11784
+ };
11785
+ const field = new NumberField(null, { ...inferred, ...props, value: value ?? null });
11180
11786
  host.append(field.el);
11181
11787
  entry.component = field;
11182
11788
  entry.control = field.getInput();
@@ -11407,9 +12013,12 @@ var Table = class extends Component {
11407
12013
  _nextEditableCell(rowIndex, columnId, direction) {
11408
12014
  const columnCount = this._columns.length;
11409
12015
  if (rowIndex < 0 || columnCount === 0) return null;
11410
- let row = rowIndex;
12016
+ const visible = this._visibleData();
12017
+ const startingId = this._idFor(this._data[rowIndex]);
12018
+ let row = visible.findIndex((candidate) => Object.is(this._idFor(candidate), startingId));
12019
+ if (row < 0) return null;
11411
12020
  let column = this._columns.findIndex((candidate) => candidate.id === columnId);
11412
- const limit = this._data.length * columnCount + columnCount;
12021
+ const limit = visible.length * columnCount + columnCount;
11413
12022
  for (let step = 0; step < limit; step += 1) {
11414
12023
  column += direction;
11415
12024
  if (column >= columnCount) {
@@ -11419,11 +12028,15 @@ var Table = class extends Component {
11419
12028
  row -= 1;
11420
12029
  column = columnCount - 1;
11421
12030
  }
11422
- if (row < 0 || row >= this._data.length) return null;
11423
- const candidateRow = this._data[row];
12031
+ if (row < 0 || row >= visible.length) return null;
12032
+ const candidateRow = visible[row];
11424
12033
  const candidateColumn = this._columns[column];
11425
12034
  if (resolveEditable(candidateColumn, candidateRow) !== false) {
11426
- return { id: this._idFor(candidateRow), columnId: candidateColumn.id, index: row };
12035
+ return {
12036
+ id: this._idFor(candidateRow),
12037
+ columnId: candidateColumn.id,
12038
+ index: this._data.indexOf(candidateRow)
12039
+ };
11427
12040
  }
11428
12041
  }
11429
12042
  return null;
@@ -11490,14 +12103,16 @@ var Table = class extends Component {
11490
12103
  const message = this.msg(key);
11491
12104
  return message === key ? fallback : message;
11492
12105
  }
11493
- /** @param {{row: TableRow, id: unknown, index: number}} meta @param {boolean} checked @param {boolean} shiftKey @returns {void} */
12106
+ /** @param {{row: TableRow, id: unknown, index: number, visibleIndex?: number}} meta @param {boolean} checked @param {boolean} shiftKey @returns {void} */
11494
12107
  _toggleMultiSelection(meta, checked, shiftKey) {
11495
- const anchorIndex = this._data.findIndex((row) => Object.is(this._idFor(row), this._selectionAnchorId));
12108
+ const visible = this._visibleData();
12109
+ const anchorIndex = visible.findIndex((row) => Object.is(this._idFor(row), this._selectionAnchorId));
11496
12110
  if (shiftKey && anchorIndex >= 0) {
11497
- const start = Math.min(anchorIndex, meta.index);
11498
- const end = Math.max(anchorIndex, meta.index);
12111
+ const currentIndex = meta.visibleIndex ?? visible.findIndex((row) => Object.is(this._idFor(row), meta.id));
12112
+ const start = Math.min(anchorIndex, currentIndex);
12113
+ const end = Math.max(anchorIndex, currentIndex);
11499
12114
  for (let index = start; index <= end; index += 1) {
11500
- const id = this._idFor(this._data[index]);
12115
+ const id = this._idFor(visible[index]);
11501
12116
  if (checked) this._selected.add(id);
11502
12117
  else this._selected.delete(id);
11503
12118
  }
@@ -11548,7 +12163,34 @@ var Table = class extends Component {
11548
12163
  const column = this._columns.find((candidate) => candidate.id === this._sort.id);
11549
12164
  if (!column) return;
11550
12165
  const getValue = typeof column.sortValue === "function" ? column.sortValue : (row) => row?.[column.id];
11551
- this._data = sortRows(this._data, getValue, this._sort.dir);
12166
+ this._data = this._hierarchy ? sortHierarchyRows(this._data, this.options.rowId, this._hierarchy.parentId, getValue, this._sort.dir) : sortRows(this._data, getValue, this._sort.dir);
12167
+ }
12168
+ /** @param {unknown} id @param {boolean} expanded @param {{silent?: boolean}} options @returns {this} */
12169
+ _setRowExpanded(id, expanded, options) {
12170
+ if (!this._hierarchy) return this;
12171
+ const branchIds = hierarchyBranchIds(this._data, this.options.rowId, this._hierarchy.parentId);
12172
+ if (!branchIds.some((candidate) => Object.is(candidate, id))) return this;
12173
+ if (this._expanded.has(id) === expanded) return this;
12174
+ if (expanded) this._expanded.add(id);
12175
+ else this._expanded.delete(id);
12176
+ this._renderBody();
12177
+ if (!options.silent) this._emitRowToggle(id, expanded);
12178
+ return this;
12179
+ }
12180
+ /** @param {unknown} id @param {boolean} expanded @returns {void} */
12181
+ _emitRowToggle(id, expanded) {
12182
+ const row = this.getRow(id);
12183
+ if (row) this.emit("rowtoggle", { row, id, expanded });
12184
+ }
12185
+ /** Keeps the reader's current expansion state limited to branches still present in the data. */
12186
+ _reconcileExpanded() {
12187
+ if (!this._hierarchy) {
12188
+ this._expanded.clear();
12189
+ return;
12190
+ }
12191
+ const branches = hierarchyBranchIds(this._data, this.options.rowId, this._hierarchy.parentId);
12192
+ const valid = new Set(branches);
12193
+ for (const id of this._expanded) if (!valid.has(id)) this._expanded.delete(id);
11552
12194
  }
11553
12195
  /** @returns {boolean} Whether selection changed. */
11554
12196
  _pruneSelection() {
@@ -11572,6 +12214,65 @@ var Table = class extends Component {
11572
12214
  this.emit("datachange", { rows: this.getData() });
11573
12215
  }
11574
12216
  };
12217
+ function formatTableCell(column, row, index = 0) {
12218
+ if (typeof column?.render === "function") return column.render(row, index);
12219
+ const value = row?.[column?.id];
12220
+ const type = column?.type ?? "text";
12221
+ if (type === "text") return value;
12222
+ const locale = resolveColumnOption(column.locale, row);
12223
+ const decimals = resolveColumnDecimals(column, row);
12224
+ const options = {
12225
+ ...locale != null ? { locale: String(locale) } : {},
12226
+ ...decimals != null ? { decimals } : {}
12227
+ };
12228
+ if (type === "number") return formatNumber(value, options);
12229
+ if (type === "currency") {
12230
+ const currency = resolveColumnOption(column.currency, row);
12231
+ return formatCurrency(value, currency == null ? null : String(currency), options);
12232
+ }
12233
+ if (type === "percent") return formatPercent(value, options);
12234
+ if (type === "unit") {
12235
+ const number = formatNumber(value, options);
12236
+ const unit = resolveColumnOption(column.unit, row);
12237
+ return number && unit != null && String(unit) !== "" ? `${number}\xA0${unit}` : number;
12238
+ }
12239
+ return value;
12240
+ }
12241
+ function projectHierarchyRows(rows, rowId, parentId, expanded = []) {
12242
+ const forest = hierarchyForest(rows, rowId, parentId);
12243
+ const open = expanded instanceof Set ? expanded : new Set(expanded);
12244
+ const result = [];
12245
+ const stack = forest.roots.slice().reverse().map((node) => ({ node, depth: 0 }));
12246
+ while (stack.length) {
12247
+ const { node, depth } = stack.pop();
12248
+ result.push({
12249
+ row: node.row,
12250
+ id: node.id,
12251
+ index: node.index,
12252
+ depth,
12253
+ hasChildren: node.children.length > 0,
12254
+ parentId: node.parent?.id ?? null
12255
+ });
12256
+ if (!open.has(node.id)) continue;
12257
+ for (let index = node.children.length - 1; index >= 0; index -= 1) {
12258
+ stack.push({ node: node.children[index], depth: depth + 1 });
12259
+ }
12260
+ }
12261
+ return result;
12262
+ }
12263
+ function sortHierarchyRows(rows, rowId, parentId, getValue, direction) {
12264
+ const forest = hierarchyForest(rows, rowId, parentId);
12265
+ const sorted = (nodes) => sortRows(nodes, (node) => getValue(node.row), direction);
12266
+ const result = [];
12267
+ const stack = sorted(forest.roots).reverse();
12268
+ while (stack.length) {
12269
+ const node = stack.pop();
12270
+ result.push(node.row);
12271
+ const children = sorted(node.children);
12272
+ for (let index = children.length - 1; index >= 0; index -= 1) stack.push(children[index]);
12273
+ }
12274
+ return result;
12275
+ }
11575
12276
  function resolveEditable(column, row) {
11576
12277
  if (!column || typeof column !== "object") return false;
11577
12278
  if (typeof column.editor === "function") {
@@ -11579,7 +12280,7 @@ function resolveEditable(column, row) {
11579
12280
  return "custom";
11580
12281
  }
11581
12282
  const declared = typeof column.editable === "function" ? column.editable(row) : column.editable;
11582
- if (declared === true) return "text";
12283
+ if (declared === true) return isNumericColumn(column) ? "number" : "text";
11583
12284
  if (typeof declared === "string" && EDITOR_TYPES.has(declared)) return (
11584
12285
  /** @type {TableEditorType} */
11585
12286
  declared
@@ -11628,6 +12329,71 @@ function firstValidationError(values, columns, row, fallbackMessage = "Invalid v
11628
12329
  }
11629
12330
  return null;
11630
12331
  }
12332
+ function resolveColumnOption(option, row) {
12333
+ return typeof option === "function" ? option(row) : option;
12334
+ }
12335
+ function resolveColumnDecimals(column, row) {
12336
+ const value = resolveColumnOption(column?.decimals, row);
12337
+ if (value == null) return null;
12338
+ const number = Number(value);
12339
+ return Number.isFinite(number) ? Math.max(0, Math.trunc(number)) : null;
12340
+ }
12341
+ function isNumericColumn(column) {
12342
+ return ["number", "currency", "percent", "unit"].includes(column?.type);
12343
+ }
12344
+ function hierarchyForest(rows, rowId, parentId) {
12345
+ const nodes = (Array.isArray(rows) ? rows : []).map((row, index) => ({
12346
+ row,
12347
+ id: readRowValue(rowId, row),
12348
+ index,
12349
+ parent: null,
12350
+ children: []
12351
+ }));
12352
+ const byId = /* @__PURE__ */ new Map();
12353
+ for (const node of nodes) if (!byId.has(node.id)) byId.set(node.id, node);
12354
+ const parentOf = /* @__PURE__ */ new Map();
12355
+ for (const node of nodes) {
12356
+ const parent = byId.get(readRowValue(parentId, node.row)) ?? null;
12357
+ parentOf.set(node, parent === node ? null : parent);
12358
+ }
12359
+ for (const node of nodes) {
12360
+ const seen = /* @__PURE__ */ new Set([node]);
12361
+ let cursor = parentOf.get(node) ?? null;
12362
+ while (cursor) {
12363
+ if (seen.has(cursor)) {
12364
+ parentOf.set(cursor, null);
12365
+ break;
12366
+ }
12367
+ seen.add(cursor);
12368
+ cursor = parentOf.get(cursor) ?? null;
12369
+ }
12370
+ }
12371
+ const roots = [];
12372
+ for (const node of nodes) {
12373
+ node.parent = parentOf.get(node) ?? null;
12374
+ if (node.parent) node.parent.children.push(node);
12375
+ else roots.push(node);
12376
+ }
12377
+ return { roots, nodes };
12378
+ }
12379
+ function hierarchyBranchIds(rows, rowId, parentId) {
12380
+ return hierarchyForest(rows, rowId, parentId).nodes.filter((node) => node.children.length > 0).map((node) => node.id);
12381
+ }
12382
+ function readRowValue(accessor, row) {
12383
+ return typeof accessor === "function" ? accessor(row) : row?.[accessor];
12384
+ }
12385
+ function normalizeHierarchy(option, columns) {
12386
+ if (!option || typeof option !== "object") return null;
12387
+ const parentId = option.parentId;
12388
+ if (typeof parentId !== "string" && typeof parentId !== "function") return null;
12389
+ const requested = typeof option.column === "string" ? option.column : null;
12390
+ const column = columns.some((candidate) => candidate.id === requested) ? requested : columns[0]?.id;
12391
+ if (!column) return null;
12392
+ return { ...option, parentId, column };
12393
+ }
12394
+ function initialExpanded(option) {
12395
+ return new Set(Array.isArray(option) ? option : []);
12396
+ }
11631
12397
  function normalizeEditMode(mode) {
11632
12398
  if (mode === "cell" || mode === "row") return mode;
11633
12399
  return mode === true ? "cell" : false;
@@ -12094,6 +12860,29 @@ var MenuButton = class extends Component {
12094
12860
  isOpen() {
12095
12861
  return this.#dropdown.isOpen();
12096
12862
  }
12863
+ /** Returns the trigger button. @returns {HTMLElement} */
12864
+ getTrigger() {
12865
+ return (
12866
+ /** @type {HTMLElement} */
12867
+ this.el
12868
+ );
12869
+ }
12870
+ /** Returns the owned menu panel. @returns {HTMLElement} */
12871
+ getPanel() {
12872
+ return this.#panel;
12873
+ }
12874
+ /** Opens the menu and focuses its first enabled item. @returns {this} */
12875
+ focusFirst() {
12876
+ this.open();
12877
+ this.#roving.focusFirst();
12878
+ return this;
12879
+ }
12880
+ /** Opens the menu and focuses its last enabled item. @returns {this} */
12881
+ focusLast() {
12882
+ this.open();
12883
+ this.#roving.focusLast();
12884
+ return this;
12885
+ }
12097
12886
  /**
12098
12887
  * Sets the visible trigger label.
12099
12888
  * @param {string} label Trigger label.
@@ -12153,7 +12942,8 @@ var MenuButton = class extends Component {
12153
12942
  const index = Number(element.getAttribute("data-menu-item"));
12154
12943
  const item = this.#items[index];
12155
12944
  if (!item || item === "-" || item.disabled) return;
12156
- this.emit("select", { value: item.value, item });
12945
+ const selected = this.emit("select", { value: item.value, item });
12946
+ if (selected.defaultPrevented) return;
12157
12947
  try {
12158
12948
  item.onselect?.(item.value, item, this);
12159
12949
  } finally {
@@ -13403,177 +14193,6 @@ function restore9(element, state) {
13403
14193
  element.replaceChildren(...state.children);
13404
14194
  }
13405
14195
 
13406
- // src/components/permission/permission.js
13407
- var Permission = class extends Component {
13408
- static cssName = "permission";
13409
- /** @type {PermissionOptions} */
13410
- static defaults = {
13411
- value: true,
13412
- groups: [],
13413
- groupsValueKey: "ID",
13414
- groupsLabelKey: "name"
13415
- };
13416
- /** @returns {HTMLElement} */
13417
- render() {
13418
- this._mode = "public";
13419
- this._groupValue = null;
13420
- this._groupSelect = null;
13421
- this._createdRoot = false;
13422
- this._originalChildren = [];
13423
- this._root = null;
13424
- this._originalState = null;
13425
- this._createdRoot = this.el === null;
13426
- const root = (
13427
- /** @type {HTMLElement} */
13428
- this.el ?? h("div")
13429
- );
13430
- this.el = root;
13431
- this._root = root;
13432
- if (!this._createdRoot) {
13433
- this._originalChildren = Array.from(root.childNodes);
13434
- this._originalState = root.getAttribute("data-state");
13435
- }
13436
- const name = uid("zx-permission");
13437
- const choices = h("div", {
13438
- class: "zx-permission__choices",
13439
- role: "radiogroup",
13440
- ariaLabel: this._message("permission.label", "Permission")
13441
- });
13442
- const privateChoice = this._choice(name, "private", this._message("permission.private", "Private"));
13443
- const publicChoice = this._choice(name, "public", this._message("permission.public", "Public"));
13444
- const groupChoice = this._choice(name, "group", this._message("permission.group", "Group"));
13445
- const groupHost = h("div", { ref: "groupHost", class: "zx-permission__groups" });
13446
- choices.append(privateChoice.label, publicChoice.label, groupChoice.label, groupHost);
13447
- root.replaceChildren(choices);
13448
- this.refs.private = privateChoice.input;
13449
- this.refs.public = publicChoice.input;
13450
- this.refs.group = groupChoice.input;
13451
- this._groupSelect = new Select(groupHost, {
13452
- items: Array.isArray(this.options.groups) ? this.options.groups : [],
13453
- valueKey: this.options.groupsValueKey,
13454
- labelKey: this.options.groupsLabelKey,
13455
- placeholder: this._message("permission.group", "Group")
13456
- });
13457
- this._groupSelect.refs.input.setAttribute(
13458
- "aria-label",
13459
- this._message("permission.group", "Group")
13460
- );
13461
- for (const input of [privateChoice.input, publicChoice.input, groupChoice.input]) {
13462
- this.listen(input, "change", () => {
13463
- if (!input.checked) return;
13464
- if (input.value === "group") this._selectGroupMode();
13465
- else this.set(input.value);
13466
- });
13467
- }
13468
- this.listen(groupHost, "zx-change", (event) => {
13469
- event.stopPropagation();
13470
- if (this._mode !== "group") return;
13471
- const previous = this.get();
13472
- this._groupValue = /** @type {CustomEvent} */
13473
- event.detail.value;
13474
- const value = this.get();
13475
- if (!Object.is(previous, value)) this.emit("change", { value });
13476
- });
13477
- this.set(this.options.value, { silent: true });
13478
- return root;
13479
- }
13480
- /** Returns the normalized permission value. @returns {'private'|'public'|unknown} */
13481
- get() {
13482
- if (this._mode === "private") return "private";
13483
- if (this._mode === "public") return "public";
13484
- return this._groupValue;
13485
- }
13486
- /**
13487
- * Sets a public, private, or group permission.
13488
- * @param {boolean|string|number|symbol|bigint|null} value Permission value.
13489
- * @param {{silent?: boolean}} [options={}] Update behavior.
13490
- * @returns {this}
13491
- * @fires Permission#change
13492
- */
13493
- set(value, { silent = false } = {}) {
13494
- const previous = this.get();
13495
- if (value === true || value === "public") {
13496
- this._mode = "public";
13497
- } else if (value === false || value === "private") {
13498
- this._mode = "private";
13499
- } else {
13500
- this._mode = "group";
13501
- this._groupValue = value;
13502
- this._groupSelect?.set(value, { silent: true });
13503
- }
13504
- this._sync();
13505
- const next = this.get();
13506
- if (!silent && !Object.is(previous, next)) this.emit("change", { value: next });
13507
- return this;
13508
- }
13509
- /** Destroys the embedded Select and restores an enhanced target. @returns {void} */
13510
- destroy() {
13511
- this._groupSelect?.destroy();
13512
- this._groupSelect = null;
13513
- if (!this._createdRoot && this.el) {
13514
- this.el.replaceChildren(...this._originalChildren);
13515
- if (this._originalState === null) this.el.removeAttribute("data-state");
13516
- else this.el.setAttribute("data-state", this._originalState);
13517
- }
13518
- super.destroy();
13519
- }
13520
- /** @returns {void} */
13521
- _selectGroupMode() {
13522
- const previous = this.get();
13523
- this._mode = "group";
13524
- if (this._groupValue == null) {
13525
- const first = Array.isArray(this.options.groups) ? this.options.groups[0] : null;
13526
- this._groupValue = first == null ? null : read(first, this.options.groupsValueKey);
13527
- }
13528
- this._groupSelect?.set(this._groupValue, { silent: true });
13529
- this._sync();
13530
- const next = this.get();
13531
- if (!Object.is(previous, next)) this.emit("change", { value: next });
13532
- }
13533
- /** @returns {void} */
13534
- _sync() {
13535
- this.refs.private.checked = this._mode === "private";
13536
- this.refs.public.checked = this._mode === "public";
13537
- this.refs.group.checked = this._mode === "group";
13538
- if (this._mode === "group") this._groupSelect?.enable();
13539
- else this._groupSelect?.disable();
13540
- this._root?.setAttribute("data-state", this._mode);
13541
- }
13542
- /**
13543
- * @param {string} name
13544
- * @param {'private'|'public'|'group'} value
13545
- * @param {string} text
13546
- * @returns {{label: HTMLLabelElement, input: HTMLInputElement}}
13547
- */
13548
- _choice(name, value, text3) {
13549
- const input = (
13550
- /** @type {HTMLInputElement} */
13551
- h("input", {
13552
- type: "radio",
13553
- name,
13554
- value
13555
- })
13556
- );
13557
- const label = (
13558
- /** @type {HTMLLabelElement} */
13559
- h("label", {
13560
- class: "zx-permission__choice"
13561
- }, input, h("span", {}, text3))
13562
- );
13563
- return { label, input };
13564
- }
13565
- /** @param {string} key @param {string} fallback @returns {string} */
13566
- _message(key, fallback) {
13567
- const message = this.msg(key);
13568
- return message === key ? fallback : message;
13569
- }
13570
- };
13571
- function read(item, reader) {
13572
- if (typeof reader === "function") return reader(item);
13573
- if (item === null || typeof item !== "object") return item;
13574
- return item[reader];
13575
- }
13576
-
13577
14196
  // src/components/data-filter/filter-core.js
13578
14197
  function normalizeText(value) {
13579
14198
  return String(value ?? "").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLocaleLowerCase();
@@ -15157,9 +15776,9 @@ function translateChecklistOptions(options = {}) {
15157
15776
  delete translated.onClick;
15158
15777
  return translated;
15159
15778
  }
15160
- function translateDateboxFormat(format) {
15161
- if (typeof format === "string") return format.replaceAll("&nbsp;", " ");
15162
- const parts = Array.isArray(format) ? format : ["d", ".", "M", ".", "y"];
15779
+ function translateDateboxFormat(format2) {
15780
+ if (typeof format2 === "string") return format2.replaceAll("&nbsp;", " ");
15781
+ const parts = Array.isArray(format2) ? format2 : ["d", ".", "M", ".", "y"];
15163
15782
  const tokens = {
15164
15783
  d: "%d",
15165
15784
  m: "%m",
@@ -15347,26 +15966,39 @@ var Checklist2 = class extends ChecklistBase {
15347
15966
  var BootstrapChecklist = class extends ChecklistBase {
15348
15967
  static legacyName = "gx.bootstrap.Checklist";
15349
15968
  };
15350
- var Permission2 = class extends GxWrapper {
15969
+ function toLegacyPermission(value) {
15970
+ if (value === false) return "private";
15971
+ if (value === true) return "public";
15972
+ return value;
15973
+ }
15974
+ function fromLegacyPermission(value) {
15975
+ if (value === false || value === "private") return false;
15976
+ if (value === true || value === "public") return true;
15977
+ return value;
15978
+ }
15979
+ var Permission = class extends GxWrapper {
15351
15980
  static legacyName = "gx.zeyos.Permission";
15352
15981
  /** @param {Element|string|null} display @param {Record<string, any>} [options={}] */
15353
15982
  constructor(display, options = {}) {
15354
15983
  super(options, { value: true, groups: [] });
15355
- const component = new Permission(display, {
15356
- value: options.value ?? true,
15984
+ const component = Select.permission(display, {
15985
+ value: fromLegacyPermission(options.value ?? true),
15357
15986
  groups: options.groups ?? options.data ?? [],
15358
- groupsValueKey: options.groupsValueKey ?? options.elementIndex ?? "ID",
15359
- groupsLabelKey: options.groupsLabelKey ?? options.elementLabel ?? "name"
15987
+ valueKey: options.groupsValueKey ?? options.elementIndex ?? "ID",
15988
+ labelKey: options.groupsLabelKey ?? options.elementLabel ?? "name"
15989
+ });
15990
+ this._attach(component, {
15991
+ events: { change: { type: "change", args: (detail) => [toLegacyPermission(detail.value)] } },
15992
+ setters: { value: "set" }
15360
15993
  });
15361
- this._attach(component, { events: { change: { type: "change", args: (detail) => [detail.value] } }, setters: { value: "set" } });
15362
15994
  }
15363
15995
  /** @returns {unknown} */
15364
15996
  get() {
15365
- return this._zx.get();
15997
+ return toLegacyPermission(this._zx.value);
15366
15998
  }
15367
15999
  /** @param {unknown} value @param {boolean} [silent=false] @returns {this} */
15368
16000
  set(value, silent = false) {
15369
- this._zx.set(value, { silent });
16001
+ this._zx.set(fromLegacyPermission(value), { silent });
15370
16002
  return this;
15371
16003
  }
15372
16004
  };
@@ -15599,11 +16231,11 @@ var Datebox2 = class extends GxWrapper {
15599
16231
  constructor(display, options = {}) {
15600
16232
  super(options, { timestamp: 0, unit: "milliseconds", format: ["d", ".", "M", ".", "y", " ", "h", ":", "i"] });
15601
16233
  const initial = options.timestamp == null || Number(options.timestamp) === 0 ? /* @__PURE__ */ new Date() : legacyDate(options.timestamp, options.unit ?? "milliseconds");
15602
- const format = translateDateboxFormat(options.format);
16234
+ const format2 = translateDateboxFormat(options.format);
15603
16235
  const component = new Datebox(display, {
15604
16236
  value: initial,
15605
- format,
15606
- time: /%[HMS]/.test(format),
16237
+ format: format2,
16238
+ time: /%[HMS]/.test(format2),
15607
16239
  disabled: Boolean(options.disabled)
15608
16240
  });
15609
16241
  this._attach(component, {
@@ -15669,13 +16301,13 @@ var PickerBase = class extends GxWrapper {
15669
16301
  return this;
15670
16302
  }
15671
16303
  /** @param {string} [format] @returns {Date|number|string|null} */
15672
- get(format = this.options.return_format) {
16304
+ get(format2 = this.options.return_format) {
15673
16305
  const date = this._readDate();
15674
16306
  if (!date) return null;
15675
- if (format == null) return date;
15676
- if (format === "%s" || format === "seconds") return Math.floor(date.getTime() / 1e3);
15677
- if (format === "date") return date;
15678
- return formatDate(date, String(format));
16307
+ if (format2 == null) return date;
16308
+ if (format2 === "%s" || format2 === "seconds") return Math.floor(date.getTime() / 1e3);
16309
+ if (format2 === "date") return date;
16310
+ return formatDate(date, String(format2));
15679
16311
  }
15680
16312
  /** @returns {number|null} */
15681
16313
  getSeconds() {
@@ -15688,8 +16320,8 @@ var PickerBase = class extends GxWrapper {
15688
16320
  return this;
15689
16321
  }
15690
16322
  /** @param {string} format @param {boolean} [enableTimePicker] @returns {this} */
15691
- setFormat(format, enableTimePicker) {
15692
- this.options.format = format;
16323
+ setFormat(format2, enableTimePicker) {
16324
+ this.options.format = format2;
15693
16325
  this.options.timePicker = enableTimePicker;
15694
16326
  return this;
15695
16327
  }
@@ -17154,7 +17786,7 @@ var gx = {
17154
17786
  SelectDyn,
17155
17787
  SelectPrio,
17156
17788
  Checklist: Checklist2,
17157
- Permission: Permission2,
17789
+ Permission,
17158
17790
  Table: Table2,
17159
17791
  Tabbox: Tabbox2,
17160
17792
  Panel: Panel2,