@zeyos/zx 2.3.0 → 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 (105) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/DESIGN-SYSTEM.md +136 -0
  3. package/MIGRATION.md +155 -4
  4. package/README.md +24 -13
  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 +7 -1
  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/sheet/sheet-stack.d.ts +167 -0
  27. package/dist/types/components/sheet/sheet.d.ts +294 -0
  28. package/dist/types/components/split-view/split-view.d.ts +2 -21
  29. package/dist/types/components/table/table.d.ts +219 -12
  30. package/dist/types/components/tooltip/tooltip.d.ts +2 -2
  31. package/dist/types/components/value-list/value-list.d.ts +24 -3
  32. package/dist/types/core/dom.d.ts +114 -0
  33. package/dist/types/core/drag-axis.d.ts +111 -0
  34. package/dist/types/core/position.d.ts +4 -2
  35. package/dist/types/index.d.ts +12 -2
  36. package/dist/zx-compat.esm.js +641 -277
  37. package/dist/zx-compat.esm.min.js +1 -1
  38. package/dist/zx-compat.global.js +7609 -2236
  39. package/dist/zx-compat.global.min.js +3 -3
  40. package/dist/zx-zeyos.esm.js +546 -87
  41. package/dist/zx-zeyos.esm.min.js +1 -1
  42. package/dist/zx.css +1452 -101
  43. package/dist/zx.esm.js +7952 -2592
  44. package/dist/zx.esm.min.js +3 -3
  45. package/dist/zx.global.js +7952 -2592
  46. package/dist/zx.global.min.js +3 -3
  47. package/dist/zx.min.css +1 -1
  48. package/docs/api.json +2276 -337
  49. package/docs/llms.md +426 -30
  50. package/docs/llms.txt +21 -9
  51. package/package.json +4 -2
  52. package/src/compat/globals.js +3 -1
  53. package/src/compat/map/data.js +31 -9
  54. package/src/components/account-menu/account-menu.css +70 -0
  55. package/src/components/account-menu/account-menu.js +208 -0
  56. package/src/components/app-rail/app-rail.css +107 -0
  57. package/src/components/app-rail/app-rail.js +668 -0
  58. package/src/components/app-sidebar/app-sidebar.css +68 -0
  59. package/src/components/app-sidebar/app-sidebar.js +388 -0
  60. package/src/components/avatar/avatar.css +61 -0
  61. package/src/components/avatar/avatar.js +222 -0
  62. package/src/components/button/button.css +0 -1
  63. package/src/components/chart/chart.css +61 -0
  64. package/src/components/chart/chart.js +518 -0
  65. package/src/components/date-picker/month-picker.js +9 -0
  66. package/src/components/dialog/dialog.js +11 -4
  67. package/src/components/dock/dock.css +228 -0
  68. package/src/components/dock/dock.js +977 -0
  69. package/src/components/dropdown/dropdown.js +1 -1
  70. package/src/components/field/field.js +2 -0
  71. package/src/components/field-upload/field-upload.js +9 -4
  72. package/src/components/form/form.js +6 -3
  73. package/src/components/grid/grid.css +6 -0
  74. package/src/components/grid/grid.js +213 -0
  75. package/src/components/launcher/launcher.css +100 -0
  76. package/src/components/launcher/launcher.js +557 -0
  77. package/src/components/layout/layout.js +2 -2
  78. package/src/components/menu-button/menu-button.js +27 -2
  79. package/src/components/modal/modal.css +5 -1
  80. package/src/components/modal/modal.js +37 -2
  81. package/src/components/multi-value-editor/multi-value-editor.js +2 -1
  82. package/src/components/questionnaire/items.js +370 -0
  83. package/src/components/questionnaire/questionnaire.css +387 -0
  84. package/src/components/questionnaire/questionnaire.js +1033 -0
  85. package/src/components/select/select.css +0 -1
  86. package/src/components/sheet/sheet-stack.js +304 -0
  87. package/src/components/sheet/sheet.css +351 -0
  88. package/src/components/sheet/sheet.js +551 -0
  89. package/src/components/split-view/split-view.js +76 -161
  90. package/src/components/table/table.css +43 -0
  91. package/src/components/table/table.js +523 -45
  92. package/src/components/tooltip/tooltip.js +5 -2
  93. package/src/components/value-list/value-list.js +6 -3
  94. package/src/core/dom.js +302 -0
  95. package/src/core/drag-axis.js +203 -0
  96. package/src/core/position.js +68 -22
  97. package/src/index.js +19 -2
  98. package/styles/tokens/dark.css +2 -3
  99. package/styles/tokens/global.css +5 -5
  100. package/styles/tokens/semantic.css +5 -3
  101. package/styles/tokens/themes.css +1 -1
  102. package/styles/zx.css +10 -1
  103. package/dist/types/components/permission/permission.d.ts +0 -96
  104. package/src/components/permission/permission.css +0 -47
  105. package/src/components/permission/permission.js +0 -201
@@ -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",
@@ -9074,6 +9124,82 @@ function onBreakpoint(handler, options = {}) {
9074
9124
  };
9075
9125
  }
9076
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
+
9077
9203
  // src/components/table/sort.js
9078
9204
  function compareStrings(left, right) {
9079
9205
  return String(left).localeCompare(String(right));
@@ -9129,7 +9255,8 @@ var Table = class extends Component {
9129
9255
  emptyText: null,
9130
9256
  rowClass: null,
9131
9257
  zebra: true,
9132
- editMode: false
9258
+ editMode: false,
9259
+ hierarchy: false
9133
9260
  };
9134
9261
  /**
9135
9262
  * Creates a table around a target or creates a new root when target is null.
@@ -9166,8 +9293,17 @@ var Table = class extends Component {
9166
9293
  this._rovingKey = null;
9167
9294
  this._rovingCell = null;
9168
9295
  this._rovingMatched = false;
9296
+ this._hierarchy = normalizeHierarchy(this.options.hierarchy, this._columns);
9297
+ this._expanded = initialExpanded(this._hierarchy?.expanded);
9169
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();
9170
9305
  this._table = h("table", { class: "zx-table__table" });
9306
+ if (this._hierarchy) this._table.setAttribute("role", "treegrid");
9171
9307
  this._colgroup = h("colgroup", { class: "zx-table__columns" });
9172
9308
  this._thead = h("thead", { class: "zx-table__head" });
9173
9309
  this._tbody = h("tbody", { class: "zx-table__body" });
@@ -9236,6 +9372,7 @@ var Table = class extends Component {
9236
9372
  assertRows(rows);
9237
9373
  this._data = [...rows];
9238
9374
  if (this._sort && this.options.sortMode === "local") this._sortData();
9375
+ this._reconcileExpanded();
9239
9376
  const selectionChanged = this._pruneSelection();
9240
9377
  this._renderBody();
9241
9378
  if (selectionChanged) this._emitSelectionChange();
@@ -9253,6 +9390,7 @@ var Table = class extends Component {
9253
9390
  assertRows(rows);
9254
9391
  this._data.push(...rows);
9255
9392
  if (this._sort && this.options.sortMode === "local") this._sortData();
9393
+ this._reconcileExpanded();
9256
9394
  this._renderBody();
9257
9395
  this._emitDataChange();
9258
9396
  return this;
@@ -9272,6 +9410,7 @@ var Table = class extends Component {
9272
9410
  this._data[index] = row;
9273
9411
  if (wasSelected) this._selected.add(this._idFor(row));
9274
9412
  if (this._sort && this.options.sortMode === "local") this._sortData();
9413
+ this._reconcileExpanded();
9275
9414
  this._renderBody();
9276
9415
  if (wasSelected && !Object.is(id, this._idFor(row))) this._emitSelectionChange();
9277
9416
  this._emitDataChange();
@@ -9289,6 +9428,7 @@ var Table = class extends Component {
9289
9428
  const index = this._data.findIndex((candidate) => Object.is(this._idFor(candidate), id));
9290
9429
  if (index < 0) return this;
9291
9430
  this._data.splice(index, 1);
9431
+ this._reconcileExpanded();
9292
9432
  const selectionChanged = this._selected.delete(id);
9293
9433
  if (Object.is(this._selectionAnchorId, id)) this._selectionAnchorId = null;
9294
9434
  this._renderBody();
@@ -9304,7 +9444,10 @@ var Table = class extends Component {
9304
9444
  getRow(id) {
9305
9445
  return this._data.find((row) => Object.is(this._idFor(row), id)) ?? null;
9306
9446
  }
9307
- /** @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
+ */
9308
9451
  getData() {
9309
9452
  return [...this._data];
9310
9453
  }
@@ -9318,6 +9461,7 @@ var Table = class extends Component {
9318
9461
  empty() {
9319
9462
  const selectionChanged = this._selected.size > 0;
9320
9463
  this._data = [];
9464
+ this._expanded.clear();
9321
9465
  this._selected.clear();
9322
9466
  this._selectionAnchorId = null;
9323
9467
  this._renderBody();
@@ -9352,7 +9496,10 @@ var Table = class extends Component {
9352
9496
  if (!options.silent) this.emit("sort", { id, dir: direction });
9353
9497
  return this;
9354
9498
  }
9355
- /** @returns {TableRow[]} Selected rows in current display order. */
9499
+ /**
9500
+ * Returns selected rows in current display order.
9501
+ * @returns {TableRow[]} Selected rows.
9502
+ */
9356
9503
  getSelection() {
9357
9504
  return this._data.filter((row) => this._selected.has(this._idFor(row)));
9358
9505
  }
@@ -9391,6 +9538,75 @@ var Table = class extends Component {
9391
9538
  this._emitSelectionChange();
9392
9539
  return this;
9393
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
+ }
9394
9610
  /**
9395
9611
  * Opens the editor for a cell. No-op unless the table runs with `editMode` and the column
9396
9612
  * resolves to an editable type for that row. In `'row'` mode every editable cell of the row is
@@ -9498,11 +9714,17 @@ var Table = class extends Component {
9498
9714
  this._abortEdit({ focus: true });
9499
9715
  return this;
9500
9716
  }
9501
- /** @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
+ */
9502
9721
  isEditing() {
9503
9722
  return this._edit !== null;
9504
9723
  }
9505
- /** @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
+ */
9506
9728
  getEditing() {
9507
9729
  return this._edit ? { id: this._edit.id, columnId: this._edit.columnId } : null;
9508
9730
  }
@@ -9511,7 +9733,31 @@ var Table = class extends Component {
9511
9733
  * @returns {TableRow[]}
9512
9734
  */
9513
9735
  _visibleData() {
9514
- 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);
9515
9761
  }
9516
9762
  /**
9517
9763
  * Renders the next batch of rows.
@@ -9523,10 +9769,11 @@ var Table = class extends Component {
9523
9769
  if (!this._growStep) return this;
9524
9770
  const step = Number(count) > 0 ? Math.trunc(Number(count)) : this._growStep;
9525
9771
  const before = this._rendered;
9526
- 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);
9527
9774
  if (this._rendered === before) return this;
9528
9775
  this._renderBody();
9529
- this.emit("grow", { rendered: this._rendered, total: this._data.length });
9776
+ this.emit("grow", { rendered: this._rendered, total });
9530
9777
  return this;
9531
9778
  }
9532
9779
  /**
@@ -9535,14 +9782,14 @@ var Table = class extends Component {
9535
9782
  * @fires Table#grow
9536
9783
  */
9537
9784
  showAll() {
9538
- return this.growBy(this._data.length);
9785
+ return this.growBy(this._projectedData().length);
9539
9786
  }
9540
9787
  /**
9541
9788
  * How many rows are currently rendered.
9542
9789
  * @returns {number}
9543
9790
  */
9544
9791
  getRenderedCount() {
9545
- return Math.min(this._rendered, this._data.length);
9792
+ return Math.min(this._rendered, this._projectedData().length);
9546
9793
  }
9547
9794
  /**
9548
9795
  * Keeps the "show more" control in step with what is rendered.
@@ -9554,7 +9801,7 @@ var Table = class extends Component {
9554
9801
  */
9555
9802
  _syncGrowing() {
9556
9803
  if (!this._growStep) return;
9557
- const remaining = this._data.length - this.getRenderedCount();
9804
+ const remaining = this._projectedData().length - this.getRenderedCount();
9558
9805
  if (remaining <= 0) {
9559
9806
  this._more?.remove();
9560
9807
  this._more = null;
@@ -9625,10 +9872,11 @@ var Table = class extends Component {
9625
9872
  * @returns {void}
9626
9873
  */
9627
9874
  _applyStackedRoles(stacked) {
9628
- 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"]];
9629
9876
  for (const [element, role] of parts) {
9630
9877
  if (!element) continue;
9631
9878
  if (stacked) element.setAttribute("role", role);
9879
+ else if (element === this._table && this._hierarchy) element.setAttribute("role", "treegrid");
9632
9880
  else element.removeAttribute("role");
9633
9881
  }
9634
9882
  for (const [selector, role] of [["tr", "row"], ["td", "cell"], ["th", "columnheader"]]) {
@@ -9659,9 +9907,9 @@ var Table = class extends Component {
9659
9907
  }
9660
9908
  /** @returns {void} */
9661
9909
  _renderColumns() {
9662
- const fragment = document.createDocumentFragment();
9910
+ const fragment2 = document.createDocumentFragment();
9663
9911
  if (this.options.selectable === "multi") {
9664
- fragment.append(h("col", { class: "zx-table__selection-column" }));
9912
+ fragment2.append(h("col", { class: "zx-table__selection-column" }));
9665
9913
  }
9666
9914
  const fractions = this._columns.map((column) => parseFraction(column.width));
9667
9915
  const total = fractions.reduce((sum, value) => sum + value, 0);
@@ -9670,9 +9918,9 @@ var Table = class extends Component {
9670
9918
  const col = h("col");
9671
9919
  if (fractions[index] > 0) col.style.width = `${fractions[index] / total * 100}%`;
9672
9920
  else if (column.width && column.width !== "auto") col.style.width = String(column.width);
9673
- fragment.append(col);
9921
+ fragment2.append(col);
9674
9922
  });
9675
- this._colgroup.replaceChildren(fragment);
9923
+ this._colgroup.replaceChildren(fragment2);
9676
9924
  }
9677
9925
  /** @returns {void} */
9678
9926
  _renderHeader() {
@@ -9705,7 +9953,8 @@ var Table = class extends Component {
9705
9953
  this._headers = /* @__PURE__ */ new Map();
9706
9954
  for (const column of this._columns) {
9707
9955
  const th = h("th", { scope: "col" });
9708
- if (column.align) th.dataset.align = column.align;
9956
+ const align = column.align ?? (isNumericColumn(column) ? "end" : null);
9957
+ if (align) th.dataset.align = align;
9709
9958
  if (column.headerTitle) th.title = column.headerTitle;
9710
9959
  if (column.sortable) {
9711
9960
  const button = h("button", {
@@ -9729,13 +9978,15 @@ var Table = class extends Component {
9729
9978
  /** @returns {void} @fires Table#editcancel */
9730
9979
  _renderBody() {
9731
9980
  this._abortEdit({ focus: false });
9732
- const fragment = document.createDocumentFragment();
9981
+ const fragment2 = document.createDocumentFragment();
9733
9982
  this._rowMeta = /* @__PURE__ */ new WeakMap();
9734
9983
  this._rowElements = /* @__PURE__ */ new Map();
9735
9984
  this._rovingCell = null;
9736
9985
  this._rovingMatched = false;
9986
+ const projected = this._projectedData();
9987
+ this._hierarchyMeta = new Map(projected.map((entry) => [entry.id, entry]));
9737
9988
  if (this._data.length === 0) {
9738
- fragment.append(h(
9989
+ fragment2.append(h(
9739
9990
  "tr",
9740
9991
  { class: "zx-table__empty-row" },
9741
9992
  h("td", {
@@ -9744,10 +9995,12 @@ var Table = class extends Component {
9744
9995
  }, this._emptyContent())
9745
9996
  ));
9746
9997
  } else {
9747
- 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
+ });
9748
10001
  }
9749
- this._tbody.replaceChildren(fragment);
9750
- 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));
9751
10004
  this._syncSelectAll();
9752
10005
  this._syncGrowing();
9753
10006
  if (this._stacked) this._applyStackedRoles(true);
@@ -9766,18 +10019,27 @@ var Table = class extends Component {
9766
10019
  if (empty == null) return this.msg("table.empty");
9767
10020
  return typeof empty === "function" ? empty() : empty;
9768
10021
  }
9769
- /** @param {TableRow} row @param {number} index @returns {HTMLTableRowElement} */
9770
- _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) {
9771
10029
  const id = this._idFor(row);
9772
10030
  const tr = (
9773
10031
  /** @type {HTMLTableRowElement} */
9774
10032
  h("tr", { dataset: { row: "" } })
9775
10033
  );
9776
- if (this._growStep) tr.setAttribute("aria-rowindex", String(index + 2));
10034
+ if (this._growStep) tr.setAttribute("aria-rowindex", String(tree.visibleIndex + 2));
9777
10035
  const rowClass = typeof this.options.rowClass === "function" ? this.options.rowClass(row) : "";
9778
10036
  if (rowClass) tr.classList.add(...String(rowClass).split(/\s+/).filter(Boolean));
9779
10037
  if (this.options.selectable !== false) tr.setAttribute("aria-selected", String(this._selected.has(id)));
9780
- 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 });
9781
10043
  const elements = this._rowElements.get(id) ?? [];
9782
10044
  elements.push(tr);
9783
10045
  this._rowElements.set(id, elements);
@@ -9795,7 +10057,8 @@ var Table = class extends Component {
9795
10057
  /** @type {HTMLTableCellElement} */
9796
10058
  h("td")
9797
10059
  );
9798
- if (column.align) cell.dataset.align = column.align;
10060
+ const align = column.align ?? (isNumericColumn(column) ? "end" : null);
10061
+ if (align) cell.dataset.align = align;
9799
10062
  cell.dataset.label = column.label;
9800
10063
  if (column.popin === false) cell.dataset.popin = "false";
9801
10064
  if (this._editMode !== false) {
@@ -9806,7 +10069,7 @@ var Table = class extends Component {
9806
10069
  this._offerRovingCell(cell, id, column.id);
9807
10070
  }
9808
10071
  }
9809
- this._renderCellContent(cell, column, row, index);
10072
+ this._renderCellContent(cell, column, row, index, tree);
9810
10073
  tr.append(cell);
9811
10074
  });
9812
10075
  return tr;
@@ -9817,13 +10080,33 @@ var Table = class extends Component {
9817
10080
  * @param {TableColumn} column Column definition.
9818
10081
  * @param {TableRow} row Row data.
9819
10082
  * @param {number} index Row index.
10083
+ * @param {TableHierarchyEntry} [tree] Hierarchy metadata for the row.
9820
10084
  * @returns {void}
9821
10085
  */
9822
- _renderCellContent(cell, column, row, index) {
9823
- const value = typeof column.render === "function" ? column.render(row, index) : row?.[column.id];
9824
- if (value && typeof value === "object" && typeof value.nodeType === "number") cell.replaceChildren(value);
9825
- else if (value != null) cell.replaceChildren(document.createTextNode(String(value)));
9826
- 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);
9827
10110
  }
9828
10111
  /** @param {MouseEvent} event @returns {void} */
9829
10112
  _handleBodyClick(event) {
@@ -9836,7 +10119,15 @@ var Table = class extends Component {
9836
10119
  if (!rowElement || !this._tbody.contains(rowElement)) return;
9837
10120
  const meta = this._rowMeta.get(rowElement);
9838
10121
  if (!meta) return;
9839
- 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
+ }
9840
10131
  const checkbox = target.closest(".zx-table__row-checkbox");
9841
10132
  if (checkbox && this.options.selectable === "multi") {
9842
10133
  this._toggleMultiSelection(
@@ -9932,7 +10223,15 @@ var Table = class extends Component {
9932
10223
  return entry;
9933
10224
  }
9934
10225
  if (type === "number") {
9935
- 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 });
9936
10235
  host.append(field.el);
9937
10236
  entry.component = field;
9938
10237
  entry.control = field.getInput();
@@ -10163,9 +10462,12 @@ var Table = class extends Component {
10163
10462
  _nextEditableCell(rowIndex, columnId, direction) {
10164
10463
  const columnCount = this._columns.length;
10165
10464
  if (rowIndex < 0 || columnCount === 0) return null;
10166
- 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;
10167
10469
  let column = this._columns.findIndex((candidate) => candidate.id === columnId);
10168
- const limit = this._data.length * columnCount + columnCount;
10470
+ const limit = visible.length * columnCount + columnCount;
10169
10471
  for (let step = 0; step < limit; step += 1) {
10170
10472
  column += direction;
10171
10473
  if (column >= columnCount) {
@@ -10175,11 +10477,15 @@ var Table = class extends Component {
10175
10477
  row -= 1;
10176
10478
  column = columnCount - 1;
10177
10479
  }
10178
- if (row < 0 || row >= this._data.length) return null;
10179
- const candidateRow = this._data[row];
10480
+ if (row < 0 || row >= visible.length) return null;
10481
+ const candidateRow = visible[row];
10180
10482
  const candidateColumn = this._columns[column];
10181
10483
  if (resolveEditable(candidateColumn, candidateRow) !== false) {
10182
- 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
+ };
10183
10489
  }
10184
10490
  }
10185
10491
  return null;
@@ -10246,14 +10552,16 @@ var Table = class extends Component {
10246
10552
  const message = this.msg(key);
10247
10553
  return message === key ? fallback : message;
10248
10554
  }
10249
- /** @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} */
10250
10556
  _toggleMultiSelection(meta, checked, shiftKey) {
10251
- 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));
10252
10559
  if (shiftKey && anchorIndex >= 0) {
10253
- const start = Math.min(anchorIndex, meta.index);
10254
- 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);
10255
10563
  for (let index = start; index <= end; index += 1) {
10256
- const id = this._idFor(this._data[index]);
10564
+ const id = this._idFor(visible[index]);
10257
10565
  if (checked) this._selected.add(id);
10258
10566
  else this._selected.delete(id);
10259
10567
  }
@@ -10304,7 +10612,34 @@ var Table = class extends Component {
10304
10612
  const column = this._columns.find((candidate) => candidate.id === this._sort.id);
10305
10613
  if (!column) return;
10306
10614
  const getValue = typeof column.sortValue === "function" ? column.sortValue : (row) => row?.[column.id];
10307
- 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);
10308
10643
  }
10309
10644
  /** @returns {boolean} Whether selection changed. */
10310
10645
  _pruneSelection() {
@@ -10328,6 +10663,65 @@ var Table = class extends Component {
10328
10663
  this.emit("datachange", { rows: this.getData() });
10329
10664
  }
10330
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
+ }
10331
10725
  function resolveEditable(column, row) {
10332
10726
  if (!column || typeof column !== "object") return false;
10333
10727
  if (typeof column.editor === "function") {
@@ -10335,7 +10729,7 @@ function resolveEditable(column, row) {
10335
10729
  return "custom";
10336
10730
  }
10337
10731
  const declared = typeof column.editable === "function" ? column.editable(row) : column.editable;
10338
- if (declared === true) return "text";
10732
+ if (declared === true) return isNumericColumn(column) ? "number" : "text";
10339
10733
  if (typeof declared === "string" && EDITOR_TYPES.has(declared)) return (
10340
10734
  /** @type {TableEditorType} */
10341
10735
  declared
@@ -10384,6 +10778,71 @@ function firstValidationError(values, columns, row, fallbackMessage = "Invalid v
10384
10778
  }
10385
10779
  return null;
10386
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
+ }
10387
10846
  function normalizeEditMode(mode) {
10388
10847
  if (mode === "cell" || mode === "row") return mode;
10389
10848
  return mode === true ? "cell" : false;
@@ -11951,7 +12410,7 @@ function buildZeyosTableConfig(client, resource, opts = {}) {
11951
12410
  const monetary = isMonetaryField(meta);
11952
12411
  const column = fieldToZxColumn(monetary ? { ...meta, format: "money" } : meta);
11953
12412
  if (monetary) {
11954
- column.render = (row) => formatCurrency(row?.[meta.id], row?.currency);
12413
+ column.render = (row) => formatCurrency2(row?.[meta.id], row?.currency);
11955
12414
  if (schemaHasField(client, resource, "currency")) projection.currency = "currency";
11956
12415
  }
11957
12416
  return column;
@@ -12076,10 +12535,10 @@ function inferLabelKey2(client, resource) {
12076
12535
  return names[0] ?? "ID";
12077
12536
  }
12078
12537
  function isMonetaryField(meta) {
12079
- const format = String(meta.format ?? "").toLowerCase();
12080
- 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());
12081
12540
  }
12082
- function formatCurrency(value, currency) {
12541
+ function formatCurrency2(value, currency) {
12083
12542
  if (value == null || value === "") return "";
12084
12543
  const number = Number(value);
12085
12544
  if (!Number.isFinite(number)) return "";