@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
@@ -136,12 +136,12 @@ function registerFieldType(typeOrFormat, strategy) {
136
136
  function fieldToZxField(meta) {
137
137
  const normalized = normalizeMeta(meta);
138
138
  const strategy = strategyFor(normalized);
139
- const fragment = strategyPart(strategy.field, normalized);
139
+ const fragment2 = strategyPart(strategy.field, normalized);
140
140
  const descriptor = {
141
141
  id: normalized.id,
142
142
  type: "text",
143
143
  label: fieldLabel(normalized),
144
- ...fragment
144
+ ...fragment2
145
145
  };
146
146
  if (normalized.description != null) descriptor.description = String(normalized.description);
147
147
  if (normalized.required === true || normalized.nullable === false) descriptor.required = true;
@@ -2970,6 +2970,14 @@ var MonthPicker = class extends DatePicker {
2970
2970
  static cssName = "month-picker";
2971
2971
  /** @type {MonthPickerOptions} */
2972
2972
  static defaults = { value: null, min: null, max: null };
2973
+ /**
2974
+ * Creates a month picker with its narrower month-based option contract.
2975
+ * @param {Element|string|null} target Existing element, selector, or null.
2976
+ * @param {MonthPickerOptions} [options={}] Month-picker options.
2977
+ */
2978
+ constructor(target, options = {}) {
2979
+ super(target, options);
2980
+ }
2973
2981
  /** @returns {HTMLElement} */
2974
2982
  render() {
2975
2983
  const root = (
@@ -3189,7 +3197,13 @@ var PLACEMENTS = /* @__PURE__ */ new Set([
3189
3197
  "top-start",
3190
3198
  "top-end",
3191
3199
  "bottom",
3192
- "top"
3200
+ "top",
3201
+ "right-start",
3202
+ "right-end",
3203
+ "left-start",
3204
+ "left-end",
3205
+ "right",
3206
+ "left"
3193
3207
  ]);
3194
3208
  function position(anchor, floating, options = {}) {
3195
3209
  if (!(anchor instanceof Element) || !(floating instanceof HTMLElement)) {
@@ -3217,13 +3231,15 @@ function position(anchor, floating, options = {}) {
3217
3231
  floating.style.inset = "auto";
3218
3232
  floating.style.margin = "0";
3219
3233
  floating.style.setProperty("position-anchor", anchorName);
3220
- floating.style.setProperty("position-area", positionArea(placement));
3221
- floating.style.setProperty("inset-area", positionArea(placement));
3234
+ const area = positionArea(placement, getComputedStyle(anchor).direction === "rtl");
3235
+ floating.style.setProperty("position-area", area);
3236
+ floating.style.setProperty("inset-area", area);
3222
3237
  floating.style.setProperty(
3223
3238
  "position-try-fallbacks",
3224
3239
  flip ? "flip-block, flip-inline, flip-block flip-inline" : "none"
3225
3240
  );
3226
- floating.style.setProperty(placement.startsWith("top") ? "margin-block-end" : "margin-block-start", `${offset}px`);
3241
+ const margin = placement.startsWith("top") ? "margin-bottom" : placement.startsWith("bottom") ? "margin-top" : placement.startsWith("left") ? "margin-right" : "margin-left";
3242
+ floating.style.setProperty(margin, `${offset}px`);
3227
3243
  update = () => {
3228
3244
  if (matchWidth) floating.style.minWidth = `${anchor.getBoundingClientRect().width}px`;
3229
3245
  };
@@ -3267,14 +3283,20 @@ function position(anchor, floating, options = {}) {
3267
3283
  }
3268
3284
  };
3269
3285
  }
3270
- function positionArea(placement) {
3286
+ function positionArea(placement, rtl) {
3271
3287
  return {
3272
- "bottom-start": "block-end span-inline-end",
3273
- "bottom-end": "block-end span-inline-start",
3274
- "top-start": "block-start span-inline-end",
3275
- "top-end": "block-start span-inline-start",
3276
- bottom: "block-end",
3277
- top: "block-start"
3288
+ "bottom-start": rtl ? "bottom span-left" : "bottom span-right",
3289
+ "bottom-end": rtl ? "bottom span-right" : "bottom span-left",
3290
+ "top-start": rtl ? "top span-left" : "top span-right",
3291
+ "top-end": rtl ? "top span-right" : "top span-left",
3292
+ "right-start": "right span-bottom",
3293
+ "right-end": "right span-top",
3294
+ "left-start": "left span-bottom",
3295
+ "left-end": "left span-top",
3296
+ bottom: "bottom",
3297
+ top: "top",
3298
+ right: "right",
3299
+ left: "left"
3278
3300
  }[placement];
3279
3301
  }
3280
3302
  function updateFallback(anchor, floating, { placement, offset, flip, matchWidth }) {
@@ -3284,26 +3306,54 @@ function updateFallback(anchor, floating, { placement, offset, flip, matchWidth
3284
3306
  const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
3285
3307
  const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
3286
3308
  let actualPlacement = placement;
3287
- const prefersBottom = placement.startsWith("bottom");
3309
+ const side = placementSide(placement);
3288
3310
  const bottomY = anchorRect.bottom + offset;
3289
3311
  const topY = anchorRect.top - offset - floatingRect.height;
3312
+ const rightX = anchorRect.right + offset;
3313
+ const leftX = anchorRect.left - offset - floatingRect.width;
3290
3314
  if (flip) {
3291
- if (prefersBottom && bottomY + floatingRect.height > viewportHeight && topY >= 0) {
3292
- actualPlacement = /** @type {typeof placement} */
3293
- placement.replace("bottom", "top");
3294
- } else if (!prefersBottom && topY < 0 && bottomY + floatingRect.height <= viewportHeight) {
3295
- actualPlacement = /** @type {typeof placement} */
3296
- placement.replace("top", "bottom");
3315
+ if (side === "bottom" && bottomY + floatingRect.height > viewportHeight && topY >= 0) {
3316
+ actualPlacement = flipPlacement(placement, "bottom", "top");
3317
+ } else if (side === "top" && topY < 0 && bottomY + floatingRect.height <= viewportHeight) {
3318
+ actualPlacement = flipPlacement(placement, "top", "bottom");
3319
+ } else if (side === "right" && rightX + floatingRect.width > viewportWidth && leftX >= 0) {
3320
+ actualPlacement = flipPlacement(placement, "right", "left");
3321
+ } else if (side === "left" && leftX < 0 && rightX + floatingRect.width <= viewportWidth) {
3322
+ actualPlacement = flipPlacement(placement, "left", "right");
3297
3323
  }
3298
3324
  }
3325
+ const actualSide = placementSide(actualPlacement);
3299
3326
  let left;
3300
- if (actualPlacement.endsWith("-end")) left = anchorRect.right - floatingRect.width;
3301
- else if (actualPlacement.endsWith("-start")) left = anchorRect.left;
3302
- else left = anchorRect.left + (anchorRect.width - floatingRect.width) / 2;
3303
- const top = actualPlacement.startsWith("bottom") ? bottomY : topY;
3327
+ let top;
3328
+ if (actualSide === "bottom" || actualSide === "top") {
3329
+ const rtl = getComputedStyle(anchor).direction === "rtl";
3330
+ if (actualPlacement.endsWith("-end")) {
3331
+ left = rtl ? anchorRect.left : anchorRect.right - floatingRect.width;
3332
+ } else if (actualPlacement.endsWith("-start")) {
3333
+ left = rtl ? anchorRect.right - floatingRect.width : anchorRect.left;
3334
+ } else left = anchorRect.left + (anchorRect.width - floatingRect.width) / 2;
3335
+ top = actualSide === "bottom" ? bottomY : topY;
3336
+ } else {
3337
+ left = actualSide === "right" ? rightX : leftX;
3338
+ if (actualPlacement.endsWith("-end")) top = anchorRect.bottom - floatingRect.height;
3339
+ else if (actualPlacement.endsWith("-start")) top = anchorRect.top;
3340
+ else top = anchorRect.top + (anchorRect.height - floatingRect.height) / 2;
3341
+ }
3304
3342
  floating.style.left = `${Math.round(clamp(left, 0, Math.max(0, viewportWidth - floatingRect.width)))}px`;
3305
3343
  floating.style.top = `${Math.round(clamp(top, 0, Math.max(0, viewportHeight - floatingRect.height)))}px`;
3306
3344
  }
3345
+ function placementSide(placement) {
3346
+ return (
3347
+ /** @type {'top'|'bottom'|'left'|'right'} */
3348
+ placement.split("-")[0]
3349
+ );
3350
+ }
3351
+ function flipPlacement(placement, from, to) {
3352
+ return (
3353
+ /** @type {Placement} */
3354
+ placement.replace(from, to)
3355
+ );
3356
+ }
3307
3357
  function showPopover(element) {
3308
3358
  if (typeof element.showPopover !== "function") return;
3309
3359
  try {
@@ -4136,31 +4186,31 @@ var PLACEHOLDER_TOKENS = {
4136
4186
  "%Y": "YYYY",
4137
4187
  "%y": "YY"
4138
4188
  };
4139
- function parseRangeText(text3, format, separator) {
4189
+ function parseRangeText(text3, format2, separator) {
4140
4190
  const source = String(text3 ?? "").trim();
4141
4191
  if (source === "") return { start: null, end: null };
4142
4192
  const raw2 = String(separator ?? " \u2013 ");
4143
4193
  const core = raw2.trim();
4144
- const strict = core !== "" && String(format).includes(core) && /^\s|\s$/.test(raw2);
4194
+ const strict = core !== "" && String(format2).includes(core) && /^\s|\s$/.test(raw2);
4145
4195
  const lead = strict ? "\\s+" : "\\s*";
4146
4196
  const tail = strict ? "(?:\\s+|$)" : "\\s*";
4147
4197
  const parts = core === "" ? [source] : source.split(new RegExp(`${lead}${escapeRegExp(core)}${tail}`));
4148
4198
  if (parts.length > 2) return null;
4149
- const first = parseDate(parts[0].trim(), format);
4199
+ const first = parseDate(parts[0].trim(), format2);
4150
4200
  if (!first) return null;
4151
4201
  if (parts.length === 1) return { start: first, end: new Date(first.getTime()) };
4152
4202
  if (parts[1].trim() === "") return { start: first, end: null };
4153
- const second = parseDate(parts[1].trim(), format);
4203
+ const second = parseDate(parts[1].trim(), format2);
4154
4204
  if (!second) return null;
4155
4205
  return normalizeRange(first, second);
4156
4206
  }
4157
- function formatRangeText(range, format, separator) {
4207
+ function formatRangeText(range, format2, separator) {
4158
4208
  const start = range?.start ?? null;
4159
4209
  const end = range?.end ?? null;
4160
4210
  if (!start && !end) return "";
4161
- if (!end) return formatDate(start, format);
4162
- if (!start) return formatDate(end, format);
4163
- return `${formatDate(start, format)}${separator}${formatDate(end, format)}`;
4211
+ if (!end) return formatDate(start, format2);
4212
+ if (!start) return formatDate(end, format2);
4213
+ return `${formatDate(start, format2)}${separator}${formatDate(end, format2)}`;
4164
4214
  }
4165
4215
  var DateRangeBox = class extends Component {
4166
4216
  static cssName = "date-range-box";
@@ -4818,12 +4868,12 @@ var DateTimeBox = function DateTimeBox2(target, options = {}) {
4818
4868
  function validDate4(value) {
4819
4869
  return value instanceof Date && !Number.isNaN(value.getTime()) ? new Date(value.getTime()) : null;
4820
4870
  }
4821
- function effectiveFormat(format, time) {
4822
- const value = String(format || "%d.%m.%Y");
4871
+ function effectiveFormat(format2, time) {
4872
+ const value = String(format2 || "%d.%m.%Y");
4823
4873
  return time && !/%[HM]/.test(value) ? `${value} %H:%M` : value;
4824
4874
  }
4825
- function placeholderFromFormat(format) {
4826
- return format.replace(/%[demYyHMS]/g, (token) => ({
4875
+ function placeholderFromFormat(format2) {
4876
+ return format2.replace(/%[demYyHMS]/g, (token) => ({
4827
4877
  "%d": "DD",
4828
4878
  "%e": "D",
4829
4879
  "%m": "MM",
@@ -5250,18 +5300,34 @@ function registerFieldUploadAdapter() {
5250
5300
  }
5251
5301
 
5252
5302
  // src/components/multi-value-editor/multi-value-editor.js
5303
+ var REORDER = Object.freeze({
5304
+ both: { drag: true, buttons: true },
5305
+ buttons: { drag: false, buttons: true },
5306
+ drag: { drag: true, buttons: false },
5307
+ none: { drag: false, buttons: false }
5308
+ });
5309
+ var PARTS = Object.freeze({
5310
+ value: ".zx-multi-value-editor__value",
5311
+ handle: ".zx-multi-value-editor__handle",
5312
+ up: '[data-action="up"]',
5313
+ down: '[data-action="down"]'
5314
+ });
5253
5315
  var MultiValueEditor = class extends Component {
5254
5316
  static cssName = "multi-value-editor";
5255
5317
  /** @type {MultiValueEditorOptions} */
5256
- static defaults = { values: [], options: null, addLabel: "Add value" };
5318
+ static defaults = { values: [], options: null, addLabel: "Add value", reorder: "both" };
5257
5319
  /** @returns {HTMLElement} */
5258
5320
  render() {
5259
5321
  const root = (
5260
5322
  /** @type {HTMLElement} */
5261
5323
  this.el ?? h("div")
5262
5324
  );
5325
+ this.el = root;
5263
5326
  this._values = Array.isArray(this.options.values) ? this.options.values.map(String) : [];
5264
5327
  this._ownedNodes = [];
5328
+ this._initialState = root.getAttribute("data-state");
5329
+ this._dragIndex = -1;
5330
+ this._armed = null;
5265
5331
  this._rows = h("div", { class: "zx-multi-value-editor__rows" });
5266
5332
  this._add = h("button", {
5267
5333
  class: "zx-btn zx-multi-value-editor__add",
@@ -5272,6 +5338,17 @@ var MultiValueEditor = class extends Component {
5272
5338
  root.append(this._rows, this._add);
5273
5339
  this.listen(root, "click", (event) => this._handleClick(event));
5274
5340
  this.listen(this._rows, "change", (event) => this._handleChange(event));
5341
+ this.listen(this._rows, "keydown", (event) => this._handleKeydown(event));
5342
+ if (this._reorder().drag) {
5343
+ this.listen(this._rows, "pointerdown", (event) => this._handlePointerDown(event));
5344
+ this.listen(this._rows, "dragstart", (event) => this._handleDragStart(event));
5345
+ this.listen(this._rows, "dragover", (event) => this._handleDragOver(event));
5346
+ this.listen(this._rows, "dragleave", (event) => this._handleDragLeave(event));
5347
+ this.listen(this._rows, "drop", (event) => this._handleDrop(event));
5348
+ this.listen(this._rows, "dragend", () => this._finishDrag());
5349
+ this.listen(document, "pointerup", () => this._disarm());
5350
+ this.listen(document, "pointercancel", () => this._disarm());
5351
+ }
5275
5352
  this._renderRows();
5276
5353
  return root;
5277
5354
  }
@@ -5294,16 +5371,27 @@ var MultiValueEditor = class extends Component {
5294
5371
  if (!silent) this.emit("change", { values: this.getValues() });
5295
5372
  return this;
5296
5373
  }
5297
- /** @param {number} [focusIndex=-1] @returns {void} */
5298
- _renderRows(focusIndex = -1) {
5374
+ /** @returns {{drag: boolean, buttons: boolean}} */
5375
+ _reorder() {
5376
+ return REORDER[this.options.reorder] ?? REORDER.both;
5377
+ }
5378
+ /**
5379
+ * @param {number} [focusIndex=-1] Row to focus after the render, or -1 for none.
5380
+ * @param {keyof PARTS} [focusPart='value'] Which control of that row takes focus.
5381
+ * @returns {void}
5382
+ */
5383
+ _renderRows(focusIndex = -1, focusPart = "value") {
5299
5384
  const choices = normalizeOptions2(this.options.options);
5300
5385
  const useSelect = this.options.options !== null;
5386
+ const { drag, buttons } = this._reorder();
5387
+ const last = this._values.length - 1;
5301
5388
  const rows = this._values.map((value, index) => {
5389
+ const entries = !useSelect || choices.some(([option]) => option === value) ? choices : [[value, value], ...choices];
5302
5390
  const control = useSelect ? h("select", {
5303
5391
  class: "zx-multi-value-editor__value",
5304
5392
  ariaLabel: `Value ${index + 1}`,
5305
5393
  dataset: { index }
5306
- }, choices.map(([optionValue, label]) => h("option", {
5394
+ }, entries.map(([optionValue, label]) => h("option", {
5307
5395
  value: optionValue,
5308
5396
  selected: optionValue === value
5309
5397
  }, label))) : h("input", {
@@ -5316,40 +5404,52 @@ var MultiValueEditor = class extends Component {
5316
5404
  return h(
5317
5405
  "div",
5318
5406
  { class: "zx-multi-value-editor__row", dataset: { row: index } },
5407
+ drag ? h("button", {
5408
+ class: "zx-multi-value-editor__handle",
5409
+ type: "button",
5410
+ ariaLabel: `Reorder value ${index + 1}. Use the arrow keys to move it.`,
5411
+ dataset: { action: "grab", index }
5412
+ }, icon("drag", { size: 12 })) : null,
5319
5413
  control,
5320
5414
  h(
5321
5415
  "div",
5322
5416
  { class: "zx-multi-value-editor__controls" },
5323
- h("button", {
5417
+ buttons ? h("button", {
5324
5418
  class: "zx-btn",
5325
5419
  type: "button",
5326
- disabled: index === 0,
5420
+ // `aria-disabled` rather than `disabled`: the buttons re-render on every move, and a
5421
+ // control that disables itself under the reader's finger drops focus to the document.
5422
+ ariaDisabled: String(index === 0),
5327
5423
  ariaLabel: `Move value ${index + 1} up`,
5328
5424
  dataset: { action: "up", index }
5329
- }, "\u2191"),
5330
- h("button", {
5425
+ }, icon("chevron-up", { size: 12 })) : null,
5426
+ buttons ? h("button", {
5331
5427
  class: "zx-btn",
5332
5428
  type: "button",
5333
- disabled: index === this._values.length - 1,
5429
+ ariaDisabled: String(index === last),
5334
5430
  ariaLabel: `Move value ${index + 1} down`,
5335
5431
  dataset: { action: "down", index }
5336
- }, "\u2193"),
5432
+ }, icon("chevron-down", { size: 12 })) : null,
5337
5433
  h("button", {
5338
5434
  class: "zx-btn",
5339
5435
  type: "button",
5340
5436
  ariaLabel: `Remove value ${index + 1}`,
5341
5437
  dataset: { action: "remove", index }
5342
- }, "\xD7")
5438
+ }, icon("x", { size: 12 }))
5343
5439
  )
5344
5440
  );
5345
5441
  });
5346
5442
  this._rows.replaceChildren(...rows);
5347
- if (focusIndex >= 0) rows[focusIndex]?.querySelector(".zx-multi-value-editor__value")?.focus();
5443
+ if (focusIndex >= 0) {
5444
+ const row = rows[Math.min(focusIndex, rows.length - 1)];
5445
+ row?.querySelector(PARTS[focusPart] ?? PARTS.value)?.focus();
5446
+ }
5348
5447
  }
5349
5448
  /** @param {MouseEvent} event @returns {void} */
5350
5449
  _handleClick(event) {
5351
5450
  const button = event.target instanceof Element ? event.target.closest("[data-action]") : null;
5352
5451
  if (!button || !this.el.contains(button)) return;
5452
+ if (button.getAttribute("aria-disabled") === "true") return;
5353
5453
  const action = button.dataset.action;
5354
5454
  const index = Number(button.dataset.index);
5355
5455
  if (action === "add") {
@@ -5362,28 +5462,156 @@ var MultiValueEditor = class extends Component {
5362
5462
  this._renderRows(Math.min(index, this._values.length - 1));
5363
5463
  this._emitChange();
5364
5464
  } else if ((action === "up" || action === "down") && Number.isInteger(index)) {
5365
- const target = index + (action === "up" ? -1 : 1);
5366
- if (target < 0 || target >= this._values.length) return;
5367
- [this._values[index], this._values[target]] = [this._values[target], this._values[index]];
5368
- this._renderRows(target);
5369
- this._emitChange();
5465
+ this._move(index, index + (action === "up" ? -1 : 1), action);
5370
5466
  }
5371
5467
  }
5372
5468
  /** @param {Event} event @returns {void} */
5373
5469
  _handleChange(event) {
5374
5470
  const control = event.target instanceof Element ? event.target.closest("[data-index]") : null;
5375
- if (!control) return;
5471
+ if (!control || !("value" in control)) return;
5376
5472
  const index = Number(control.dataset.index);
5377
5473
  if (!Number.isInteger(index) || index < 0 || index >= this._values.length) return;
5378
5474
  this._values[index] = control.value;
5379
5475
  this._emitChange();
5380
5476
  }
5477
+ /**
5478
+ * The handle is the keyboard equivalent of dragging it, so `reorder: 'drag'` is still operable
5479
+ * without a pointer.
5480
+ * @param {KeyboardEvent} event
5481
+ * @returns {void}
5482
+ */
5483
+ _handleKeydown(event) {
5484
+ const handle = event.target instanceof Element ? event.target.closest(".zx-multi-value-editor__handle") : null;
5485
+ if (!handle) return;
5486
+ const index = Number(handle.dataset.index);
5487
+ const target = event.key === "ArrowUp" ? index - 1 : event.key === "ArrowDown" ? index + 1 : event.key === "Home" ? 0 : event.key === "End" ? this._values.length - 1 : -1;
5488
+ if (target < 0 || target >= this._values.length) return;
5489
+ event.preventDefault();
5490
+ this._move(index, target, "handle");
5491
+ }
5492
+ /**
5493
+ * Arms the row for dragging. Only the handle does this, so text selection inside a row's input
5494
+ * keeps working and a stray drag cannot reorder anything.
5495
+ * @param {PointerEvent} event
5496
+ * @returns {void}
5497
+ */
5498
+ _handlePointerDown(event) {
5499
+ const handle = event.target instanceof Element ? event.target.closest(".zx-multi-value-editor__handle") : null;
5500
+ this._disarm();
5501
+ if (!handle || handle.disabled) return;
5502
+ const row = (
5503
+ /** @type {HTMLElement|null} */
5504
+ handle.closest(".zx-multi-value-editor__row")
5505
+ );
5506
+ if (!row) return;
5507
+ row.draggable = true;
5508
+ this._armed = row;
5509
+ }
5510
+ /** @returns {void} */
5511
+ _disarm() {
5512
+ if (this._armed) this._armed.draggable = false;
5513
+ this._armed = null;
5514
+ }
5515
+ /** @param {DragEvent} event @returns {void} */
5516
+ _handleDragStart(event) {
5517
+ const row = event.target instanceof Element ? event.target.closest(".zx-multi-value-editor__row") : null;
5518
+ if (!row || row !== this._armed) {
5519
+ event.preventDefault();
5520
+ return;
5521
+ }
5522
+ this._dragIndex = Number(row.dataset.row);
5523
+ row.dataset.dragging = "true";
5524
+ this.el.setAttribute("data-state", "dragging");
5525
+ event.dataTransfer?.setData("text/plain", String(this._dragIndex));
5526
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
5527
+ }
5528
+ /** @param {DragEvent} event @returns {void} */
5529
+ _handleDragOver(event) {
5530
+ if (this._dragIndex < 0) return;
5531
+ event.preventDefault();
5532
+ if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
5533
+ this._showDrop(this._dropTarget(event.clientY));
5534
+ }
5535
+ /** @param {DragEvent} event @returns {void} */
5536
+ _handleDragLeave(event) {
5537
+ const to = event.relatedTarget;
5538
+ if (to instanceof Node && this._rows.contains(to)) return;
5539
+ this._clearDrop();
5540
+ }
5541
+ /** @param {DragEvent} event @returns {void} */
5542
+ _handleDrop(event) {
5543
+ if (this._dragIndex < 0) return;
5544
+ event.preventDefault();
5545
+ const from = this._dragIndex;
5546
+ const slot = this._dropTarget(event.clientY);
5547
+ this._finishDrag();
5548
+ this._move(from, slot > from ? slot - 1 : slot, "handle");
5549
+ }
5550
+ /** @returns {void} */
5551
+ _finishDrag() {
5552
+ this._dragIndex = -1;
5553
+ this._disarm();
5554
+ this._clearDrop();
5555
+ for (const row of this._rows.children) delete /** @type {HTMLElement} */
5556
+ row.dataset.dragging;
5557
+ if (this._initialState === null) this.el.removeAttribute("data-state");
5558
+ else this.el.setAttribute("data-state", this._initialState);
5559
+ }
5560
+ /**
5561
+ * Insertion slot (0…length) for a pointer at `clientY`.
5562
+ * @param {number} clientY Pointer position.
5563
+ * @returns {number}
5564
+ */
5565
+ _dropTarget(clientY) {
5566
+ const rows = [...this._rows.children];
5567
+ for (const [index, row] of rows.entries()) {
5568
+ const box = row.getBoundingClientRect();
5569
+ if (clientY < box.top + box.height / 2) return index;
5570
+ }
5571
+ return rows.length;
5572
+ }
5573
+ /** @param {number} slot Insertion slot to mark. @returns {void} */
5574
+ _showDrop(slot) {
5575
+ const rows = [...this._rows.children];
5576
+ const inert = slot === this._dragIndex || slot === this._dragIndex + 1;
5577
+ for (const [index, row] of rows.entries()) {
5578
+ const cell = (
5579
+ /** @type {HTMLElement} */
5580
+ row
5581
+ );
5582
+ if (!inert && index === slot) cell.dataset.drop = "before";
5583
+ else if (!inert && slot === rows.length && index === rows.length - 1) cell.dataset.drop = "after";
5584
+ else delete cell.dataset.drop;
5585
+ }
5586
+ }
5587
+ /** @returns {void} */
5588
+ _clearDrop() {
5589
+ for (const row of this._rows.children) delete /** @type {HTMLElement} */
5590
+ row.dataset.drop;
5591
+ }
5592
+ /**
5593
+ * @param {number} from Index to move.
5594
+ * @param {number} to Destination index.
5595
+ * @param {keyof PARTS} part Control of the moved row that takes focus.
5596
+ * @returns {boolean} Whether anything moved.
5597
+ */
5598
+ _move(from, to, part) {
5599
+ const size = this._values.length;
5600
+ if (from === to || from < 0 || to < 0 || from >= size || to >= size) return false;
5601
+ const [value] = this._values.splice(from, 1);
5602
+ this._values.splice(to, 0, value);
5603
+ this._renderRows(to, part);
5604
+ this._emitChange();
5605
+ return true;
5606
+ }
5381
5607
  /** @returns {void} */
5382
5608
  _emitChange() {
5383
5609
  this.emit("change", { values: this.getValues() });
5384
5610
  }
5385
5611
  /** @returns {void} */
5386
5612
  destroy() {
5613
+ if (this._initialState === null) this.el?.removeAttribute("data-state");
5614
+ else this.el?.setAttribute("data-state", this._initialState);
5387
5615
  for (const node of this._ownedNodes ?? []) node.remove();
5388
5616
  super.destroy();
5389
5617
  }
@@ -6311,6 +6539,7 @@ var Select = class _Select extends Component {
6311
6539
  /** @type {SelectOptions} */
6312
6540
  static defaults = {
6313
6541
  items: [],
6542
+ fixedItems: [],
6314
6543
  valueKey: "ID",
6315
6544
  labelKey: "name",
6316
6545
  renderItem: null,
@@ -6330,6 +6559,8 @@ var Select = class _Select extends Component {
6330
6559
  render() {
6331
6560
  this._items = [];
6332
6561
  this._visibleItems = [];
6562
+ this._fixedItems = [];
6563
+ this._visibleFixed = [];
6333
6564
  this._selected = null;
6334
6565
  this._optionEntries = [];
6335
6566
  this._activeIndex = -1;
@@ -6413,6 +6644,8 @@ var Select = class _Select extends Component {
6413
6644
  root.replaceChildren(control, panel);
6414
6645
  this._items = Array.isArray(this.options.items) ? this.options.items.slice() : [];
6415
6646
  this._visibleItems = this._items.slice();
6647
+ this._fixedItems = Array.isArray(this.options.fixedItems) ? this.options.fixedItems.slice() : [];
6648
+ this._visibleFixed = this._fixedItems.slice();
6416
6649
  this._disabled = Boolean(this.options.disabled);
6417
6650
  this._handleTypeahead = typeahead(
6418
6651
  () => this._optionEntries.filter((entry) => !entry.clear).map((entry) => entry.element),
@@ -6489,7 +6722,22 @@ var Select = class _Select extends Component {
6489
6722
  const selectedValue = this.value;
6490
6723
  this._items = Array.isArray(items) ? items.slice() : [];
6491
6724
  this._visibleItems = this._items.slice();
6492
- this._selected = selectedValue == null ? null : this._items.find((item) => Object.is(this._itemValue(item), selectedValue)) ?? null;
6725
+ this._selected = selectedValue == null ? null : this._findItem(selectedValue);
6726
+ this._syncInputValue();
6727
+ this._renderOptions();
6728
+ this._position?.update();
6729
+ return this;
6730
+ }
6731
+ /**
6732
+ * Replaces the pinned choices and preserves selection by ID when possible.
6733
+ * @param {SelectItem[]} items New fixed items.
6734
+ * @returns {this}
6735
+ */
6736
+ setFixedItems(items) {
6737
+ const selectedValue = this.value;
6738
+ this._fixedItems = Array.isArray(items) ? items.slice() : [];
6739
+ this._visibleFixed = this._fixedItems.slice();
6740
+ this._selected = selectedValue == null ? null : this._findItem(selectedValue);
6493
6741
  this._syncInputValue();
6494
6742
  this._renderOptions();
6495
6743
  this._position?.update();
@@ -6543,6 +6791,7 @@ var Select = class _Select extends Component {
6543
6791
  this.refs.input.removeAttribute("aria-activedescendant");
6544
6792
  this._activeIndex = -1;
6545
6793
  this._visibleItems = this._items.slice();
6794
+ this._visibleFixed = this._fixedItems.slice();
6546
6795
  this._syncInputValue();
6547
6796
  this.emit("close", {});
6548
6797
  return this;
@@ -6598,6 +6847,38 @@ var Select = class _Select extends Component {
6598
6847
  select.set(requestedValue, { silent: true });
6599
6848
  return select;
6600
6849
  }
6850
+ /**
6851
+ * Creates the record-permission picker: the fixed Private and Public choices above the groups a
6852
+ * record can be shared with. The value is the tri-state ZeyOS stores — `false` for private,
6853
+ * `true` for public, or a group ID — so the control binds straight to the record field.
6854
+ *
6855
+ * @param {Element|string|null} target Component target.
6856
+ * @param {SelectOptions & {groups?: SelectItem[]|((query: string) => Promise<SelectItem[]>|SelectItem[])}} [options={}]
6857
+ * Select overrides other than the fixed choices; `groups` holds the shareable groups, either as
6858
+ * an array (filtered locally) or as a loader function (queried remotely).
6859
+ * @returns {Select}
6860
+ */
6861
+ static permission(target, options = {}) {
6862
+ const { groups = [], value = true, valueKey = "ID", labelKey = "name", ...rest } = options;
6863
+ const loader = typeof groups === "function" ? groups : null;
6864
+ const shareable = loader ? [] : groups;
6865
+ const choices = [{ ID: false, name: "Private" }, { ID: true, name: "Public" }];
6866
+ const select = new _Select(target, {
6867
+ filter: loader ?? "local",
6868
+ ...rest,
6869
+ value: null,
6870
+ items: shareable,
6871
+ fixedItems: [],
6872
+ // The two choices carry their own ID and label, so a caller's group readers stay untouched.
6873
+ valueKey: (item) => choices.includes(item) ? item.ID : readItem(item, valueKey),
6874
+ labelKey: (item) => choices.includes(item) ? item.name : String(readItem(item, labelKey) ?? "")
6875
+ });
6876
+ choices[0].name = select._message("permission.private", "Private");
6877
+ choices[1].name = select._message("permission.public", "Public");
6878
+ select.setFixedItems(choices);
6879
+ select.set(value, { silent: true });
6880
+ return select;
6881
+ }
6601
6882
  /** Cleans up the panel, pending async work, and enhanced target contents. @returns {void} */
6602
6883
  destroy() {
6603
6884
  this._cancelQuery();
@@ -6668,8 +6949,8 @@ var Select = class _Select extends Component {
6668
6949
  );
6669
6950
  if (!this._open) this.open();
6670
6951
  if (this.options.filter === "local") {
6671
- const keys = this.options.searchKeys ?? [this.options.labelKey];
6672
- this._visibleItems = matchItems(this._items, query, keys);
6952
+ this._visibleFixed = this._matchFixed(query);
6953
+ this._visibleItems = matchItems(this._items, query, this._searchKeys());
6673
6954
  this._renderOptions();
6674
6955
  this._setActive(this._optionEntries.length > 0 ? 0 : -1);
6675
6956
  this._position?.update();
@@ -6724,6 +7005,10 @@ var Select = class _Select extends Component {
6724
7005
  if (this.options.clearable) {
6725
7006
  this._appendOption(null, true, this.options.placeholder || this._message("select.none", "No selection"));
6726
7007
  }
7008
+ for (const item of this._visibleFixed) this._appendOption(item, false);
7009
+ if (this._visibleFixed.length > 0 && this._visibleItems.length > 0 && !this.options.groupKey) {
7010
+ list.append(h("div", { class: "zx-select__separator", role: "presentation" }));
7011
+ }
6727
7012
  for (const item of this._visibleItems) {
6728
7013
  if (this.options.groupKey) {
6729
7014
  const group = this._read(item, this.options.groupKey);
@@ -6737,7 +7022,7 @@ var Select = class _Select extends Component {
6737
7022
  }
6738
7023
  this._appendOption(item, false);
6739
7024
  }
6740
- if (this._visibleItems.length === 0 && !this._loading) {
7025
+ if (this._visibleItems.length === 0 && this._visibleFixed.length === 0 && !this._loading) {
6741
7026
  list.append(h("div", {
6742
7027
  class: "zx-select__empty",
6743
7028
  role: "status"
@@ -6777,6 +7062,7 @@ var Select = class _Select extends Component {
6777
7062
  if (this._queryTimer !== null) clearTimeout(this._queryTimer);
6778
7063
  this._requestSequence += 1;
6779
7064
  const sequence = this._requestSequence;
7065
+ this._visibleFixed = this._matchFixed(query);
6780
7066
  if (query.length < Math.max(0, Number(this.options.minQuery) || 0)) {
6781
7067
  this._setLoading(false);
6782
7068
  this._visibleItems = [];
@@ -6861,13 +7147,14 @@ var Select = class _Select extends Component {
6861
7147
  }
6862
7148
  /** @param {unknown} candidate @returns {SelectItem|null} */
6863
7149
  _findItem(candidate) {
6864
- const direct = this._items.find((item) => item === candidate);
7150
+ const pool = this._fixedItems.length === 0 ? this._items : [...this._fixedItems, ...this._items];
7151
+ const direct = pool.find((item) => item === candidate);
6865
7152
  if (direct !== void 0) return direct;
6866
7153
  const value = candidate !== null && typeof candidate === "object" ? this._itemValue(
6867
7154
  /** @type {SelectItem} */
6868
7155
  candidate
6869
7156
  ) : candidate;
6870
- return this._items.find((item) => Object.is(this._itemValue(item), value)) ?? null;
7157
+ return pool.find((item) => Object.is(this._itemValue(item), value)) ?? null;
6871
7158
  }
6872
7159
  /** @param {SelectItem|null} item @returns {unknown} */
6873
7160
  _itemValue(item) {
@@ -6880,9 +7167,20 @@ var Select = class _Select extends Component {
6880
7167
  }
6881
7168
  /** @param {SelectItem|null} item @param {SelectValueReader|SelectLabelReader} reader @returns {unknown} */
6882
7169
  _read(item, reader) {
6883
- if (typeof reader === "function") return reader(item);
6884
- if (item === null || typeof item !== "object") return item;
6885
- return item[reader];
7170
+ return readItem(item, reader);
7171
+ }
7172
+ /** @returns {Array<SelectValueReader|SelectLabelReader>} */
7173
+ _searchKeys() {
7174
+ return this.options.searchKeys ?? [this.options.labelKey];
7175
+ }
7176
+ /**
7177
+ * Narrows the pinned choices. They are always matched here and never handed to a filter
7178
+ * function: a fixed choice belongs to the control, not to whatever the remote source returns.
7179
+ * @param {string} query Search text.
7180
+ * @returns {SelectItem[]}
7181
+ */
7182
+ _matchFixed(query) {
7183
+ return matchItems(this._fixedItems, query, this._searchKeys());
6886
7184
  }
6887
7185
  /** @returns {number} */
6888
7186
  _selectedEntryIndex() {
@@ -6905,6 +7203,11 @@ var Select = class _Select extends Component {
6905
7203
  return message === key ? fallback : message;
6906
7204
  }
6907
7205
  };
7206
+ function readItem(item, reader) {
7207
+ if (typeof reader === "function") return reader(item);
7208
+ if (item === null || typeof item !== "object") return item;
7209
+ return item[reader];
7210
+ }
6908
7211
  function isPrintable(event) {
6909
7212
  return event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey;
6910
7213
  }
@@ -8821,6 +9124,82 @@ function onBreakpoint(handler, options = {}) {
8821
9124
  };
8822
9125
  }
8823
9126
 
9127
+ // src/core/format.js
9128
+ var MIN_DIGITS = 0;
9129
+ var MAX_DIGITS = 20;
9130
+ var DEFAULT_MAX_DIGITS = 3;
9131
+ var numberFormatters = /* @__PURE__ */ new Map();
9132
+ function formatNumber(value, options = {}) {
9133
+ const number = toFiniteNumber(value);
9134
+ if (number === null) return "";
9135
+ const { locale = getLanguage(), decimals, minDecimals, maxDecimals, group = true } = options;
9136
+ return format(locale, digitOptions({ decimals, minDecimals, maxDecimals, group }), number);
9137
+ }
9138
+ function formatCurrency(value, currency, options = {}) {
9139
+ const number = toFiniteNumber(value);
9140
+ if (number === null) return "";
9141
+ const { locale = getLanguage(), decimals } = options;
9142
+ const code = currencyCode(currency);
9143
+ if (code === null) return formatNumber(number, { locale, decimals });
9144
+ const digits = digitOptions({ decimals, group: true });
9145
+ return format(locale, { ...digits, style: "currency", currency: code }, number);
9146
+ }
9147
+ function formatPercent2(value, options = {}) {
9148
+ const number = toFiniteNumber(value);
9149
+ if (number === null) return "";
9150
+ const { locale = getLanguage(), decimals = 0 } = options;
9151
+ return format(locale, { ...digitOptions({ decimals, group: true }), style: "percent" }, number);
9152
+ }
9153
+ function format(locale, options, value) {
9154
+ const key = `${locale}\0${JSON.stringify(options)}`;
9155
+ let formatter = numberFormatters.get(key);
9156
+ if (formatter === void 0) {
9157
+ formatter = createNumberFormat(locale, options);
9158
+ numberFormatters.set(key, formatter);
9159
+ }
9160
+ return formatter.format(value);
9161
+ }
9162
+ function createNumberFormat(locale, options) {
9163
+ try {
9164
+ return new Intl.NumberFormat(locale, options);
9165
+ } catch {
9166
+ try {
9167
+ return new Intl.NumberFormat(void 0, options);
9168
+ } catch {
9169
+ return new Intl.NumberFormat();
9170
+ }
9171
+ }
9172
+ }
9173
+ function digitOptions({ decimals, minDecimals, maxDecimals, group = true }) {
9174
+ const result = { useGrouping: group !== false };
9175
+ const min = digitCount(minDecimals ?? decimals);
9176
+ const max = digitCount(maxDecimals ?? decimals);
9177
+ if (min !== null) result.minimumFractionDigits = min;
9178
+ if (max !== null) {
9179
+ result.maximumFractionDigits = max;
9180
+ if (min !== null && min > max) result.minimumFractionDigits = max;
9181
+ } else if (min !== null) {
9182
+ result.maximumFractionDigits = Math.max(min, DEFAULT_MAX_DIGITS);
9183
+ }
9184
+ return result;
9185
+ }
9186
+ function digitCount(value) {
9187
+ if (value == null) return null;
9188
+ const number = Number(value);
9189
+ if (!Number.isFinite(number)) return null;
9190
+ return Math.min(MAX_DIGITS, Math.max(MIN_DIGITS, Math.round(number)));
9191
+ }
9192
+ function toFiniteNumber(value) {
9193
+ if (value === null || value === void 0 || value === "") return null;
9194
+ const number = typeof value === "number" ? value : Number(value);
9195
+ return Number.isFinite(number) ? number : null;
9196
+ }
9197
+ function currencyCode(value) {
9198
+ if (typeof value !== "string") return null;
9199
+ const code = value.trim().toUpperCase();
9200
+ return /^[A-Z]{3}$/.test(code) ? code : null;
9201
+ }
9202
+
8824
9203
  // src/components/table/sort.js
8825
9204
  function compareStrings(left, right) {
8826
9205
  return String(left).localeCompare(String(right));
@@ -8876,7 +9255,8 @@ var Table = class extends Component {
8876
9255
  emptyText: null,
8877
9256
  rowClass: null,
8878
9257
  zebra: true,
8879
- editMode: false
9258
+ editMode: false,
9259
+ hierarchy: false
8880
9260
  };
8881
9261
  /**
8882
9262
  * Creates a table around a target or creates a new root when target is null.
@@ -8913,8 +9293,17 @@ var Table = class extends Component {
8913
9293
  this._rovingKey = null;
8914
9294
  this._rovingCell = null;
8915
9295
  this._rovingMatched = false;
9296
+ this._hierarchy = normalizeHierarchy(this.options.hierarchy, this._columns);
9297
+ this._expanded = initialExpanded(this._hierarchy?.expanded);
8916
9298
  if (this._sort && this.options.sortMode === "local") this._sortData();
9299
+ if (this._hierarchy?.expanded === true) {
9300
+ for (const id of hierarchyBranchIds(this._data, this.options.rowId, this._hierarchy.parentId)) {
9301
+ this._expanded.add(id);
9302
+ }
9303
+ }
9304
+ this._reconcileExpanded();
8917
9305
  this._table = h("table", { class: "zx-table__table" });
9306
+ if (this._hierarchy) this._table.setAttribute("role", "treegrid");
8918
9307
  this._colgroup = h("colgroup", { class: "zx-table__columns" });
8919
9308
  this._thead = h("thead", { class: "zx-table__head" });
8920
9309
  this._tbody = h("tbody", { class: "zx-table__body" });
@@ -8983,6 +9372,7 @@ var Table = class extends Component {
8983
9372
  assertRows(rows);
8984
9373
  this._data = [...rows];
8985
9374
  if (this._sort && this.options.sortMode === "local") this._sortData();
9375
+ this._reconcileExpanded();
8986
9376
  const selectionChanged = this._pruneSelection();
8987
9377
  this._renderBody();
8988
9378
  if (selectionChanged) this._emitSelectionChange();
@@ -9000,6 +9390,7 @@ var Table = class extends Component {
9000
9390
  assertRows(rows);
9001
9391
  this._data.push(...rows);
9002
9392
  if (this._sort && this.options.sortMode === "local") this._sortData();
9393
+ this._reconcileExpanded();
9003
9394
  this._renderBody();
9004
9395
  this._emitDataChange();
9005
9396
  return this;
@@ -9019,6 +9410,7 @@ var Table = class extends Component {
9019
9410
  this._data[index] = row;
9020
9411
  if (wasSelected) this._selected.add(this._idFor(row));
9021
9412
  if (this._sort && this.options.sortMode === "local") this._sortData();
9413
+ this._reconcileExpanded();
9022
9414
  this._renderBody();
9023
9415
  if (wasSelected && !Object.is(id, this._idFor(row))) this._emitSelectionChange();
9024
9416
  this._emitDataChange();
@@ -9036,6 +9428,7 @@ var Table = class extends Component {
9036
9428
  const index = this._data.findIndex((candidate) => Object.is(this._idFor(candidate), id));
9037
9429
  if (index < 0) return this;
9038
9430
  this._data.splice(index, 1);
9431
+ this._reconcileExpanded();
9039
9432
  const selectionChanged = this._selected.delete(id);
9040
9433
  if (Object.is(this._selectionAnchorId, id)) this._selectionAnchorId = null;
9041
9434
  this._renderBody();
@@ -9051,7 +9444,10 @@ var Table = class extends Component {
9051
9444
  getRow(id) {
9052
9445
  return this._data.find((row) => Object.is(this._idFor(row), id)) ?? null;
9053
9446
  }
9054
- /** @returns {TableRow[]} Shallow copy of current rows in display order. */
9447
+ /**
9448
+ * Returns a shallow copy of the current rows in display order.
9449
+ * @returns {TableRow[]} Current table rows.
9450
+ */
9055
9451
  getData() {
9056
9452
  return [...this._data];
9057
9453
  }
@@ -9065,6 +9461,7 @@ var Table = class extends Component {
9065
9461
  empty() {
9066
9462
  const selectionChanged = this._selected.size > 0;
9067
9463
  this._data = [];
9464
+ this._expanded.clear();
9068
9465
  this._selected.clear();
9069
9466
  this._selectionAnchorId = null;
9070
9467
  this._renderBody();
@@ -9099,7 +9496,10 @@ var Table = class extends Component {
9099
9496
  if (!options.silent) this.emit("sort", { id, dir: direction });
9100
9497
  return this;
9101
9498
  }
9102
- /** @returns {TableRow[]} Selected rows in current display order. */
9499
+ /**
9500
+ * Returns selected rows in current display order.
9501
+ * @returns {TableRow[]} Selected rows.
9502
+ */
9103
9503
  getSelection() {
9104
9504
  return this._data.filter((row) => this._selected.has(this._idFor(row)));
9105
9505
  }
@@ -9138,6 +9538,75 @@ var Table = class extends Component {
9138
9538
  this._emitSelectionChange();
9139
9539
  return this;
9140
9540
  }
9541
+ /**
9542
+ * Toggles one hierarchy branch. A leaf or an id absent from the table is a no-op.
9543
+ * @param {unknown} id Row id.
9544
+ * @param {{silent?: boolean}} [options={}] Event behavior.
9545
+ * @returns {this}
9546
+ * @fires Table#rowtoggle
9547
+ */
9548
+ toggleRow(id, options = {}) {
9549
+ return this._expanded.has(id) ? this.collapseRow(id, options) : this.expandRow(id, options);
9550
+ }
9551
+ /**
9552
+ * Expands one hierarchy branch.
9553
+ * @param {unknown} id Row id.
9554
+ * @param {{silent?: boolean}} [options={}] Event behavior.
9555
+ * @returns {this}
9556
+ * @fires Table#rowtoggle
9557
+ */
9558
+ expandRow(id, options = {}) {
9559
+ return this._setRowExpanded(id, true, options);
9560
+ }
9561
+ /**
9562
+ * Collapses one hierarchy branch without discarding its descendants or their expansion state.
9563
+ * @param {unknown} id Row id.
9564
+ * @param {{silent?: boolean}} [options={}] Event behavior.
9565
+ * @returns {this}
9566
+ * @fires Table#rowtoggle
9567
+ */
9568
+ collapseRow(id, options = {}) {
9569
+ return this._setRowExpanded(id, false, options);
9570
+ }
9571
+ /**
9572
+ * Expands every row that currently has children.
9573
+ * @param {{silent?: boolean}} [options={}] Event behavior. When not silent, one `rowtoggle`
9574
+ * event is emitted per branch whose state changed.
9575
+ * @returns {this}
9576
+ */
9577
+ expandAll(options = {}) {
9578
+ if (!this._hierarchy) return this;
9579
+ const branchIds = hierarchyBranchIds(this._data, this.options.rowId, this._hierarchy.parentId);
9580
+ const changed = branchIds.filter((id) => !this._expanded.has(id));
9581
+ for (const id of changed) this._expanded.add(id);
9582
+ if (changed.length) this._renderBody();
9583
+ if (!options.silent) {
9584
+ for (const id of changed) this._emitRowToggle(id, true);
9585
+ }
9586
+ return this;
9587
+ }
9588
+ /**
9589
+ * Collapses every hierarchy branch.
9590
+ * @param {{silent?: boolean}} [options={}] Event behavior.
9591
+ * @returns {this}
9592
+ */
9593
+ collapseAll(options = {}) {
9594
+ if (!this._hierarchy || this._expanded.size === 0) return this;
9595
+ const changed = [...this._expanded];
9596
+ this._expanded.clear();
9597
+ this._renderBody();
9598
+ if (!options.silent) {
9599
+ for (const id of changed) this._emitRowToggle(id, false);
9600
+ }
9601
+ return this;
9602
+ }
9603
+ /**
9604
+ * Returns expanded branch ids in current flat-data order.
9605
+ * @returns {unknown[]} Expanded branch ids.
9606
+ */
9607
+ getExpanded() {
9608
+ return this._data.map((row) => this._idFor(row)).filter((id) => this._expanded.has(id));
9609
+ }
9141
9610
  /**
9142
9611
  * Opens the editor for a cell. No-op unless the table runs with `editMode` and the column
9143
9612
  * resolves to an editable type for that row. In `'row'` mode every editable cell of the row is
@@ -9245,11 +9714,17 @@ var Table = class extends Component {
9245
9714
  this._abortEdit({ focus: true });
9246
9715
  return this;
9247
9716
  }
9248
- /** @returns {boolean} Whether a cell or row is currently open for editing. */
9717
+ /**
9718
+ * Reports whether a cell or row is currently open for editing.
9719
+ * @returns {boolean} Whether an edit is open.
9720
+ */
9249
9721
  isEditing() {
9250
9722
  return this._edit !== null;
9251
9723
  }
9252
- /** @returns {{id: unknown, columnId: string}|null} Row id and focused column id, or null. */
9724
+ /**
9725
+ * Returns the row id and focused column id for the open edit.
9726
+ * @returns {{id: unknown, columnId: string}|null} Editing location, or null.
9727
+ */
9253
9728
  getEditing() {
9254
9729
  return this._edit ? { id: this._edit.id, columnId: this._edit.columnId } : null;
9255
9730
  }
@@ -9258,7 +9733,31 @@ var Table = class extends Component {
9258
9733
  * @returns {TableRow[]}
9259
9734
  */
9260
9735
  _visibleData() {
9261
- return this._rendered === Infinity ? this._data : this._data.slice(0, this._rendered);
9736
+ return this._visibleEntries().map((entry) => entry.row);
9737
+ }
9738
+ /** @returns {TableHierarchyEntry[]} Rows visible after hierarchy expansion, before growing. */
9739
+ _projectedData() {
9740
+ if (!this._hierarchy) {
9741
+ return this._data.map((row, index) => ({
9742
+ row,
9743
+ id: this._idFor(row),
9744
+ index,
9745
+ depth: 0,
9746
+ hasChildren: false,
9747
+ parentId: null
9748
+ }));
9749
+ }
9750
+ return projectHierarchyRows(
9751
+ this._data,
9752
+ this.options.rowId,
9753
+ this._hierarchy.parentId,
9754
+ this._expanded
9755
+ );
9756
+ }
9757
+ /** @returns {TableHierarchyEntry[]} Rows that currently have DOM entries. */
9758
+ _visibleEntries() {
9759
+ const projected = this._projectedData();
9760
+ return this._rendered === Infinity ? projected : projected.slice(0, this._rendered);
9262
9761
  }
9263
9762
  /**
9264
9763
  * Renders the next batch of rows.
@@ -9270,10 +9769,11 @@ var Table = class extends Component {
9270
9769
  if (!this._growStep) return this;
9271
9770
  const step = Number(count) > 0 ? Math.trunc(Number(count)) : this._growStep;
9272
9771
  const before = this._rendered;
9273
- this._rendered = Math.min(this._data.length, this._rendered + step);
9772
+ const total = this._projectedData().length;
9773
+ this._rendered = Math.min(total, this._rendered + step);
9274
9774
  if (this._rendered === before) return this;
9275
9775
  this._renderBody();
9276
- this.emit("grow", { rendered: this._rendered, total: this._data.length });
9776
+ this.emit("grow", { rendered: this._rendered, total });
9277
9777
  return this;
9278
9778
  }
9279
9779
  /**
@@ -9282,14 +9782,14 @@ var Table = class extends Component {
9282
9782
  * @fires Table#grow
9283
9783
  */
9284
9784
  showAll() {
9285
- return this.growBy(this._data.length);
9785
+ return this.growBy(this._projectedData().length);
9286
9786
  }
9287
9787
  /**
9288
9788
  * How many rows are currently rendered.
9289
9789
  * @returns {number}
9290
9790
  */
9291
9791
  getRenderedCount() {
9292
- return Math.min(this._rendered, this._data.length);
9792
+ return Math.min(this._rendered, this._projectedData().length);
9293
9793
  }
9294
9794
  /**
9295
9795
  * Keeps the "show more" control in step with what is rendered.
@@ -9301,7 +9801,7 @@ var Table = class extends Component {
9301
9801
  */
9302
9802
  _syncGrowing() {
9303
9803
  if (!this._growStep) return;
9304
- const remaining = this._data.length - this.getRenderedCount();
9804
+ const remaining = this._projectedData().length - this.getRenderedCount();
9305
9805
  if (remaining <= 0) {
9306
9806
  this._more?.remove();
9307
9807
  this._more = null;
@@ -9372,10 +9872,11 @@ var Table = class extends Component {
9372
9872
  * @returns {void}
9373
9873
  */
9374
9874
  _applyStackedRoles(stacked) {
9375
- const parts = [[this._table, "table"], [this._thead, "rowgroup"], [this._tbody, "rowgroup"]];
9875
+ const parts = [[this._table, this._hierarchy ? "treegrid" : "table"], [this._thead, "rowgroup"], [this._tbody, "rowgroup"]];
9376
9876
  for (const [element, role] of parts) {
9377
9877
  if (!element) continue;
9378
9878
  if (stacked) element.setAttribute("role", role);
9879
+ else if (element === this._table && this._hierarchy) element.setAttribute("role", "treegrid");
9379
9880
  else element.removeAttribute("role");
9380
9881
  }
9381
9882
  for (const [selector, role] of [["tr", "row"], ["td", "cell"], ["th", "columnheader"]]) {
@@ -9406,9 +9907,9 @@ var Table = class extends Component {
9406
9907
  }
9407
9908
  /** @returns {void} */
9408
9909
  _renderColumns() {
9409
- const fragment = document.createDocumentFragment();
9910
+ const fragment2 = document.createDocumentFragment();
9410
9911
  if (this.options.selectable === "multi") {
9411
- fragment.append(h("col", { class: "zx-table__selection-column" }));
9912
+ fragment2.append(h("col", { class: "zx-table__selection-column" }));
9412
9913
  }
9413
9914
  const fractions = this._columns.map((column) => parseFraction(column.width));
9414
9915
  const total = fractions.reduce((sum, value) => sum + value, 0);
@@ -9417,9 +9918,9 @@ var Table = class extends Component {
9417
9918
  const col = h("col");
9418
9919
  if (fractions[index] > 0) col.style.width = `${fractions[index] / total * 100}%`;
9419
9920
  else if (column.width && column.width !== "auto") col.style.width = String(column.width);
9420
- fragment.append(col);
9921
+ fragment2.append(col);
9421
9922
  });
9422
- this._colgroup.replaceChildren(fragment);
9923
+ this._colgroup.replaceChildren(fragment2);
9423
9924
  }
9424
9925
  /** @returns {void} */
9425
9926
  _renderHeader() {
@@ -9452,7 +9953,8 @@ var Table = class extends Component {
9452
9953
  this._headers = /* @__PURE__ */ new Map();
9453
9954
  for (const column of this._columns) {
9454
9955
  const th = h("th", { scope: "col" });
9455
- if (column.align) th.dataset.align = column.align;
9956
+ const align = column.align ?? (isNumericColumn(column) ? "end" : null);
9957
+ if (align) th.dataset.align = align;
9456
9958
  if (column.headerTitle) th.title = column.headerTitle;
9457
9959
  if (column.sortable) {
9458
9960
  const button = h("button", {
@@ -9476,13 +9978,15 @@ var Table = class extends Component {
9476
9978
  /** @returns {void} @fires Table#editcancel */
9477
9979
  _renderBody() {
9478
9980
  this._abortEdit({ focus: false });
9479
- const fragment = document.createDocumentFragment();
9981
+ const fragment2 = document.createDocumentFragment();
9480
9982
  this._rowMeta = /* @__PURE__ */ new WeakMap();
9481
9983
  this._rowElements = /* @__PURE__ */ new Map();
9482
9984
  this._rovingCell = null;
9483
9985
  this._rovingMatched = false;
9986
+ const projected = this._projectedData();
9987
+ this._hierarchyMeta = new Map(projected.map((entry) => [entry.id, entry]));
9484
9988
  if (this._data.length === 0) {
9485
- fragment.append(h(
9989
+ fragment2.append(h(
9486
9990
  "tr",
9487
9991
  { class: "zx-table__empty-row" },
9488
9992
  h("td", {
@@ -9491,10 +9995,12 @@ var Table = class extends Component {
9491
9995
  }, this._emptyContent())
9492
9996
  ));
9493
9997
  } else {
9494
- this._visibleData().forEach((row, index) => fragment.append(this._createRow(row, index)));
9998
+ this._visibleEntries().forEach((entry, visibleIndex) => {
9999
+ fragment2.append(this._createRow(entry.row, entry.index, { ...entry, visibleIndex }));
10000
+ });
9495
10001
  }
9496
- this._tbody.replaceChildren(fragment);
9497
- if (this._growStep) this._table.setAttribute("aria-rowcount", String(this._data.length + 1));
10002
+ this._tbody.replaceChildren(fragment2);
10003
+ if (this._growStep) this._table.setAttribute("aria-rowcount", String(projected.length + 1));
9498
10004
  this._syncSelectAll();
9499
10005
  this._syncGrowing();
9500
10006
  if (this._stacked) this._applyStackedRoles(true);
@@ -9513,18 +10019,27 @@ var Table = class extends Component {
9513
10019
  if (empty == null) return this.msg("table.empty");
9514
10020
  return typeof empty === "function" ? empty() : empty;
9515
10021
  }
9516
- /** @param {TableRow} row @param {number} index @returns {HTMLTableRowElement} */
9517
- _createRow(row, index) {
10022
+ /**
10023
+ * @param {TableRow} row
10024
+ * @param {number} index
10025
+ * @param {TableHierarchyEntry & {visibleIndex: number}} tree
10026
+ * @returns {HTMLTableRowElement}
10027
+ */
10028
+ _createRow(row, index, tree) {
9518
10029
  const id = this._idFor(row);
9519
10030
  const tr = (
9520
10031
  /** @type {HTMLTableRowElement} */
9521
10032
  h("tr", { dataset: { row: "" } })
9522
10033
  );
9523
- if (this._growStep) tr.setAttribute("aria-rowindex", String(index + 2));
10034
+ if (this._growStep) tr.setAttribute("aria-rowindex", String(tree.visibleIndex + 2));
9524
10035
  const rowClass = typeof this.options.rowClass === "function" ? this.options.rowClass(row) : "";
9525
10036
  if (rowClass) tr.classList.add(...String(rowClass).split(/\s+/).filter(Boolean));
9526
10037
  if (this.options.selectable !== false) tr.setAttribute("aria-selected", String(this._selected.has(id)));
9527
- this._rowMeta.set(tr, { row, id, index });
10038
+ if (this._hierarchy) {
10039
+ tr.setAttribute("aria-level", String(tree.depth + 1));
10040
+ if (tree.hasChildren) tr.setAttribute("aria-expanded", String(this._expanded.has(id)));
10041
+ }
10042
+ this._rowMeta.set(tr, { row, id, index, visibleIndex: tree.visibleIndex });
9528
10043
  const elements = this._rowElements.get(id) ?? [];
9529
10044
  elements.push(tr);
9530
10045
  this._rowElements.set(id, elements);
@@ -9542,7 +10057,8 @@ var Table = class extends Component {
9542
10057
  /** @type {HTMLTableCellElement} */
9543
10058
  h("td")
9544
10059
  );
9545
- if (column.align) cell.dataset.align = column.align;
10060
+ const align = column.align ?? (isNumericColumn(column) ? "end" : null);
10061
+ if (align) cell.dataset.align = align;
9546
10062
  cell.dataset.label = column.label;
9547
10063
  if (column.popin === false) cell.dataset.popin = "false";
9548
10064
  if (this._editMode !== false) {
@@ -9553,7 +10069,7 @@ var Table = class extends Component {
9553
10069
  this._offerRovingCell(cell, id, column.id);
9554
10070
  }
9555
10071
  }
9556
- this._renderCellContent(cell, column, row, index);
10072
+ this._renderCellContent(cell, column, row, index, tree);
9557
10073
  tr.append(cell);
9558
10074
  });
9559
10075
  return tr;
@@ -9564,13 +10080,33 @@ var Table = class extends Component {
9564
10080
  * @param {TableColumn} column Column definition.
9565
10081
  * @param {TableRow} row Row data.
9566
10082
  * @param {number} index Row index.
10083
+ * @param {TableHierarchyEntry} [tree] Hierarchy metadata for the row.
9567
10084
  * @returns {void}
9568
10085
  */
9569
- _renderCellContent(cell, column, row, index) {
9570
- const value = typeof column.render === "function" ? column.render(row, index) : row?.[column.id];
9571
- if (value && typeof value === "object" && typeof value.nodeType === "number") cell.replaceChildren(value);
9572
- else if (value != null) cell.replaceChildren(document.createTextNode(String(value)));
9573
- else cell.replaceChildren();
10086
+ _renderCellContent(cell, column, row, index, tree = this._hierarchyMeta?.get(this._idFor(row))) {
10087
+ const value = formatTableCell(column, row, index);
10088
+ const content = h("span", { class: "zx-table__cell-content" });
10089
+ if (value && typeof value === "object" && typeof value.nodeType === "number") content.append(value);
10090
+ else if (value != null) content.append(document.createTextNode(String(value)));
10091
+ if (!this._hierarchy || column.id !== this._hierarchy.column || !tree) {
10092
+ cell.replaceChildren(...content.childNodes);
10093
+ return;
10094
+ }
10095
+ const branch = h("span", { class: "zx-table__tree-cell" });
10096
+ branch.style.setProperty("--zx-table-depth", String(tree.depth));
10097
+ if (tree.hasChildren) {
10098
+ const expanded = this._expanded.has(tree.id);
10099
+ branch.append(h("button", {
10100
+ class: "zx-table__tree-toggle",
10101
+ type: "button",
10102
+ ariaExpanded: String(expanded),
10103
+ ariaLabel: `${expanded ? "Collapse" : "Expand"} row ${index + 1}`
10104
+ }, icon(expanded ? "chevron-down" : "chevron-right", { size: 14 })));
10105
+ } else {
10106
+ branch.append(h("span", { class: "zx-table__tree-spacer", ariaHidden: "true" }));
10107
+ }
10108
+ branch.append(content);
10109
+ cell.replaceChildren(branch);
9574
10110
  }
9575
10111
  /** @param {MouseEvent} event @returns {void} */
9576
10112
  _handleBodyClick(event) {
@@ -9583,7 +10119,15 @@ var Table = class extends Component {
9583
10119
  if (!rowElement || !this._tbody.contains(rowElement)) return;
9584
10120
  const meta = this._rowMeta.get(rowElement);
9585
10121
  if (!meta) return;
9586
- if (this._edit) this.commitEdit();
10122
+ if (this._edit) {
10123
+ this.commitEdit();
10124
+ if (this._edit) return;
10125
+ }
10126
+ const disclosure = target.closest(".zx-table__tree-toggle");
10127
+ if (disclosure) {
10128
+ this.toggleRow(meta.id);
10129
+ return;
10130
+ }
9587
10131
  const checkbox = target.closest(".zx-table__row-checkbox");
9588
10132
  if (checkbox && this.options.selectable === "multi") {
9589
10133
  this._toggleMultiSelection(
@@ -9679,7 +10223,15 @@ var Table = class extends Component {
9679
10223
  return entry;
9680
10224
  }
9681
10225
  if (type === "number") {
9682
- const field = new NumberField(null, { ...props, value: value ?? null });
10226
+ const locale = resolveColumnOption(column.locale, row);
10227
+ const decimals = resolveColumnDecimals(column, row);
10228
+ const suffix = column.type === "currency" ? resolveColumnOption(column.currency, row) : column.type === "unit" ? resolveColumnOption(column.unit, row) : null;
10229
+ const inferred = {
10230
+ ...locale != null && !Object.prototype.hasOwnProperty.call(props, "locale") ? { locale } : {},
10231
+ ...decimals != null && !Object.prototype.hasOwnProperty.call(props, "precision") ? { precision: decimals } : {},
10232
+ ...suffix != null && !Object.prototype.hasOwnProperty.call(props, "unit") ? { unit: String(suffix) } : {}
10233
+ };
10234
+ const field = new NumberField(null, { ...inferred, ...props, value: value ?? null });
9683
10235
  host.append(field.el);
9684
10236
  entry.component = field;
9685
10237
  entry.control = field.getInput();
@@ -9910,9 +10462,12 @@ var Table = class extends Component {
9910
10462
  _nextEditableCell(rowIndex, columnId, direction) {
9911
10463
  const columnCount = this._columns.length;
9912
10464
  if (rowIndex < 0 || columnCount === 0) return null;
9913
- let row = rowIndex;
10465
+ const visible = this._visibleData();
10466
+ const startingId = this._idFor(this._data[rowIndex]);
10467
+ let row = visible.findIndex((candidate) => Object.is(this._idFor(candidate), startingId));
10468
+ if (row < 0) return null;
9914
10469
  let column = this._columns.findIndex((candidate) => candidate.id === columnId);
9915
- const limit = this._data.length * columnCount + columnCount;
10470
+ const limit = visible.length * columnCount + columnCount;
9916
10471
  for (let step = 0; step < limit; step += 1) {
9917
10472
  column += direction;
9918
10473
  if (column >= columnCount) {
@@ -9922,11 +10477,15 @@ var Table = class extends Component {
9922
10477
  row -= 1;
9923
10478
  column = columnCount - 1;
9924
10479
  }
9925
- if (row < 0 || row >= this._data.length) return null;
9926
- const candidateRow = this._data[row];
10480
+ if (row < 0 || row >= visible.length) return null;
10481
+ const candidateRow = visible[row];
9927
10482
  const candidateColumn = this._columns[column];
9928
10483
  if (resolveEditable(candidateColumn, candidateRow) !== false) {
9929
- return { id: this._idFor(candidateRow), columnId: candidateColumn.id, index: row };
10484
+ return {
10485
+ id: this._idFor(candidateRow),
10486
+ columnId: candidateColumn.id,
10487
+ index: this._data.indexOf(candidateRow)
10488
+ };
9930
10489
  }
9931
10490
  }
9932
10491
  return null;
@@ -9993,14 +10552,16 @@ var Table = class extends Component {
9993
10552
  const message = this.msg(key);
9994
10553
  return message === key ? fallback : message;
9995
10554
  }
9996
- /** @param {{row: TableRow, id: unknown, index: number}} meta @param {boolean} checked @param {boolean} shiftKey @returns {void} */
10555
+ /** @param {{row: TableRow, id: unknown, index: number, visibleIndex?: number}} meta @param {boolean} checked @param {boolean} shiftKey @returns {void} */
9997
10556
  _toggleMultiSelection(meta, checked, shiftKey) {
9998
- const anchorIndex = this._data.findIndex((row) => Object.is(this._idFor(row), this._selectionAnchorId));
10557
+ const visible = this._visibleData();
10558
+ const anchorIndex = visible.findIndex((row) => Object.is(this._idFor(row), this._selectionAnchorId));
9999
10559
  if (shiftKey && anchorIndex >= 0) {
10000
- const start = Math.min(anchorIndex, meta.index);
10001
- const end = Math.max(anchorIndex, meta.index);
10560
+ const currentIndex = meta.visibleIndex ?? visible.findIndex((row) => Object.is(this._idFor(row), meta.id));
10561
+ const start = Math.min(anchorIndex, currentIndex);
10562
+ const end = Math.max(anchorIndex, currentIndex);
10002
10563
  for (let index = start; index <= end; index += 1) {
10003
- const id = this._idFor(this._data[index]);
10564
+ const id = this._idFor(visible[index]);
10004
10565
  if (checked) this._selected.add(id);
10005
10566
  else this._selected.delete(id);
10006
10567
  }
@@ -10051,7 +10612,34 @@ var Table = class extends Component {
10051
10612
  const column = this._columns.find((candidate) => candidate.id === this._sort.id);
10052
10613
  if (!column) return;
10053
10614
  const getValue = typeof column.sortValue === "function" ? column.sortValue : (row) => row?.[column.id];
10054
- this._data = sortRows(this._data, getValue, this._sort.dir);
10615
+ this._data = this._hierarchy ? sortHierarchyRows(this._data, this.options.rowId, this._hierarchy.parentId, getValue, this._sort.dir) : sortRows(this._data, getValue, this._sort.dir);
10616
+ }
10617
+ /** @param {unknown} id @param {boolean} expanded @param {{silent?: boolean}} options @returns {this} */
10618
+ _setRowExpanded(id, expanded, options) {
10619
+ if (!this._hierarchy) return this;
10620
+ const branchIds = hierarchyBranchIds(this._data, this.options.rowId, this._hierarchy.parentId);
10621
+ if (!branchIds.some((candidate) => Object.is(candidate, id))) return this;
10622
+ if (this._expanded.has(id) === expanded) return this;
10623
+ if (expanded) this._expanded.add(id);
10624
+ else this._expanded.delete(id);
10625
+ this._renderBody();
10626
+ if (!options.silent) this._emitRowToggle(id, expanded);
10627
+ return this;
10628
+ }
10629
+ /** @param {unknown} id @param {boolean} expanded @returns {void} */
10630
+ _emitRowToggle(id, expanded) {
10631
+ const row = this.getRow(id);
10632
+ if (row) this.emit("rowtoggle", { row, id, expanded });
10633
+ }
10634
+ /** Keeps the reader's current expansion state limited to branches still present in the data. */
10635
+ _reconcileExpanded() {
10636
+ if (!this._hierarchy) {
10637
+ this._expanded.clear();
10638
+ return;
10639
+ }
10640
+ const branches = hierarchyBranchIds(this._data, this.options.rowId, this._hierarchy.parentId);
10641
+ const valid = new Set(branches);
10642
+ for (const id of this._expanded) if (!valid.has(id)) this._expanded.delete(id);
10055
10643
  }
10056
10644
  /** @returns {boolean} Whether selection changed. */
10057
10645
  _pruneSelection() {
@@ -10075,6 +10663,65 @@ var Table = class extends Component {
10075
10663
  this.emit("datachange", { rows: this.getData() });
10076
10664
  }
10077
10665
  };
10666
+ function formatTableCell(column, row, index = 0) {
10667
+ if (typeof column?.render === "function") return column.render(row, index);
10668
+ const value = row?.[column?.id];
10669
+ const type = column?.type ?? "text";
10670
+ if (type === "text") return value;
10671
+ const locale = resolveColumnOption(column.locale, row);
10672
+ const decimals = resolveColumnDecimals(column, row);
10673
+ const options = {
10674
+ ...locale != null ? { locale: String(locale) } : {},
10675
+ ...decimals != null ? { decimals } : {}
10676
+ };
10677
+ if (type === "number") return formatNumber(value, options);
10678
+ if (type === "currency") {
10679
+ const currency = resolveColumnOption(column.currency, row);
10680
+ return formatCurrency(value, currency == null ? null : String(currency), options);
10681
+ }
10682
+ if (type === "percent") return formatPercent2(value, options);
10683
+ if (type === "unit") {
10684
+ const number = formatNumber(value, options);
10685
+ const unit = resolveColumnOption(column.unit, row);
10686
+ return number && unit != null && String(unit) !== "" ? `${number}\xA0${unit}` : number;
10687
+ }
10688
+ return value;
10689
+ }
10690
+ function projectHierarchyRows(rows, rowId, parentId, expanded = []) {
10691
+ const forest = hierarchyForest(rows, rowId, parentId);
10692
+ const open = expanded instanceof Set ? expanded : new Set(expanded);
10693
+ const result = [];
10694
+ const stack = forest.roots.slice().reverse().map((node) => ({ node, depth: 0 }));
10695
+ while (stack.length) {
10696
+ const { node, depth } = stack.pop();
10697
+ result.push({
10698
+ row: node.row,
10699
+ id: node.id,
10700
+ index: node.index,
10701
+ depth,
10702
+ hasChildren: node.children.length > 0,
10703
+ parentId: node.parent?.id ?? null
10704
+ });
10705
+ if (!open.has(node.id)) continue;
10706
+ for (let index = node.children.length - 1; index >= 0; index -= 1) {
10707
+ stack.push({ node: node.children[index], depth: depth + 1 });
10708
+ }
10709
+ }
10710
+ return result;
10711
+ }
10712
+ function sortHierarchyRows(rows, rowId, parentId, getValue, direction) {
10713
+ const forest = hierarchyForest(rows, rowId, parentId);
10714
+ const sorted = (nodes) => sortRows(nodes, (node) => getValue(node.row), direction);
10715
+ const result = [];
10716
+ const stack = sorted(forest.roots).reverse();
10717
+ while (stack.length) {
10718
+ const node = stack.pop();
10719
+ result.push(node.row);
10720
+ const children = sorted(node.children);
10721
+ for (let index = children.length - 1; index >= 0; index -= 1) stack.push(children[index]);
10722
+ }
10723
+ return result;
10724
+ }
10078
10725
  function resolveEditable(column, row) {
10079
10726
  if (!column || typeof column !== "object") return false;
10080
10727
  if (typeof column.editor === "function") {
@@ -10082,7 +10729,7 @@ function resolveEditable(column, row) {
10082
10729
  return "custom";
10083
10730
  }
10084
10731
  const declared = typeof column.editable === "function" ? column.editable(row) : column.editable;
10085
- if (declared === true) return "text";
10732
+ if (declared === true) return isNumericColumn(column) ? "number" : "text";
10086
10733
  if (typeof declared === "string" && EDITOR_TYPES.has(declared)) return (
10087
10734
  /** @type {TableEditorType} */
10088
10735
  declared
@@ -10131,6 +10778,71 @@ function firstValidationError(values, columns, row, fallbackMessage = "Invalid v
10131
10778
  }
10132
10779
  return null;
10133
10780
  }
10781
+ function resolveColumnOption(option, row) {
10782
+ return typeof option === "function" ? option(row) : option;
10783
+ }
10784
+ function resolveColumnDecimals(column, row) {
10785
+ const value = resolveColumnOption(column?.decimals, row);
10786
+ if (value == null) return null;
10787
+ const number = Number(value);
10788
+ return Number.isFinite(number) ? Math.max(0, Math.trunc(number)) : null;
10789
+ }
10790
+ function isNumericColumn(column) {
10791
+ return ["number", "currency", "percent", "unit"].includes(column?.type);
10792
+ }
10793
+ function hierarchyForest(rows, rowId, parentId) {
10794
+ const nodes = (Array.isArray(rows) ? rows : []).map((row, index) => ({
10795
+ row,
10796
+ id: readRowValue(rowId, row),
10797
+ index,
10798
+ parent: null,
10799
+ children: []
10800
+ }));
10801
+ const byId = /* @__PURE__ */ new Map();
10802
+ for (const node of nodes) if (!byId.has(node.id)) byId.set(node.id, node);
10803
+ const parentOf = /* @__PURE__ */ new Map();
10804
+ for (const node of nodes) {
10805
+ const parent = byId.get(readRowValue(parentId, node.row)) ?? null;
10806
+ parentOf.set(node, parent === node ? null : parent);
10807
+ }
10808
+ for (const node of nodes) {
10809
+ const seen = /* @__PURE__ */ new Set([node]);
10810
+ let cursor = parentOf.get(node) ?? null;
10811
+ while (cursor) {
10812
+ if (seen.has(cursor)) {
10813
+ parentOf.set(cursor, null);
10814
+ break;
10815
+ }
10816
+ seen.add(cursor);
10817
+ cursor = parentOf.get(cursor) ?? null;
10818
+ }
10819
+ }
10820
+ const roots = [];
10821
+ for (const node of nodes) {
10822
+ node.parent = parentOf.get(node) ?? null;
10823
+ if (node.parent) node.parent.children.push(node);
10824
+ else roots.push(node);
10825
+ }
10826
+ return { roots, nodes };
10827
+ }
10828
+ function hierarchyBranchIds(rows, rowId, parentId) {
10829
+ return hierarchyForest(rows, rowId, parentId).nodes.filter((node) => node.children.length > 0).map((node) => node.id);
10830
+ }
10831
+ function readRowValue(accessor, row) {
10832
+ return typeof accessor === "function" ? accessor(row) : row?.[accessor];
10833
+ }
10834
+ function normalizeHierarchy(option, columns) {
10835
+ if (!option || typeof option !== "object") return null;
10836
+ const parentId = option.parentId;
10837
+ if (typeof parentId !== "string" && typeof parentId !== "function") return null;
10838
+ const requested = typeof option.column === "string" ? option.column : null;
10839
+ const column = columns.some((candidate) => candidate.id === requested) ? requested : columns[0]?.id;
10840
+ if (!column) return null;
10841
+ return { ...option, parentId, column };
10842
+ }
10843
+ function initialExpanded(option) {
10844
+ return new Set(Array.isArray(option) ? option : []);
10845
+ }
10134
10846
  function normalizeEditMode(mode) {
10135
10847
  if (mode === "cell" || mode === "row") return mode;
10136
10848
  return mode === true ? "cell" : false;
@@ -11698,7 +12410,7 @@ function buildZeyosTableConfig(client, resource, opts = {}) {
11698
12410
  const monetary = isMonetaryField(meta);
11699
12411
  const column = fieldToZxColumn(monetary ? { ...meta, format: "money" } : meta);
11700
12412
  if (monetary) {
11701
- column.render = (row) => formatCurrency(row?.[meta.id], row?.currency);
12413
+ column.render = (row) => formatCurrency2(row?.[meta.id], row?.currency);
11702
12414
  if (schemaHasField(client, resource, "currency")) projection.currency = "currency";
11703
12415
  }
11704
12416
  return column;
@@ -11823,10 +12535,10 @@ function inferLabelKey2(client, resource) {
11823
12535
  return names[0] ?? "ID";
11824
12536
  }
11825
12537
  function isMonetaryField(meta) {
11826
- const format = String(meta.format ?? "").toLowerCase();
11827
- return /money|currency|price/.test(format) || /(?:amount|price)$/.test(String(meta.id).toLowerCase());
12538
+ const format2 = String(meta.format ?? "").toLowerCase();
12539
+ return /money|currency|price/.test(format2) || /(?:amount|price)$/.test(String(meta.id).toLowerCase());
11828
12540
  }
11829
- function formatCurrency(value, currency) {
12541
+ function formatCurrency2(value, currency) {
11830
12542
  if (value == null || value === "") return "";
11831
12543
  const number = Number(value);
11832
12544
  if (!Number.isFinite(number)) return "";