@zeyos/zx 2.3.0 → 4.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 (150) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/DESIGN-SYSTEM.md +96 -0
  3. package/README.md +29 -39
  4. package/dist/types/components/account-menu/account-menu.d.ts +144 -0
  5. package/dist/types/components/app-sidebar/app-sidebar.d.ts +243 -0
  6. package/dist/types/components/avatar/avatar.d.ts +100 -0
  7. package/dist/types/components/card/card.d.ts +128 -0
  8. package/dist/types/components/chart/chart.d.ts +322 -0
  9. package/dist/types/components/date-picker/month-picker.d.ts +6 -0
  10. package/dist/types/components/dialog/dialog.d.ts +9 -0
  11. package/dist/types/components/dock/dock.d.ts +495 -0
  12. package/dist/types/components/dropdown/dropdown.d.ts +2 -2
  13. package/dist/types/components/field/field.d.ts +14 -0
  14. package/dist/types/components/field-upload/field-upload.d.ts +37 -4
  15. package/dist/types/components/form/form.d.ts +25 -3
  16. package/dist/types/components/grid/grid.d.ts +172 -0
  17. package/dist/types/components/launcher/launcher.d.ts +359 -0
  18. package/dist/types/components/layout/layout.d.ts +4 -4
  19. package/dist/types/components/menu-button/menu-button.d.ts +10 -2
  20. package/dist/types/components/modal/modal.d.ts +25 -0
  21. package/dist/types/components/multi-value-editor/multi-value-editor.d.ts +7 -1
  22. package/dist/types/components/questionnaire/items.d.ts +327 -0
  23. package/dist/types/components/questionnaire/questionnaire.d.ts +449 -0
  24. package/dist/types/components/select/select.d.ts +51 -0
  25. package/dist/types/components/sheet/sheet-stack.d.ts +167 -0
  26. package/dist/types/components/sheet/sheet.d.ts +294 -0
  27. package/dist/types/components/split-view/split-view.d.ts +2 -21
  28. package/dist/types/components/table/table.d.ts +223 -15
  29. package/dist/types/components/tooltip/tooltip.d.ts +2 -2
  30. package/dist/types/components/value-list/value-list.d.ts +24 -3
  31. package/dist/types/core/dom.d.ts +122 -0
  32. package/dist/types/core/drag-axis.d.ts +111 -0
  33. package/dist/types/core/position.d.ts +4 -2
  34. package/dist/types/index.d.ts +13 -2
  35. package/dist/types/internal/app-rail.d.ts +265 -0
  36. package/dist/types/zeyos/icons.d.ts +5 -0
  37. package/dist/types/zeyos/index.d.ts +2 -1
  38. package/dist/types/zeyos/launcher.d.ts +147 -0
  39. package/dist/types/zeyos/select.d.ts +39 -0
  40. package/dist/zx-zeyos.esm.js +1870 -133
  41. package/dist/zx-zeyos.esm.min.js +1 -1
  42. package/dist/zx.css +2006 -267
  43. package/dist/zx.esm.js +8987 -2757
  44. package/dist/zx.esm.min.js +3 -3
  45. package/dist/zx.global.js +8987 -2757
  46. package/dist/zx.global.min.js +3 -3
  47. package/dist/zx.min.css +1 -1
  48. package/docs/api.json +4396 -2344
  49. package/docs/llms.md +463 -86
  50. package/docs/llms.txt +32 -19
  51. package/package.json +10 -17
  52. package/src/components/account-menu/account-menu.css +70 -0
  53. package/src/components/account-menu/account-menu.js +208 -0
  54. package/src/components/app-rail/app-rail.css +107 -0
  55. package/src/components/app-sidebar/app-sidebar.css +80 -0
  56. package/src/components/app-sidebar/app-sidebar.js +588 -0
  57. package/src/components/avatar/avatar.css +61 -0
  58. package/src/components/avatar/avatar.js +222 -0
  59. package/src/components/button/button.css +0 -1
  60. package/src/components/card/card.css +149 -0
  61. package/src/components/card/card.js +285 -0
  62. package/src/components/chart/chart.css +61 -0
  63. package/src/components/chart/chart.js +523 -0
  64. package/src/components/date-picker/month-picker.js +9 -0
  65. package/src/components/dialog/dialog.js +11 -4
  66. package/src/components/dock/dock.css +228 -0
  67. package/src/components/dock/dock.js +977 -0
  68. package/src/components/dropdown/dropdown.js +1 -1
  69. package/src/components/field/field.js +2 -0
  70. package/src/components/field-upload/field-upload.js +9 -4
  71. package/src/components/form/form.js +6 -3
  72. package/src/components/grid/grid.css +6 -0
  73. package/src/components/grid/grid.js +220 -0
  74. package/src/components/launcher/launcher.css +210 -0
  75. package/src/components/launcher/launcher.js +765 -0
  76. package/src/components/layout/layout.js +2 -2
  77. package/src/components/menu-button/menu-button.js +27 -2
  78. package/src/components/modal/modal.css +5 -1
  79. package/src/components/modal/modal.js +37 -2
  80. package/src/components/multi-value-editor/multi-value-editor.js +2 -1
  81. package/src/components/questionnaire/items.js +370 -0
  82. package/src/components/questionnaire/questionnaire.css +387 -0
  83. package/src/components/questionnaire/questionnaire.js +1033 -0
  84. package/src/components/select/field-adapter.js +9 -4
  85. package/src/components/select/select.css +144 -11
  86. package/src/components/select/select.js +259 -27
  87. package/src/components/sheet/sheet-stack.js +304 -0
  88. package/src/components/sheet/sheet.css +351 -0
  89. package/src/components/sheet/sheet.js +551 -0
  90. package/src/components/split-view/split-view.js +76 -161
  91. package/src/components/table/table.css +43 -0
  92. package/src/components/table/table.js +531 -52
  93. package/src/components/tooltip/tooltip.js +5 -2
  94. package/src/components/value-list/value-list.js +6 -3
  95. package/src/core/dom.js +322 -0
  96. package/src/core/drag-axis.js +203 -0
  97. package/src/core/position.js +68 -22
  98. package/src/index.js +20 -2
  99. package/src/internal/app-rail.js +718 -0
  100. package/src/zeyos/icons.js +8 -3
  101. package/src/zeyos/index.js +7 -1
  102. package/src/zeyos/launcher.js +338 -0
  103. package/src/zeyos/modules.js +1 -0
  104. package/src/zeyos/select.js +115 -1
  105. package/styles/tokens/dark.css +2 -3
  106. package/styles/tokens/global.css +6 -6
  107. package/styles/tokens/modules.css +1 -0
  108. package/styles/tokens/semantic.css +5 -3
  109. package/styles/tokens/themes.css +1 -1
  110. package/styles/zx.css +11 -1
  111. package/MIGRATION.md +0 -598
  112. package/dist/types/compat/base.d.ts +0 -156
  113. package/dist/types/compat/globals.d.ts +0 -20
  114. package/dist/types/compat/index.d.ts +0 -128
  115. package/dist/types/compat/map/basic.d.ts +0 -175
  116. package/dist/types/compat/map/data.d.ts +0 -102
  117. package/dist/types/compat/map/date-time.d.ts +0 -80
  118. package/dist/types/compat/map/factory.d.ts +0 -9
  119. package/dist/types/compat/map/form.d.ts +0 -98
  120. package/dist/types/compat/map/helpers.d.ts +0 -12
  121. package/dist/types/compat/map/http.d.ts +0 -36
  122. package/dist/types/compat/map/layout.d.ts +0 -106
  123. package/dist/types/compat/map/options.d.ts +0 -80
  124. package/dist/types/compat/map/select.d.ts +0 -95
  125. package/dist/types/compat/map/stubs.d.ts +0 -32
  126. package/dist/types/compat/map/utilities.d.ts +0 -37
  127. package/dist/types/compat-entry.d.ts +0 -5
  128. package/dist/types/components/permission/permission.d.ts +0 -96
  129. package/dist/zx-compat.esm.js +0 -17484
  130. package/dist/zx-compat.esm.min.js +0 -1
  131. package/dist/zx-compat.global.js +0 -23635
  132. package/dist/zx-compat.global.min.js +0 -3
  133. package/src/compat/base.js +0 -291
  134. package/src/compat/globals.js +0 -165
  135. package/src/compat/index.js +0 -84
  136. package/src/compat/map/basic.js +0 -464
  137. package/src/compat/map/data.js +0 -232
  138. package/src/compat/map/date-time.js +0 -177
  139. package/src/compat/map/factory.js +0 -45
  140. package/src/compat/map/form.js +0 -330
  141. package/src/compat/map/helpers.js +0 -46
  142. package/src/compat/map/http.js +0 -117
  143. package/src/compat/map/layout.js +0 -256
  144. package/src/compat/map/options.js +0 -267
  145. package/src/compat/map/select.js +0 -198
  146. package/src/compat/map/stubs.js +0 -26
  147. package/src/compat/map/utilities.js +0 -113
  148. package/src/compat-entry.js +0 -11
  149. package/src/components/permission/permission.css +0 -47
  150. 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;
@@ -524,6 +524,18 @@ function cloneValue(value) {
524
524
 
525
525
  // src/core/dom.js
526
526
  var activeRefs = null;
527
+ function safeHref(value) {
528
+ if (typeof value !== "string") return null;
529
+ const href = value.trim();
530
+ if (!href) return null;
531
+ try {
532
+ const protocol = new URL(href, globalThis.location?.href ?? "https://zx.invalid/").protocol;
533
+ if (["javascript:", "data:", "vbscript:"].includes(protocol.toLowerCase())) return null;
534
+ } catch {
535
+ return null;
536
+ }
537
+ return href;
538
+ }
527
539
  function h(tag, props, ...children) {
528
540
  const element = document.createElement(tag);
529
541
  let properties = props;
@@ -2970,6 +2982,14 @@ var MonthPicker = class extends DatePicker {
2970
2982
  static cssName = "month-picker";
2971
2983
  /** @type {MonthPickerOptions} */
2972
2984
  static defaults = { value: null, min: null, max: null };
2985
+ /**
2986
+ * Creates a month picker with its narrower month-based option contract.
2987
+ * @param {Element|string|null} target Existing element, selector, or null.
2988
+ * @param {MonthPickerOptions} [options={}] Month-picker options.
2989
+ */
2990
+ constructor(target, options = {}) {
2991
+ super(target, options);
2992
+ }
2973
2993
  /** @returns {HTMLElement} */
2974
2994
  render() {
2975
2995
  const root = (
@@ -3189,7 +3209,13 @@ var PLACEMENTS = /* @__PURE__ */ new Set([
3189
3209
  "top-start",
3190
3210
  "top-end",
3191
3211
  "bottom",
3192
- "top"
3212
+ "top",
3213
+ "right-start",
3214
+ "right-end",
3215
+ "left-start",
3216
+ "left-end",
3217
+ "right",
3218
+ "left"
3193
3219
  ]);
3194
3220
  function position(anchor, floating, options = {}) {
3195
3221
  if (!(anchor instanceof Element) || !(floating instanceof HTMLElement)) {
@@ -3217,13 +3243,15 @@ function position(anchor, floating, options = {}) {
3217
3243
  floating.style.inset = "auto";
3218
3244
  floating.style.margin = "0";
3219
3245
  floating.style.setProperty("position-anchor", anchorName);
3220
- floating.style.setProperty("position-area", positionArea(placement));
3221
- floating.style.setProperty("inset-area", positionArea(placement));
3246
+ const area = positionArea(placement, getComputedStyle(anchor).direction === "rtl");
3247
+ floating.style.setProperty("position-area", area);
3248
+ floating.style.setProperty("inset-area", area);
3222
3249
  floating.style.setProperty(
3223
3250
  "position-try-fallbacks",
3224
3251
  flip ? "flip-block, flip-inline, flip-block flip-inline" : "none"
3225
3252
  );
3226
- floating.style.setProperty(placement.startsWith("top") ? "margin-block-end" : "margin-block-start", `${offset}px`);
3253
+ const margin = placement.startsWith("top") ? "margin-bottom" : placement.startsWith("bottom") ? "margin-top" : placement.startsWith("left") ? "margin-right" : "margin-left";
3254
+ floating.style.setProperty(margin, `${offset}px`);
3227
3255
  update = () => {
3228
3256
  if (matchWidth) floating.style.minWidth = `${anchor.getBoundingClientRect().width}px`;
3229
3257
  };
@@ -3267,14 +3295,20 @@ function position(anchor, floating, options = {}) {
3267
3295
  }
3268
3296
  };
3269
3297
  }
3270
- function positionArea(placement) {
3298
+ function positionArea(placement, rtl) {
3271
3299
  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"
3300
+ "bottom-start": rtl ? "bottom span-left" : "bottom span-right",
3301
+ "bottom-end": rtl ? "bottom span-right" : "bottom span-left",
3302
+ "top-start": rtl ? "top span-left" : "top span-right",
3303
+ "top-end": rtl ? "top span-right" : "top span-left",
3304
+ "right-start": "right span-bottom",
3305
+ "right-end": "right span-top",
3306
+ "left-start": "left span-bottom",
3307
+ "left-end": "left span-top",
3308
+ bottom: "bottom",
3309
+ top: "top",
3310
+ right: "right",
3311
+ left: "left"
3278
3312
  }[placement];
3279
3313
  }
3280
3314
  function updateFallback(anchor, floating, { placement, offset, flip, matchWidth }) {
@@ -3284,26 +3318,54 @@ function updateFallback(anchor, floating, { placement, offset, flip, matchWidth
3284
3318
  const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
3285
3319
  const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
3286
3320
  let actualPlacement = placement;
3287
- const prefersBottom = placement.startsWith("bottom");
3321
+ const side = placementSide(placement);
3288
3322
  const bottomY = anchorRect.bottom + offset;
3289
3323
  const topY = anchorRect.top - offset - floatingRect.height;
3324
+ const rightX = anchorRect.right + offset;
3325
+ const leftX = anchorRect.left - offset - floatingRect.width;
3290
3326
  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");
3327
+ if (side === "bottom" && bottomY + floatingRect.height > viewportHeight && topY >= 0) {
3328
+ actualPlacement = flipPlacement(placement, "bottom", "top");
3329
+ } else if (side === "top" && topY < 0 && bottomY + floatingRect.height <= viewportHeight) {
3330
+ actualPlacement = flipPlacement(placement, "top", "bottom");
3331
+ } else if (side === "right" && rightX + floatingRect.width > viewportWidth && leftX >= 0) {
3332
+ actualPlacement = flipPlacement(placement, "right", "left");
3333
+ } else if (side === "left" && leftX < 0 && rightX + floatingRect.width <= viewportWidth) {
3334
+ actualPlacement = flipPlacement(placement, "left", "right");
3297
3335
  }
3298
3336
  }
3337
+ const actualSide = placementSide(actualPlacement);
3299
3338
  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;
3339
+ let top;
3340
+ if (actualSide === "bottom" || actualSide === "top") {
3341
+ const rtl = getComputedStyle(anchor).direction === "rtl";
3342
+ if (actualPlacement.endsWith("-end")) {
3343
+ left = rtl ? anchorRect.left : anchorRect.right - floatingRect.width;
3344
+ } else if (actualPlacement.endsWith("-start")) {
3345
+ left = rtl ? anchorRect.right - floatingRect.width : anchorRect.left;
3346
+ } else left = anchorRect.left + (anchorRect.width - floatingRect.width) / 2;
3347
+ top = actualSide === "bottom" ? bottomY : topY;
3348
+ } else {
3349
+ left = actualSide === "right" ? rightX : leftX;
3350
+ if (actualPlacement.endsWith("-end")) top = anchorRect.bottom - floatingRect.height;
3351
+ else if (actualPlacement.endsWith("-start")) top = anchorRect.top;
3352
+ else top = anchorRect.top + (anchorRect.height - floatingRect.height) / 2;
3353
+ }
3304
3354
  floating.style.left = `${Math.round(clamp(left, 0, Math.max(0, viewportWidth - floatingRect.width)))}px`;
3305
3355
  floating.style.top = `${Math.round(clamp(top, 0, Math.max(0, viewportHeight - floatingRect.height)))}px`;
3306
3356
  }
3357
+ function placementSide(placement) {
3358
+ return (
3359
+ /** @type {'top'|'bottom'|'left'|'right'} */
3360
+ placement.split("-")[0]
3361
+ );
3362
+ }
3363
+ function flipPlacement(placement, from, to) {
3364
+ return (
3365
+ /** @type {Placement} */
3366
+ placement.replace(from, to)
3367
+ );
3368
+ }
3307
3369
  function showPopover(element) {
3308
3370
  if (typeof element.showPopover !== "function") return;
3309
3371
  try {
@@ -4136,31 +4198,31 @@ var PLACEHOLDER_TOKENS = {
4136
4198
  "%Y": "YYYY",
4137
4199
  "%y": "YY"
4138
4200
  };
4139
- function parseRangeText(text3, format, separator) {
4201
+ function parseRangeText(text3, format2, separator) {
4140
4202
  const source = String(text3 ?? "").trim();
4141
4203
  if (source === "") return { start: null, end: null };
4142
4204
  const raw2 = String(separator ?? " \u2013 ");
4143
4205
  const core = raw2.trim();
4144
- const strict = core !== "" && String(format).includes(core) && /^\s|\s$/.test(raw2);
4206
+ const strict = core !== "" && String(format2).includes(core) && /^\s|\s$/.test(raw2);
4145
4207
  const lead = strict ? "\\s+" : "\\s*";
4146
4208
  const tail = strict ? "(?:\\s+|$)" : "\\s*";
4147
4209
  const parts = core === "" ? [source] : source.split(new RegExp(`${lead}${escapeRegExp(core)}${tail}`));
4148
4210
  if (parts.length > 2) return null;
4149
- const first = parseDate(parts[0].trim(), format);
4211
+ const first = parseDate(parts[0].trim(), format2);
4150
4212
  if (!first) return null;
4151
4213
  if (parts.length === 1) return { start: first, end: new Date(first.getTime()) };
4152
4214
  if (parts[1].trim() === "") return { start: first, end: null };
4153
- const second = parseDate(parts[1].trim(), format);
4215
+ const second = parseDate(parts[1].trim(), format2);
4154
4216
  if (!second) return null;
4155
4217
  return normalizeRange(first, second);
4156
4218
  }
4157
- function formatRangeText(range, format, separator) {
4219
+ function formatRangeText(range, format2, separator) {
4158
4220
  const start = range?.start ?? null;
4159
4221
  const end = range?.end ?? null;
4160
4222
  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)}`;
4223
+ if (!end) return formatDate(start, format2);
4224
+ if (!start) return formatDate(end, format2);
4225
+ return `${formatDate(start, format2)}${separator}${formatDate(end, format2)}`;
4164
4226
  }
4165
4227
  var DateRangeBox = class extends Component {
4166
4228
  static cssName = "date-range-box";
@@ -4818,12 +4880,12 @@ var DateTimeBox = function DateTimeBox2(target, options = {}) {
4818
4880
  function validDate4(value) {
4819
4881
  return value instanceof Date && !Number.isNaN(value.getTime()) ? new Date(value.getTime()) : null;
4820
4882
  }
4821
- function effectiveFormat(format, time) {
4822
- const value = String(format || "%d.%m.%Y");
4883
+ function effectiveFormat(format2, time) {
4884
+ const value = String(format2 || "%d.%m.%Y");
4823
4885
  return time && !/%[HM]/.test(value) ? `${value} %H:%M` : value;
4824
4886
  }
4825
- function placeholderFromFormat(format) {
4826
- return format.replace(/%[demYyHMS]/g, (token) => ({
4887
+ function placeholderFromFormat(format2) {
4888
+ return format2.replace(/%[demYyHMS]/g, (token) => ({
4827
4889
  "%d": "DD",
4828
4890
  "%e": "D",
4829
4891
  "%m": "MM",
@@ -6483,7 +6545,18 @@ var PRIORITY_KEYS = [
6483
6545
  "priority.high",
6484
6546
  "priority.highest"
6485
6547
  ];
6486
- var PRIORITY_FALLBACKS = ["Lowest", "Low", "Normal", "High", "Highest"];
6548
+ var PRIORITY_FALLBACKS = ["Lowest", "Low", "Medium", "High", "Highest"];
6549
+ var DEFAULT_STATUSES = Object.freeze([
6550
+ ["backlog", "status.backlog", "Backlog", "dotted", "muted"],
6551
+ ["todo", "status.todo", "Todo", "ring", "neutral"],
6552
+ ["in-progress", "status.inProgress", "In Progress", "progress", "warning"],
6553
+ ["in-review", "status.inReview", "In Review", "review", "info"],
6554
+ ["ready", "status.ready", "Ready to Merge", "ready", "accent"],
6555
+ ["waiting", "status.waiting", "Waiting for feedback", "waiting", "warning"],
6556
+ ["done", "status.done", "Done", "done", "info"],
6557
+ ["canceled", "status.canceled", "Canceled", "canceled", "muted"],
6558
+ ["duplicate", "status.duplicate", "Duplicate", "duplicate", "muted"]
6559
+ ]);
6487
6560
  var Select = class _Select extends Component {
6488
6561
  static cssName = "select";
6489
6562
  /** @type {SelectOptions} */
@@ -6494,6 +6567,10 @@ var Select = class _Select extends Component {
6494
6567
  labelKey: "name",
6495
6568
  renderItem: null,
6496
6569
  renderValue: null,
6570
+ renderValueAdornment: null,
6571
+ renderNone: null,
6572
+ noneLabel: null,
6573
+ actions: [],
6497
6574
  value: null,
6498
6575
  disabled: false,
6499
6576
  placeholder: "",
@@ -6511,6 +6588,7 @@ var Select = class _Select extends Component {
6511
6588
  this._visibleItems = [];
6512
6589
  this._fixedItems = [];
6513
6590
  this._visibleFixed = [];
6591
+ this._actions = [];
6514
6592
  this._selected = null;
6515
6593
  this._optionEntries = [];
6516
6594
  this._activeIndex = -1;
@@ -6549,6 +6627,12 @@ var Select = class _Select extends Component {
6549
6627
  placeholder: String(this.options.placeholder ?? ""),
6550
6628
  readOnly: !this.options.filter
6551
6629
  });
6630
+ const valueAdornment = h("span", {
6631
+ ref: "valueAdornment",
6632
+ class: "zx-select__value-adornment",
6633
+ ariaHidden: "true",
6634
+ hidden: true
6635
+ });
6552
6636
  const clear = h("button", {
6553
6637
  ref: "clear",
6554
6638
  class: "zx-icon-btn zx-select__clear",
@@ -6574,6 +6658,7 @@ var Select = class _Select extends Component {
6574
6658
  const control = h(
6575
6659
  "div",
6576
6660
  { ref: "control", class: "zx-select__control" },
6661
+ valueAdornment,
6577
6662
  input,
6578
6663
  clear,
6579
6664
  loading,
@@ -6596,9 +6681,10 @@ var Select = class _Select extends Component {
6596
6681
  this._visibleItems = this._items.slice();
6597
6682
  this._fixedItems = Array.isArray(this.options.fixedItems) ? this.options.fixedItems.slice() : [];
6598
6683
  this._visibleFixed = this._fixedItems.slice();
6684
+ this._actions = normalizeSelectActions(this.options.actions);
6599
6685
  this._disabled = Boolean(this.options.disabled);
6600
6686
  this._handleTypeahead = typeahead(
6601
- () => this._optionEntries.filter((entry) => !entry.clear).map((entry) => entry.element),
6687
+ () => this._optionEntries.filter((entry) => !entry.clear && !entry.action).map((entry) => entry.element),
6602
6688
  (_option, index) => {
6603
6689
  if (!this._open) this.open();
6604
6690
  this._setActive(this._itemEntryIndex(index));
@@ -6693,6 +6779,13 @@ var Select = class _Select extends Component {
6693
6779
  this._position?.update();
6694
6780
  return this;
6695
6781
  }
6782
+ /** Replaces command rows without changing the selected value. @param {SelectAction[]} actions @returns {this} */
6783
+ setActions(actions) {
6784
+ this._actions = normalizeSelectActions(actions);
6785
+ this._renderOptions();
6786
+ this._position?.update();
6787
+ return this;
6788
+ }
6696
6789
  /**
6697
6790
  * Restores the configured initial value.
6698
6791
  * @param {{silent?: boolean}} [options={}] Reset behavior.
@@ -6777,26 +6870,51 @@ var Select = class _Select extends Component {
6777
6870
  value: null,
6778
6871
  items: [],
6779
6872
  renderItem(item) {
6780
- return h(
6781
- "span",
6782
- { class: "zx-select__priority" },
6783
- h("span", {
6784
- class: "zx-select__priority-icon",
6785
- dataset: { level: item.level },
6786
- ariaHidden: "true"
6787
- }),
6788
- h("span", {}, String(item.name))
6789
- );
6873
+ return priorityContent(item, true);
6874
+ },
6875
+ renderValueAdornment(item) {
6876
+ return priorityIndicator(item.level);
6790
6877
  }
6791
6878
  });
6792
- select.setItems(PRIORITY_KEYS.map((key, level) => ({
6793
- ID: level,
6794
- name: select._message(key, PRIORITY_FALLBACKS[level]),
6795
- level
6796
- })));
6879
+ select.setItems(PRIORITY_KEYS.map((key, level) => {
6880
+ let name = level === 2 ? select.msg("priority.medium") : key;
6881
+ if (name === "priority.medium") name = select._message(key, PRIORITY_FALLBACKS[level]);
6882
+ else if (level !== 2) name = select._message(key, PRIORITY_FALLBACKS[level]);
6883
+ return { ID: level, name, level };
6884
+ }));
6797
6885
  select.set(requestedValue, { silent: true });
6798
6886
  return select;
6799
6887
  }
6888
+ /**
6889
+ * Creates a status picker with shape- and color-distinct workflow indicators.
6890
+ * @param {Element|string|null} target Component target.
6891
+ * @param {SelectOptions & {items?: Array<Record<string, any>>}} [options={}] Select overrides and status items.
6892
+ * @returns {Select}
6893
+ */
6894
+ static status(target, options = {}) {
6895
+ const { items = null, value = null, ...rest } = options;
6896
+ const select = new _Select(target, {
6897
+ ...rest,
6898
+ value: null,
6899
+ items: [],
6900
+ renderItem(item) {
6901
+ return statusContent(item);
6902
+ },
6903
+ renderValueAdornment(item) {
6904
+ return statusIndicator(item);
6905
+ }
6906
+ });
6907
+ const statuses = Array.isArray(items) ? items.map(normalizeStatusItem).filter(Boolean) : DEFAULT_STATUSES.map(([ID, key, fallback, indicator, tone], index) => ({
6908
+ ID,
6909
+ name: select._message(key, fallback),
6910
+ indicator,
6911
+ tone,
6912
+ shortcut: String(index + 1)
6913
+ }));
6914
+ select.setItems(statuses);
6915
+ select.set(value, { silent: true });
6916
+ return select;
6917
+ }
6800
6918
  /**
6801
6919
  * Creates the record-permission picker: the fixed Private and Public choices above the groups a
6802
6920
  * record can be shared with. The value is the tri-state ZeyOS stores — `false` for private,
@@ -6809,12 +6927,21 @@ var Select = class _Select extends Component {
6809
6927
  * @returns {Select}
6810
6928
  */
6811
6929
  static permission(target, options = {}) {
6812
- const { groups = [], value = true, valueKey = "ID", labelKey = "name", ...rest } = options;
6930
+ const {
6931
+ groups = [],
6932
+ value = true,
6933
+ valueKey = "ID",
6934
+ labelKey = "name",
6935
+ filter = "local",
6936
+ ...rest
6937
+ } = options;
6813
6938
  const loader = typeof groups === "function" ? groups : null;
6814
6939
  const shareable = loader ? [] : groups;
6815
6940
  const choices = [{ ID: false, name: "Private" }, { ID: true, name: "Public" }];
6816
6941
  const select = new _Select(target, {
6817
- filter: loader ?? "local",
6942
+ // An async group loader is the data source and cannot be disconnected by an incidental
6943
+ // `filter` override. Array-backed groups still honor local/disabled filtering.
6944
+ filter: loader ?? filter,
6818
6945
  ...rest,
6819
6946
  value: null,
6820
6947
  items: shareable,
@@ -6850,6 +6977,18 @@ var Select = class _Select extends Component {
6850
6977
  /** @param {KeyboardEvent} event @returns {void} */
6851
6978
  _onKeydown(event) {
6852
6979
  if (this._disabled) return;
6980
+ if (this._open && isPrintable(event)) {
6981
+ const shortcut = this._optionEntries.findIndex((entry) => !entry.clear && !entry.action && String(
6982
+ /** @type {Record<string, any>|null} */
6983
+ entry.item?.shortcut ?? ""
6984
+ ) === event.key);
6985
+ if (shortcut >= 0) {
6986
+ event.preventDefault();
6987
+ this._setActive(shortcut);
6988
+ this._chooseEntry(this._optionEntries[shortcut]);
6989
+ return;
6990
+ }
6991
+ }
6853
6992
  if (event.key === "ArrowDown") {
6854
6993
  event.preventDefault();
6855
6994
  if (!this._open) this.open();
@@ -6897,6 +7036,7 @@ var Select = class _Select extends Component {
6897
7036
  /** @type {HTMLInputElement} */
6898
7037
  this.refs.input.value
6899
7038
  );
7039
+ this._syncValueAdornment();
6900
7040
  if (!this._open) this.open();
6901
7041
  if (this.options.filter === "local") {
6902
7042
  this._visibleFixed = this._matchFixed(query);
@@ -6920,6 +7060,22 @@ var Select = class _Select extends Component {
6920
7060
  /** @param {SelectOptionEntry|undefined} entry @returns {void} */
6921
7061
  _chooseEntry(entry) {
6922
7062
  if (!entry) return;
7063
+ if (entry.action) {
7064
+ const detail = {
7065
+ id: entry.action.id,
7066
+ action: entry.action,
7067
+ query: (
7068
+ /** @type {HTMLInputElement} */
7069
+ this.refs.input.value
7070
+ ),
7071
+ select: this
7072
+ };
7073
+ const action = this.emit("action", detail);
7074
+ if (!action.defaultPrevented) entry.action.invoke?.(detail);
7075
+ this.close();
7076
+ this.focus();
7077
+ return;
7078
+ }
6923
7079
  this.set(entry.clear ? null : entry.item);
6924
7080
  this.close();
6925
7081
  this.focus();
@@ -6953,7 +7109,7 @@ var Select = class _Select extends Component {
6953
7109
  this._optionEntries = [];
6954
7110
  let lastGroup = /* @__PURE__ */ Symbol("initial-group");
6955
7111
  if (this.options.clearable) {
6956
- this._appendOption(null, true, this.options.placeholder || this._message("select.none", "No selection"));
7112
+ this._appendOption(null, true, this.options.noneLabel ?? this._message("select.none", "No selection"));
6957
7113
  }
6958
7114
  for (const item of this._visibleFixed) this._appendOption(item, false);
6959
7115
  if (this._visibleFixed.length > 0 && this._visibleItems.length > 0 && !this.options.groupKey) {
@@ -6972,7 +7128,23 @@ var Select = class _Select extends Component {
6972
7128
  }
6973
7129
  this._appendOption(item, false);
6974
7130
  }
6975
- if (this._visibleItems.length === 0 && this._visibleFixed.length === 0 && !this._loading) {
7131
+ if (this._actions.length > 0) {
7132
+ if (this._visibleFixed.length > 0 || this._visibleItems.length > 0 || this.options.clearable) {
7133
+ list.append(h("div", { class: "zx-select__separator", role: "presentation" }));
7134
+ }
7135
+ let actionGroup = /* @__PURE__ */ Symbol("initial-action-group");
7136
+ for (const action of this._actions) {
7137
+ if (action.group && action.group !== actionGroup) {
7138
+ list.append(h("div", {
7139
+ class: "zx-select__group",
7140
+ role: "presentation"
7141
+ }, action.group));
7142
+ actionGroup = action.group;
7143
+ }
7144
+ this._appendAction(action);
7145
+ }
7146
+ }
7147
+ if (this._visibleItems.length === 0 && this._visibleFixed.length === 0 && this._actions.length === 0 && !this._loading) {
6976
7148
  list.append(h("div", {
6977
7149
  class: "zx-select__empty",
6978
7150
  role: "status"
@@ -6984,7 +7156,7 @@ var Select = class _Select extends Component {
6984
7156
  _appendOption(item, clear, label) {
6985
7157
  const selected = clear ? this._selected === null : this._selected !== null && Object.is(this._itemValue(item), this.value);
6986
7158
  const index = this._optionEntries.length;
6987
- const content = clear ? String(label ?? "") : this._renderItem(item);
7159
+ const content = clear && typeof this.options.renderNone === "function" ? normalizeRenderedContent(this.options.renderNone(String(label ?? ""))) : clear ? String(label ?? "") : this._renderItem(item);
6988
7160
  const option = h("div", {
6989
7161
  class: "zx-select__option",
6990
7162
  id: uid("zx-select-option"),
@@ -6994,16 +7166,37 @@ var Select = class _Select extends Component {
6994
7166
  }, h("span", { class: "zx-select__option-content" }, content));
6995
7167
  if (selected) option.append(icon("check", { size: 15 }));
6996
7168
  this.refs.list.append(option);
6997
- this._optionEntries.push({ item, clear, element: option });
7169
+ this._optionEntries.push({ item, clear, action: null, element: option });
7170
+ }
7171
+ /** @param {SelectAction} action @returns {void} */
7172
+ _appendAction(action) {
7173
+ const index = this._optionEntries.length;
7174
+ const visual = selectActionIcon(action.icon);
7175
+ const option = h("div", {
7176
+ class: "zx-select__option zx-select__action",
7177
+ id: uid("zx-select-option"),
7178
+ role: "option",
7179
+ ariaSelected: "false",
7180
+ dataset: { index, kind: "action" }
7181
+ }, h(
7182
+ "span",
7183
+ { class: "zx-select__option-content zx-select__action-content" },
7184
+ visual ? h("span", { class: "zx-select__action-icon", ariaHidden: "true" }, visual) : null,
7185
+ h(
7186
+ "span",
7187
+ { class: "zx-select__action-copy" },
7188
+ h("span", { class: "zx-select__action-label" }, action.label),
7189
+ action.description ? h("span", { class: "zx-select__action-description" }, action.description) : null
7190
+ )
7191
+ ));
7192
+ this.refs.list.append(option);
7193
+ this._optionEntries.push({ item: null, clear: false, action, element: option });
6998
7194
  }
6999
7195
  /** @param {SelectItem|null} item @returns {Node|string} */
7000
7196
  _renderItem(item) {
7001
7197
  if (typeof this.options.renderItem === "function") {
7002
7198
  const rendered = this.options.renderItem(item);
7003
- return rendered && typeof rendered === "object" && "nodeType" in rendered ? (
7004
- /** @type {Node} */
7005
- rendered
7006
- ) : String(rendered ?? "");
7199
+ return normalizeRenderedContent(rendered);
7007
7200
  }
7008
7201
  return this._itemLabel(item);
7009
7202
  }
@@ -7074,9 +7267,29 @@ var Select = class _Select extends Component {
7074
7267
  this.refs.input
7075
7268
  );
7076
7269
  input.value = this._selected === null ? "" : typeof this.options.renderValue === "function" ? String(this.options.renderValue(this._selected) ?? "") : this._itemLabel(this._selected);
7270
+ this._syncValueAdornment();
7077
7271
  this.refs.clear.hidden = !this.options.clearable || this._selected === null;
7078
7272
  }
7079
7273
  /** @returns {void} */
7274
+ _syncValueAdornment() {
7275
+ const host = this.refs.valueAdornment;
7276
+ if (!host) return;
7277
+ const selectedLabel = this._selected === null ? "" : typeof this.options.renderValue === "function" ? String(this.options.renderValue(this._selected) ?? "") : this._itemLabel(this._selected);
7278
+ const query = String(
7279
+ /** @type {HTMLInputElement} */
7280
+ this.refs.input?.value ?? ""
7281
+ );
7282
+ if (this._selected === null || typeof this.options.renderValueAdornment !== "function" || Boolean(this.options.filter && query !== selectedLabel)) {
7283
+ host.replaceChildren();
7284
+ host.hidden = true;
7285
+ return;
7286
+ }
7287
+ const rendered = this.options.renderValueAdornment(this._selected);
7288
+ const content = normalizeRenderedContent(rendered);
7289
+ host.replaceChildren(content instanceof Node ? content : document.createTextNode(content));
7290
+ host.hidden = rendered == null || typeof content === "string" && content === "";
7291
+ }
7292
+ /** @returns {void} */
7080
7293
  _syncDisabled() {
7081
7294
  const input = (
7082
7295
  /** @type {HTMLInputElement} */
@@ -7153,6 +7366,82 @@ var Select = class _Select extends Component {
7153
7366
  return message === key ? fallback : message;
7154
7367
  }
7155
7368
  };
7369
+ function normalizeRenderedContent(rendered) {
7370
+ return rendered && typeof rendered === "object" && "nodeType" in rendered ? (
7371
+ /** @type {Node} */
7372
+ rendered
7373
+ ) : String(rendered ?? "");
7374
+ }
7375
+ function normalizeSelectActions(actions) {
7376
+ if (!Array.isArray(actions)) return [];
7377
+ return actions.filter((action) => action && typeof action === "object" && action.id != null && action.label != null).map((action) => ({
7378
+ ...action,
7379
+ label: String(action.label),
7380
+ description: action.description == null ? void 0 : String(action.description),
7381
+ group: action.group == null ? void 0 : String(action.group)
7382
+ }));
7383
+ }
7384
+ function selectActionIcon(visual) {
7385
+ if (typeof visual === "string") return icon(visual);
7386
+ if (visual instanceof Node) return visual.cloneNode(true);
7387
+ return null;
7388
+ }
7389
+ function priorityContent(item, label) {
7390
+ return h(
7391
+ "span",
7392
+ { class: "zx-select__priority" },
7393
+ priorityIndicator(item.level),
7394
+ label ? h("span", {}, String(item.name)) : null
7395
+ );
7396
+ }
7397
+ function priorityIndicator(level) {
7398
+ const normalized = Math.max(0, Math.min(4, Math.trunc(Number(level) || 0)));
7399
+ return h("span", {
7400
+ class: "zx-select__priority-icon",
7401
+ dataset: { level: normalized },
7402
+ ariaHidden: "true"
7403
+ }, Array.from({ length: 5 }, (_, index) => h("span", {
7404
+ dataset: { filled: String(index <= normalized) }
7405
+ })));
7406
+ }
7407
+ function normalizeStatusItem(input) {
7408
+ if (!input || typeof input !== "object" || input.ID == null || input.name == null) return null;
7409
+ const indicators = ["dotted", "ring", "progress", "review", "ready", "waiting", "done", "canceled", "duplicate"];
7410
+ const tones = ["muted", "neutral", "warning", "info", "accent", "success", "danger"];
7411
+ return {
7412
+ ...input,
7413
+ name: String(input.name),
7414
+ indicator: indicators.includes(String(input.indicator)) ? String(input.indicator) : "ring",
7415
+ tone: tones.includes(String(input.tone)) ? String(input.tone) : "muted",
7416
+ description: input.description == null ? "" : String(input.description),
7417
+ shortcut: input.shortcut == null ? "" : String(input.shortcut)
7418
+ };
7419
+ }
7420
+ function statusContent(item) {
7421
+ return h(
7422
+ "span",
7423
+ { class: "zx-select__status" },
7424
+ statusIndicator(item),
7425
+ h(
7426
+ "span",
7427
+ { class: "zx-select__status-copy" },
7428
+ h("span", { class: "zx-select__status-label" }, String(item.name)),
7429
+ item.description ? h("span", { class: "zx-select__status-description" }, item.description) : null
7430
+ ),
7431
+ item.shortcut ? h("kbd", { class: "zx-select__status-shortcut" }, item.shortcut) : null
7432
+ );
7433
+ }
7434
+ function statusIndicator(item) {
7435
+ const indicator = String(item.indicator ?? "ring");
7436
+ const marker = h("span", {
7437
+ class: "zx-select__status-indicator",
7438
+ dataset: { indicator, tone: String(item.tone ?? "muted") },
7439
+ ariaHidden: "true"
7440
+ });
7441
+ if (indicator === "done") marker.append(icon("check", { size: 8 }));
7442
+ if (indicator === "canceled") marker.append(icon("x", { size: 7 }));
7443
+ return marker;
7444
+ }
7156
7445
  function readItem(item, reader) {
7157
7446
  if (typeof reader === "function") return reader(item);
7158
7447
  if (item === null || typeof item !== "object") return item;
@@ -7165,10 +7454,11 @@ function isPrintable(event) {
7165
7454
  // src/components/select/field-adapter.js
7166
7455
  function registerSelectFieldAdapter() {
7167
7456
  Field.register("zxselect", (field, options) => {
7168
- const select = field.own(new Select(null, {
7169
- ...options.props ?? {},
7170
- value: options.value
7171
- }));
7457
+ const props = options.props ?? {};
7458
+ const preset = props.preset;
7459
+ const selectOptions = { ...props, value: options.value };
7460
+ delete selectOptions.preset;
7461
+ const select = field.own(preset === "priority" ? Select.priority(null, selectOptions) : preset === "status" ? Select.status(null, selectOptions) : new Select(null, selectOptions));
7172
7462
  field.listen(select.el, "change", (event) => event.stopImmediatePropagation());
7173
7463
  select.on("change", (event) => field.emit("change", { value: event.detail.value }));
7174
7464
  return {
@@ -9074,6 +9364,82 @@ function onBreakpoint(handler, options = {}) {
9074
9364
  };
9075
9365
  }
9076
9366
 
9367
+ // src/core/format.js
9368
+ var MIN_DIGITS = 0;
9369
+ var MAX_DIGITS = 20;
9370
+ var DEFAULT_MAX_DIGITS = 3;
9371
+ var numberFormatters = /* @__PURE__ */ new Map();
9372
+ function formatNumber(value, options = {}) {
9373
+ const number = toFiniteNumber(value);
9374
+ if (number === null) return "";
9375
+ const { locale = getLanguage(), decimals, minDecimals, maxDecimals, group = true } = options;
9376
+ return format(locale, digitOptions({ decimals, minDecimals, maxDecimals, group }), number);
9377
+ }
9378
+ function formatCurrency(value, currency, options = {}) {
9379
+ const number = toFiniteNumber(value);
9380
+ if (number === null) return "";
9381
+ const { locale = getLanguage(), decimals } = options;
9382
+ const code = currencyCode(currency);
9383
+ if (code === null) return formatNumber(number, { locale, decimals });
9384
+ const digits = digitOptions({ decimals, group: true });
9385
+ return format(locale, { ...digits, style: "currency", currency: code }, number);
9386
+ }
9387
+ function formatPercent2(value, options = {}) {
9388
+ const number = toFiniteNumber(value);
9389
+ if (number === null) return "";
9390
+ const { locale = getLanguage(), decimals = 0 } = options;
9391
+ return format(locale, { ...digitOptions({ decimals, group: true }), style: "percent" }, number);
9392
+ }
9393
+ function format(locale, options, value) {
9394
+ const key = `${locale}\0${JSON.stringify(options)}`;
9395
+ let formatter = numberFormatters.get(key);
9396
+ if (formatter === void 0) {
9397
+ formatter = createNumberFormat(locale, options);
9398
+ numberFormatters.set(key, formatter);
9399
+ }
9400
+ return formatter.format(value);
9401
+ }
9402
+ function createNumberFormat(locale, options) {
9403
+ try {
9404
+ return new Intl.NumberFormat(locale, options);
9405
+ } catch {
9406
+ try {
9407
+ return new Intl.NumberFormat(void 0, options);
9408
+ } catch {
9409
+ return new Intl.NumberFormat();
9410
+ }
9411
+ }
9412
+ }
9413
+ function digitOptions({ decimals, minDecimals, maxDecimals, group = true }) {
9414
+ const result = { useGrouping: group !== false };
9415
+ const min = digitCount(minDecimals ?? decimals);
9416
+ const max = digitCount(maxDecimals ?? decimals);
9417
+ if (min !== null) result.minimumFractionDigits = min;
9418
+ if (max !== null) {
9419
+ result.maximumFractionDigits = max;
9420
+ if (min !== null && min > max) result.minimumFractionDigits = max;
9421
+ } else if (min !== null) {
9422
+ result.maximumFractionDigits = Math.max(min, DEFAULT_MAX_DIGITS);
9423
+ }
9424
+ return result;
9425
+ }
9426
+ function digitCount(value) {
9427
+ if (value == null) return null;
9428
+ const number = Number(value);
9429
+ if (!Number.isFinite(number)) return null;
9430
+ return Math.min(MAX_DIGITS, Math.max(MIN_DIGITS, Math.round(number)));
9431
+ }
9432
+ function toFiniteNumber(value) {
9433
+ if (value === null || value === void 0 || value === "") return null;
9434
+ const number = typeof value === "number" ? value : Number(value);
9435
+ return Number.isFinite(number) ? number : null;
9436
+ }
9437
+ function currencyCode(value) {
9438
+ if (typeof value !== "string") return null;
9439
+ const code = value.trim().toUpperCase();
9440
+ return /^[A-Z]{3}$/.test(code) ? code : null;
9441
+ }
9442
+
9077
9443
  // src/components/table/sort.js
9078
9444
  function compareStrings(left, right) {
9079
9445
  return String(left).localeCompare(String(right));
@@ -9129,7 +9495,8 @@ var Table = class extends Component {
9129
9495
  emptyText: null,
9130
9496
  rowClass: null,
9131
9497
  zebra: true,
9132
- editMode: false
9498
+ editMode: false,
9499
+ hierarchy: false
9133
9500
  };
9134
9501
  /**
9135
9502
  * Creates a table around a target or creates a new root when target is null.
@@ -9141,12 +9508,14 @@ var Table = class extends Component {
9141
9508
  }
9142
9509
  /** @returns {HTMLElement} Table root. */
9143
9510
  render() {
9511
+ const created = this.el === null;
9144
9512
  const root = (
9145
9513
  /** @type {HTMLElement} */
9146
9514
  this.el ?? h("div")
9147
9515
  );
9148
9516
  this.el = root;
9149
- this._originalChildren = this.el ? Array.from(this.el.childNodes) : null;
9517
+ this._createdRoot = created;
9518
+ this._original = created ? null : snapshotTarget(root);
9150
9519
  this._columns = Array.isArray(this.options.columns) ? this.options.columns.map((column) => ({ ...column })) : [];
9151
9520
  this._data = Array.isArray(this.options.data) ? [...this.options.data] : [];
9152
9521
  this._selected = /* @__PURE__ */ new Set();
@@ -9166,8 +9535,17 @@ var Table = class extends Component {
9166
9535
  this._rovingKey = null;
9167
9536
  this._rovingCell = null;
9168
9537
  this._rovingMatched = false;
9538
+ this._hierarchy = normalizeHierarchy(this.options.hierarchy, this._columns);
9539
+ this._expanded = initialExpanded(this._hierarchy?.expanded);
9169
9540
  if (this._sort && this.options.sortMode === "local") this._sortData();
9541
+ if (this._hierarchy?.expanded === true) {
9542
+ for (const id of hierarchyBranchIds(this._data, this.options.rowId, this._hierarchy.parentId)) {
9543
+ this._expanded.add(id);
9544
+ }
9545
+ }
9546
+ this._reconcileExpanded();
9170
9547
  this._table = h("table", { class: "zx-table__table" });
9548
+ if (this._hierarchy) this._table.setAttribute("role", "treegrid");
9171
9549
  this._colgroup = h("colgroup", { class: "zx-table__columns" });
9172
9550
  this._thead = h("thead", { class: "zx-table__head" });
9173
9551
  this._tbody = h("tbody", { class: "zx-table__body" });
@@ -9212,8 +9590,8 @@ var Table = class extends Component {
9212
9590
  }
9213
9591
  /**
9214
9592
  * Toggles a loading state. While loading, the body is dimmed, an indeterminate progress bar is
9215
- * shown, and `aria-busy` is set — a lightweight take on the busy/skeleton pattern used by
9216
- * enterprise tables (SAP UI5, Salesforce SLDS). Call `setLoading(false)` (or `setData`) when done.
9593
+ * shown, and `aria-busy` is set — a lightweight enterprise-table busy and skeleton pattern.
9594
+ * Call `setLoading(false)` (or `setData`) when done.
9217
9595
  * @param {boolean} [loading=true] Whether the table is loading.
9218
9596
  * @returns {this}
9219
9597
  */
@@ -9236,6 +9614,7 @@ var Table = class extends Component {
9236
9614
  assertRows(rows);
9237
9615
  this._data = [...rows];
9238
9616
  if (this._sort && this.options.sortMode === "local") this._sortData();
9617
+ this._reconcileExpanded();
9239
9618
  const selectionChanged = this._pruneSelection();
9240
9619
  this._renderBody();
9241
9620
  if (selectionChanged) this._emitSelectionChange();
@@ -9253,6 +9632,7 @@ var Table = class extends Component {
9253
9632
  assertRows(rows);
9254
9633
  this._data.push(...rows);
9255
9634
  if (this._sort && this.options.sortMode === "local") this._sortData();
9635
+ this._reconcileExpanded();
9256
9636
  this._renderBody();
9257
9637
  this._emitDataChange();
9258
9638
  return this;
@@ -9272,6 +9652,7 @@ var Table = class extends Component {
9272
9652
  this._data[index] = row;
9273
9653
  if (wasSelected) this._selected.add(this._idFor(row));
9274
9654
  if (this._sort && this.options.sortMode === "local") this._sortData();
9655
+ this._reconcileExpanded();
9275
9656
  this._renderBody();
9276
9657
  if (wasSelected && !Object.is(id, this._idFor(row))) this._emitSelectionChange();
9277
9658
  this._emitDataChange();
@@ -9289,6 +9670,7 @@ var Table = class extends Component {
9289
9670
  const index = this._data.findIndex((candidate) => Object.is(this._idFor(candidate), id));
9290
9671
  if (index < 0) return this;
9291
9672
  this._data.splice(index, 1);
9673
+ this._reconcileExpanded();
9292
9674
  const selectionChanged = this._selected.delete(id);
9293
9675
  if (Object.is(this._selectionAnchorId, id)) this._selectionAnchorId = null;
9294
9676
  this._renderBody();
@@ -9304,7 +9686,10 @@ var Table = class extends Component {
9304
9686
  getRow(id) {
9305
9687
  return this._data.find((row) => Object.is(this._idFor(row), id)) ?? null;
9306
9688
  }
9307
- /** @returns {TableRow[]} Shallow copy of current rows in display order. */
9689
+ /**
9690
+ * Returns a shallow copy of the current rows in display order.
9691
+ * @returns {TableRow[]} Current table rows.
9692
+ */
9308
9693
  getData() {
9309
9694
  return [...this._data];
9310
9695
  }
@@ -9318,6 +9703,7 @@ var Table = class extends Component {
9318
9703
  empty() {
9319
9704
  const selectionChanged = this._selected.size > 0;
9320
9705
  this._data = [];
9706
+ this._expanded.clear();
9321
9707
  this._selected.clear();
9322
9708
  this._selectionAnchorId = null;
9323
9709
  this._renderBody();
@@ -9352,7 +9738,10 @@ var Table = class extends Component {
9352
9738
  if (!options.silent) this.emit("sort", { id, dir: direction });
9353
9739
  return this;
9354
9740
  }
9355
- /** @returns {TableRow[]} Selected rows in current display order. */
9741
+ /**
9742
+ * Returns selected rows in current display order.
9743
+ * @returns {TableRow[]} Selected rows.
9744
+ */
9356
9745
  getSelection() {
9357
9746
  return this._data.filter((row) => this._selected.has(this._idFor(row)));
9358
9747
  }
@@ -9391,6 +9780,75 @@ var Table = class extends Component {
9391
9780
  this._emitSelectionChange();
9392
9781
  return this;
9393
9782
  }
9783
+ /**
9784
+ * Toggles one hierarchy branch. A leaf or an id absent from the table is a no-op.
9785
+ * @param {unknown} id Row id.
9786
+ * @param {{silent?: boolean}} [options={}] Event behavior.
9787
+ * @returns {this}
9788
+ * @fires Table#rowtoggle
9789
+ */
9790
+ toggleRow(id, options = {}) {
9791
+ return this._expanded.has(id) ? this.collapseRow(id, options) : this.expandRow(id, options);
9792
+ }
9793
+ /**
9794
+ * Expands one hierarchy branch.
9795
+ * @param {unknown} id Row id.
9796
+ * @param {{silent?: boolean}} [options={}] Event behavior.
9797
+ * @returns {this}
9798
+ * @fires Table#rowtoggle
9799
+ */
9800
+ expandRow(id, options = {}) {
9801
+ return this._setRowExpanded(id, true, options);
9802
+ }
9803
+ /**
9804
+ * Collapses one hierarchy branch without discarding its descendants or their expansion state.
9805
+ * @param {unknown} id Row id.
9806
+ * @param {{silent?: boolean}} [options={}] Event behavior.
9807
+ * @returns {this}
9808
+ * @fires Table#rowtoggle
9809
+ */
9810
+ collapseRow(id, options = {}) {
9811
+ return this._setRowExpanded(id, false, options);
9812
+ }
9813
+ /**
9814
+ * Expands every row that currently has children.
9815
+ * @param {{silent?: boolean}} [options={}] Event behavior. When not silent, one `rowtoggle`
9816
+ * event is emitted per branch whose state changed.
9817
+ * @returns {this}
9818
+ */
9819
+ expandAll(options = {}) {
9820
+ if (!this._hierarchy) return this;
9821
+ const branchIds = hierarchyBranchIds(this._data, this.options.rowId, this._hierarchy.parentId);
9822
+ const changed = branchIds.filter((id) => !this._expanded.has(id));
9823
+ for (const id of changed) this._expanded.add(id);
9824
+ if (changed.length) this._renderBody();
9825
+ if (!options.silent) {
9826
+ for (const id of changed) this._emitRowToggle(id, true);
9827
+ }
9828
+ return this;
9829
+ }
9830
+ /**
9831
+ * Collapses every hierarchy branch.
9832
+ * @param {{silent?: boolean}} [options={}] Event behavior.
9833
+ * @returns {this}
9834
+ */
9835
+ collapseAll(options = {}) {
9836
+ if (!this._hierarchy || this._expanded.size === 0) return this;
9837
+ const changed = [...this._expanded];
9838
+ this._expanded.clear();
9839
+ this._renderBody();
9840
+ if (!options.silent) {
9841
+ for (const id of changed) this._emitRowToggle(id, false);
9842
+ }
9843
+ return this;
9844
+ }
9845
+ /**
9846
+ * Returns expanded branch ids in current flat-data order.
9847
+ * @returns {unknown[]} Expanded branch ids.
9848
+ */
9849
+ getExpanded() {
9850
+ return this._data.map((row) => this._idFor(row)).filter((id) => this._expanded.has(id));
9851
+ }
9394
9852
  /**
9395
9853
  * Opens the editor for a cell. No-op unless the table runs with `editMode` and the column
9396
9854
  * resolves to an editable type for that row. In `'row'` mode every editable cell of the row is
@@ -9498,11 +9956,17 @@ var Table = class extends Component {
9498
9956
  this._abortEdit({ focus: true });
9499
9957
  return this;
9500
9958
  }
9501
- /** @returns {boolean} Whether a cell or row is currently open for editing. */
9959
+ /**
9960
+ * Reports whether a cell or row is currently open for editing.
9961
+ * @returns {boolean} Whether an edit is open.
9962
+ */
9502
9963
  isEditing() {
9503
9964
  return this._edit !== null;
9504
9965
  }
9505
- /** @returns {{id: unknown, columnId: string}|null} Row id and focused column id, or null. */
9966
+ /**
9967
+ * Returns the row id and focused column id for the open edit.
9968
+ * @returns {{id: unknown, columnId: string}|null} Editing location, or null.
9969
+ */
9506
9970
  getEditing() {
9507
9971
  return this._edit ? { id: this._edit.id, columnId: this._edit.columnId } : null;
9508
9972
  }
@@ -9511,7 +9975,31 @@ var Table = class extends Component {
9511
9975
  * @returns {TableRow[]}
9512
9976
  */
9513
9977
  _visibleData() {
9514
- return this._rendered === Infinity ? this._data : this._data.slice(0, this._rendered);
9978
+ return this._visibleEntries().map((entry) => entry.row);
9979
+ }
9980
+ /** @returns {TableHierarchyEntry[]} Rows visible after hierarchy expansion, before growing. */
9981
+ _projectedData() {
9982
+ if (!this._hierarchy) {
9983
+ return this._data.map((row, index) => ({
9984
+ row,
9985
+ id: this._idFor(row),
9986
+ index,
9987
+ depth: 0,
9988
+ hasChildren: false,
9989
+ parentId: null
9990
+ }));
9991
+ }
9992
+ return projectHierarchyRows(
9993
+ this._data,
9994
+ this.options.rowId,
9995
+ this._hierarchy.parentId,
9996
+ this._expanded
9997
+ );
9998
+ }
9999
+ /** @returns {TableHierarchyEntry[]} Rows that currently have DOM entries. */
10000
+ _visibleEntries() {
10001
+ const projected = this._projectedData();
10002
+ return this._rendered === Infinity ? projected : projected.slice(0, this._rendered);
9515
10003
  }
9516
10004
  /**
9517
10005
  * Renders the next batch of rows.
@@ -9523,10 +10011,11 @@ var Table = class extends Component {
9523
10011
  if (!this._growStep) return this;
9524
10012
  const step = Number(count) > 0 ? Math.trunc(Number(count)) : this._growStep;
9525
10013
  const before = this._rendered;
9526
- this._rendered = Math.min(this._data.length, this._rendered + step);
10014
+ const total = this._projectedData().length;
10015
+ this._rendered = Math.min(total, this._rendered + step);
9527
10016
  if (this._rendered === before) return this;
9528
10017
  this._renderBody();
9529
- this.emit("grow", { rendered: this._rendered, total: this._data.length });
10018
+ this.emit("grow", { rendered: this._rendered, total });
9530
10019
  return this;
9531
10020
  }
9532
10021
  /**
@@ -9535,14 +10024,14 @@ var Table = class extends Component {
9535
10024
  * @fires Table#grow
9536
10025
  */
9537
10026
  showAll() {
9538
- return this.growBy(this._data.length);
10027
+ return this.growBy(this._projectedData().length);
9539
10028
  }
9540
10029
  /**
9541
10030
  * How many rows are currently rendered.
9542
10031
  * @returns {number}
9543
10032
  */
9544
10033
  getRenderedCount() {
9545
- return Math.min(this._rendered, this._data.length);
10034
+ return Math.min(this._rendered, this._projectedData().length);
9546
10035
  }
9547
10036
  /**
9548
10037
  * Keeps the "show more" control in step with what is rendered.
@@ -9554,7 +10043,7 @@ var Table = class extends Component {
9554
10043
  */
9555
10044
  _syncGrowing() {
9556
10045
  if (!this._growStep) return;
9557
- const remaining = this._data.length - this.getRenderedCount();
10046
+ const remaining = this._projectedData().length - this.getRenderedCount();
9558
10047
  if (remaining <= 0) {
9559
10048
  this._more?.remove();
9560
10049
  this._more = null;
@@ -9625,10 +10114,11 @@ var Table = class extends Component {
9625
10114
  * @returns {void}
9626
10115
  */
9627
10116
  _applyStackedRoles(stacked) {
9628
- const parts = [[this._table, "table"], [this._thead, "rowgroup"], [this._tbody, "rowgroup"]];
10117
+ const parts = [[this._table, this._hierarchy ? "treegrid" : "table"], [this._thead, "rowgroup"], [this._tbody, "rowgroup"]];
9629
10118
  for (const [element, role] of parts) {
9630
10119
  if (!element) continue;
9631
10120
  if (stacked) element.setAttribute("role", role);
10121
+ else if (element === this._table && this._hierarchy) element.setAttribute("role", "treegrid");
9632
10122
  else element.removeAttribute("role");
9633
10123
  }
9634
10124
  for (const [selector, role] of [["tr", "row"], ["td", "cell"], ["th", "columnheader"]]) {
@@ -9650,18 +10140,17 @@ var Table = class extends Component {
9650
10140
  this._teardownEdit();
9651
10141
  this._width?.destroy();
9652
10142
  this._width = null;
9653
- if (this.el) delete this.el.dataset.editMode;
9654
- if (!this._restored && this._originalChildren) {
9655
- this.el.replaceChildren(...this._originalChildren);
10143
+ if (!this._restored && !this._createdRoot) {
10144
+ restoreTarget(this.el, this._original);
9656
10145
  this._restored = true;
9657
10146
  }
9658
10147
  super.destroy();
9659
10148
  }
9660
10149
  /** @returns {void} */
9661
10150
  _renderColumns() {
9662
- const fragment = document.createDocumentFragment();
10151
+ const fragment2 = document.createDocumentFragment();
9663
10152
  if (this.options.selectable === "multi") {
9664
- fragment.append(h("col", { class: "zx-table__selection-column" }));
10153
+ fragment2.append(h("col", { class: "zx-table__selection-column" }));
9665
10154
  }
9666
10155
  const fractions = this._columns.map((column) => parseFraction(column.width));
9667
10156
  const total = fractions.reduce((sum, value) => sum + value, 0);
@@ -9670,9 +10159,9 @@ var Table = class extends Component {
9670
10159
  const col = h("col");
9671
10160
  if (fractions[index] > 0) col.style.width = `${fractions[index] / total * 100}%`;
9672
10161
  else if (column.width && column.width !== "auto") col.style.width = String(column.width);
9673
- fragment.append(col);
10162
+ fragment2.append(col);
9674
10163
  });
9675
- this._colgroup.replaceChildren(fragment);
10164
+ this._colgroup.replaceChildren(fragment2);
9676
10165
  }
9677
10166
  /** @returns {void} */
9678
10167
  _renderHeader() {
@@ -9705,7 +10194,8 @@ var Table = class extends Component {
9705
10194
  this._headers = /* @__PURE__ */ new Map();
9706
10195
  for (const column of this._columns) {
9707
10196
  const th = h("th", { scope: "col" });
9708
- if (column.align) th.dataset.align = column.align;
10197
+ const align = column.align ?? (isNumericColumn(column) ? "end" : null);
10198
+ if (align) th.dataset.align = align;
9709
10199
  if (column.headerTitle) th.title = column.headerTitle;
9710
10200
  if (column.sortable) {
9711
10201
  const button = h("button", {
@@ -9729,13 +10219,15 @@ var Table = class extends Component {
9729
10219
  /** @returns {void} @fires Table#editcancel */
9730
10220
  _renderBody() {
9731
10221
  this._abortEdit({ focus: false });
9732
- const fragment = document.createDocumentFragment();
10222
+ const fragment2 = document.createDocumentFragment();
9733
10223
  this._rowMeta = /* @__PURE__ */ new WeakMap();
9734
10224
  this._rowElements = /* @__PURE__ */ new Map();
9735
10225
  this._rovingCell = null;
9736
10226
  this._rovingMatched = false;
10227
+ const projected = this._projectedData();
10228
+ this._hierarchyMeta = new Map(projected.map((entry) => [entry.id, entry]));
9737
10229
  if (this._data.length === 0) {
9738
- fragment.append(h(
10230
+ fragment2.append(h(
9739
10231
  "tr",
9740
10232
  { class: "zx-table__empty-row" },
9741
10233
  h("td", {
@@ -9744,10 +10236,12 @@ var Table = class extends Component {
9744
10236
  }, this._emptyContent())
9745
10237
  ));
9746
10238
  } else {
9747
- this._visibleData().forEach((row, index) => fragment.append(this._createRow(row, index)));
10239
+ this._visibleEntries().forEach((entry, visibleIndex) => {
10240
+ fragment2.append(this._createRow(entry.row, entry.index, { ...entry, visibleIndex }));
10241
+ });
9748
10242
  }
9749
- this._tbody.replaceChildren(fragment);
9750
- if (this._growStep) this._table.setAttribute("aria-rowcount", String(this._data.length + 1));
10243
+ this._tbody.replaceChildren(fragment2);
10244
+ if (this._growStep) this._table.setAttribute("aria-rowcount", String(projected.length + 1));
9751
10245
  this._syncSelectAll();
9752
10246
  this._syncGrowing();
9753
10247
  if (this._stacked) this._applyStackedRoles(true);
@@ -9766,18 +10260,27 @@ var Table = class extends Component {
9766
10260
  if (empty == null) return this.msg("table.empty");
9767
10261
  return typeof empty === "function" ? empty() : empty;
9768
10262
  }
9769
- /** @param {TableRow} row @param {number} index @returns {HTMLTableRowElement} */
9770
- _createRow(row, index) {
10263
+ /**
10264
+ * @param {TableRow} row
10265
+ * @param {number} index
10266
+ * @param {TableHierarchyEntry & {visibleIndex: number}} tree
10267
+ * @returns {HTMLTableRowElement}
10268
+ */
10269
+ _createRow(row, index, tree) {
9771
10270
  const id = this._idFor(row);
9772
10271
  const tr = (
9773
10272
  /** @type {HTMLTableRowElement} */
9774
10273
  h("tr", { dataset: { row: "" } })
9775
10274
  );
9776
- if (this._growStep) tr.setAttribute("aria-rowindex", String(index + 2));
10275
+ if (this._growStep) tr.setAttribute("aria-rowindex", String(tree.visibleIndex + 2));
9777
10276
  const rowClass = typeof this.options.rowClass === "function" ? this.options.rowClass(row) : "";
9778
10277
  if (rowClass) tr.classList.add(...String(rowClass).split(/\s+/).filter(Boolean));
9779
10278
  if (this.options.selectable !== false) tr.setAttribute("aria-selected", String(this._selected.has(id)));
9780
- this._rowMeta.set(tr, { row, id, index });
10279
+ if (this._hierarchy) {
10280
+ tr.setAttribute("aria-level", String(tree.depth + 1));
10281
+ if (tree.hasChildren) tr.setAttribute("aria-expanded", String(this._expanded.has(id)));
10282
+ }
10283
+ this._rowMeta.set(tr, { row, id, index, visibleIndex: tree.visibleIndex });
9781
10284
  const elements = this._rowElements.get(id) ?? [];
9782
10285
  elements.push(tr);
9783
10286
  this._rowElements.set(id, elements);
@@ -9795,7 +10298,8 @@ var Table = class extends Component {
9795
10298
  /** @type {HTMLTableCellElement} */
9796
10299
  h("td")
9797
10300
  );
9798
- if (column.align) cell.dataset.align = column.align;
10301
+ const align = column.align ?? (isNumericColumn(column) ? "end" : null);
10302
+ if (align) cell.dataset.align = align;
9799
10303
  cell.dataset.label = column.label;
9800
10304
  if (column.popin === false) cell.dataset.popin = "false";
9801
10305
  if (this._editMode !== false) {
@@ -9806,7 +10310,7 @@ var Table = class extends Component {
9806
10310
  this._offerRovingCell(cell, id, column.id);
9807
10311
  }
9808
10312
  }
9809
- this._renderCellContent(cell, column, row, index);
10313
+ this._renderCellContent(cell, column, row, index, tree);
9810
10314
  tr.append(cell);
9811
10315
  });
9812
10316
  return tr;
@@ -9817,13 +10321,33 @@ var Table = class extends Component {
9817
10321
  * @param {TableColumn} column Column definition.
9818
10322
  * @param {TableRow} row Row data.
9819
10323
  * @param {number} index Row index.
10324
+ * @param {TableHierarchyEntry} [tree] Hierarchy metadata for the row.
9820
10325
  * @returns {void}
9821
10326
  */
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();
10327
+ _renderCellContent(cell, column, row, index, tree = this._hierarchyMeta?.get(this._idFor(row))) {
10328
+ const value = formatTableCell(column, row, index);
10329
+ const content = h("span", { class: "zx-table__cell-content" });
10330
+ if (value && typeof value === "object" && typeof value.nodeType === "number") content.append(value);
10331
+ else if (value != null) content.append(document.createTextNode(String(value)));
10332
+ if (!this._hierarchy || column.id !== this._hierarchy.column || !tree) {
10333
+ cell.replaceChildren(...content.childNodes);
10334
+ return;
10335
+ }
10336
+ const branch = h("span", { class: "zx-table__tree-cell" });
10337
+ branch.style.setProperty("--zx-table-depth", String(tree.depth));
10338
+ if (tree.hasChildren) {
10339
+ const expanded = this._expanded.has(tree.id);
10340
+ branch.append(h("button", {
10341
+ class: "zx-table__tree-toggle",
10342
+ type: "button",
10343
+ ariaExpanded: String(expanded),
10344
+ ariaLabel: `${expanded ? "Collapse" : "Expand"} row ${index + 1}`
10345
+ }, icon(expanded ? "chevron-down" : "chevron-right", { size: 14 })));
10346
+ } else {
10347
+ branch.append(h("span", { class: "zx-table__tree-spacer", ariaHidden: "true" }));
10348
+ }
10349
+ branch.append(content);
10350
+ cell.replaceChildren(branch);
9827
10351
  }
9828
10352
  /** @param {MouseEvent} event @returns {void} */
9829
10353
  _handleBodyClick(event) {
@@ -9836,7 +10360,15 @@ var Table = class extends Component {
9836
10360
  if (!rowElement || !this._tbody.contains(rowElement)) return;
9837
10361
  const meta = this._rowMeta.get(rowElement);
9838
10362
  if (!meta) return;
9839
- if (this._edit) this.commitEdit();
10363
+ if (this._edit) {
10364
+ this.commitEdit();
10365
+ if (this._edit) return;
10366
+ }
10367
+ const disclosure = target.closest(".zx-table__tree-toggle");
10368
+ if (disclosure) {
10369
+ this.toggleRow(meta.id);
10370
+ return;
10371
+ }
9840
10372
  const checkbox = target.closest(".zx-table__row-checkbox");
9841
10373
  if (checkbox && this.options.selectable === "multi") {
9842
10374
  this._toggleMultiSelection(
@@ -9932,7 +10464,15 @@ var Table = class extends Component {
9932
10464
  return entry;
9933
10465
  }
9934
10466
  if (type === "number") {
9935
- const field = new NumberField(null, { ...props, value: value ?? null });
10467
+ const locale = resolveColumnOption(column.locale, row);
10468
+ const decimals = resolveColumnDecimals(column, row);
10469
+ const suffix = column.type === "currency" ? resolveColumnOption(column.currency, row) : column.type === "unit" ? resolveColumnOption(column.unit, row) : null;
10470
+ const inferred = {
10471
+ ...locale != null && !Object.prototype.hasOwnProperty.call(props, "locale") ? { locale } : {},
10472
+ ...decimals != null && !Object.prototype.hasOwnProperty.call(props, "precision") ? { precision: decimals } : {},
10473
+ ...suffix != null && !Object.prototype.hasOwnProperty.call(props, "unit") ? { unit: String(suffix) } : {}
10474
+ };
10475
+ const field = new NumberField(null, { ...inferred, ...props, value: value ?? null });
9936
10476
  host.append(field.el);
9937
10477
  entry.component = field;
9938
10478
  entry.control = field.getInput();
@@ -10163,9 +10703,12 @@ var Table = class extends Component {
10163
10703
  _nextEditableCell(rowIndex, columnId, direction) {
10164
10704
  const columnCount = this._columns.length;
10165
10705
  if (rowIndex < 0 || columnCount === 0) return null;
10166
- let row = rowIndex;
10706
+ const visible = this._visibleData();
10707
+ const startingId = this._idFor(this._data[rowIndex]);
10708
+ let row = visible.findIndex((candidate) => Object.is(this._idFor(candidate), startingId));
10709
+ if (row < 0) return null;
10167
10710
  let column = this._columns.findIndex((candidate) => candidate.id === columnId);
10168
- const limit = this._data.length * columnCount + columnCount;
10711
+ const limit = visible.length * columnCount + columnCount;
10169
10712
  for (let step = 0; step < limit; step += 1) {
10170
10713
  column += direction;
10171
10714
  if (column >= columnCount) {
@@ -10175,11 +10718,15 @@ var Table = class extends Component {
10175
10718
  row -= 1;
10176
10719
  column = columnCount - 1;
10177
10720
  }
10178
- if (row < 0 || row >= this._data.length) return null;
10179
- const candidateRow = this._data[row];
10721
+ if (row < 0 || row >= visible.length) return null;
10722
+ const candidateRow = visible[row];
10180
10723
  const candidateColumn = this._columns[column];
10181
10724
  if (resolveEditable(candidateColumn, candidateRow) !== false) {
10182
- return { id: this._idFor(candidateRow), columnId: candidateColumn.id, index: row };
10725
+ return {
10726
+ id: this._idFor(candidateRow),
10727
+ columnId: candidateColumn.id,
10728
+ index: this._data.indexOf(candidateRow)
10729
+ };
10183
10730
  }
10184
10731
  }
10185
10732
  return null;
@@ -10246,14 +10793,16 @@ var Table = class extends Component {
10246
10793
  const message = this.msg(key);
10247
10794
  return message === key ? fallback : message;
10248
10795
  }
10249
- /** @param {{row: TableRow, id: unknown, index: number}} meta @param {boolean} checked @param {boolean} shiftKey @returns {void} */
10796
+ /** @param {{row: TableRow, id: unknown, index: number, visibleIndex?: number}} meta @param {boolean} checked @param {boolean} shiftKey @returns {void} */
10250
10797
  _toggleMultiSelection(meta, checked, shiftKey) {
10251
- const anchorIndex = this._data.findIndex((row) => Object.is(this._idFor(row), this._selectionAnchorId));
10798
+ const visible = this._visibleData();
10799
+ const anchorIndex = visible.findIndex((row) => Object.is(this._idFor(row), this._selectionAnchorId));
10252
10800
  if (shiftKey && anchorIndex >= 0) {
10253
- const start = Math.min(anchorIndex, meta.index);
10254
- const end = Math.max(anchorIndex, meta.index);
10801
+ const currentIndex = meta.visibleIndex ?? visible.findIndex((row) => Object.is(this._idFor(row), meta.id));
10802
+ const start = Math.min(anchorIndex, currentIndex);
10803
+ const end = Math.max(anchorIndex, currentIndex);
10255
10804
  for (let index = start; index <= end; index += 1) {
10256
- const id = this._idFor(this._data[index]);
10805
+ const id = this._idFor(visible[index]);
10257
10806
  if (checked) this._selected.add(id);
10258
10807
  else this._selected.delete(id);
10259
10808
  }
@@ -10304,7 +10853,34 @@ var Table = class extends Component {
10304
10853
  const column = this._columns.find((candidate) => candidate.id === this._sort.id);
10305
10854
  if (!column) return;
10306
10855
  const getValue = typeof column.sortValue === "function" ? column.sortValue : (row) => row?.[column.id];
10307
- this._data = sortRows(this._data, getValue, this._sort.dir);
10856
+ this._data = this._hierarchy ? sortHierarchyRows(this._data, this.options.rowId, this._hierarchy.parentId, getValue, this._sort.dir) : sortRows(this._data, getValue, this._sort.dir);
10857
+ }
10858
+ /** @param {unknown} id @param {boolean} expanded @param {{silent?: boolean}} options @returns {this} */
10859
+ _setRowExpanded(id, expanded, options) {
10860
+ if (!this._hierarchy) return this;
10861
+ const branchIds = hierarchyBranchIds(this._data, this.options.rowId, this._hierarchy.parentId);
10862
+ if (!branchIds.some((candidate) => Object.is(candidate, id))) return this;
10863
+ if (this._expanded.has(id) === expanded) return this;
10864
+ if (expanded) this._expanded.add(id);
10865
+ else this._expanded.delete(id);
10866
+ this._renderBody();
10867
+ if (!options.silent) this._emitRowToggle(id, expanded);
10868
+ return this;
10869
+ }
10870
+ /** @param {unknown} id @param {boolean} expanded @returns {void} */
10871
+ _emitRowToggle(id, expanded) {
10872
+ const row = this.getRow(id);
10873
+ if (row) this.emit("rowtoggle", { row, id, expanded });
10874
+ }
10875
+ /** Keeps the reader's current expansion state limited to branches still present in the data. */
10876
+ _reconcileExpanded() {
10877
+ if (!this._hierarchy) {
10878
+ this._expanded.clear();
10879
+ return;
10880
+ }
10881
+ const branches = hierarchyBranchIds(this._data, this.options.rowId, this._hierarchy.parentId);
10882
+ const valid = new Set(branches);
10883
+ for (const id of this._expanded) if (!valid.has(id)) this._expanded.delete(id);
10308
10884
  }
10309
10885
  /** @returns {boolean} Whether selection changed. */
10310
10886
  _pruneSelection() {
@@ -10328,6 +10904,65 @@ var Table = class extends Component {
10328
10904
  this.emit("datachange", { rows: this.getData() });
10329
10905
  }
10330
10906
  };
10907
+ function formatTableCell(column, row, index = 0) {
10908
+ if (typeof column?.render === "function") return column.render(row, index);
10909
+ const value = row?.[column?.id];
10910
+ const type = column?.type ?? "text";
10911
+ if (type === "text") return value;
10912
+ const locale = resolveColumnOption(column.locale, row);
10913
+ const decimals = resolveColumnDecimals(column, row);
10914
+ const options = {
10915
+ ...locale != null ? { locale: String(locale) } : {},
10916
+ ...decimals != null ? { decimals } : {}
10917
+ };
10918
+ if (type === "number") return formatNumber(value, options);
10919
+ if (type === "currency") {
10920
+ const currency = resolveColumnOption(column.currency, row);
10921
+ return formatCurrency(value, currency == null ? null : String(currency), options);
10922
+ }
10923
+ if (type === "percent") return formatPercent2(value, options);
10924
+ if (type === "unit") {
10925
+ const number = formatNumber(value, options);
10926
+ const unit = resolveColumnOption(column.unit, row);
10927
+ return number && unit != null && String(unit) !== "" ? `${number}\xA0${unit}` : number;
10928
+ }
10929
+ return value;
10930
+ }
10931
+ function projectHierarchyRows(rows, rowId, parentId, expanded = []) {
10932
+ const forest = hierarchyForest(rows, rowId, parentId);
10933
+ const open = expanded instanceof Set ? expanded : new Set(expanded);
10934
+ const result = [];
10935
+ const stack = forest.roots.slice().reverse().map((node) => ({ node, depth: 0 }));
10936
+ while (stack.length) {
10937
+ const { node, depth } = stack.pop();
10938
+ result.push({
10939
+ row: node.row,
10940
+ id: node.id,
10941
+ index: node.index,
10942
+ depth,
10943
+ hasChildren: node.children.length > 0,
10944
+ parentId: node.parent?.id ?? null
10945
+ });
10946
+ if (!open.has(node.id)) continue;
10947
+ for (let index = node.children.length - 1; index >= 0; index -= 1) {
10948
+ stack.push({ node: node.children[index], depth: depth + 1 });
10949
+ }
10950
+ }
10951
+ return result;
10952
+ }
10953
+ function sortHierarchyRows(rows, rowId, parentId, getValue, direction) {
10954
+ const forest = hierarchyForest(rows, rowId, parentId);
10955
+ const sorted = (nodes) => sortRows(nodes, (node) => getValue(node.row), direction);
10956
+ const result = [];
10957
+ const stack = sorted(forest.roots).reverse();
10958
+ while (stack.length) {
10959
+ const node = stack.pop();
10960
+ result.push(node.row);
10961
+ const children = sorted(node.children);
10962
+ for (let index = children.length - 1; index >= 0; index -= 1) stack.push(children[index]);
10963
+ }
10964
+ return result;
10965
+ }
10331
10966
  function resolveEditable(column, row) {
10332
10967
  if (!column || typeof column !== "object") return false;
10333
10968
  if (typeof column.editor === "function") {
@@ -10335,7 +10970,7 @@ function resolveEditable(column, row) {
10335
10970
  return "custom";
10336
10971
  }
10337
10972
  const declared = typeof column.editable === "function" ? column.editable(row) : column.editable;
10338
- if (declared === true) return "text";
10973
+ if (declared === true) return isNumericColumn(column) ? "number" : "text";
10339
10974
  if (typeof declared === "string" && EDITOR_TYPES.has(declared)) return (
10340
10975
  /** @type {TableEditorType} */
10341
10976
  declared
@@ -10384,6 +11019,71 @@ function firstValidationError(values, columns, row, fallbackMessage = "Invalid v
10384
11019
  }
10385
11020
  return null;
10386
11021
  }
11022
+ function resolveColumnOption(option, row) {
11023
+ return typeof option === "function" ? option(row) : option;
11024
+ }
11025
+ function resolveColumnDecimals(column, row) {
11026
+ const value = resolveColumnOption(column?.decimals, row);
11027
+ if (value == null) return null;
11028
+ const number = Number(value);
11029
+ return Number.isFinite(number) ? Math.max(0, Math.trunc(number)) : null;
11030
+ }
11031
+ function isNumericColumn(column) {
11032
+ return ["number", "currency", "percent", "unit"].includes(column?.type);
11033
+ }
11034
+ function hierarchyForest(rows, rowId, parentId) {
11035
+ const nodes = (Array.isArray(rows) ? rows : []).map((row, index) => ({
11036
+ row,
11037
+ id: readRowValue(rowId, row),
11038
+ index,
11039
+ parent: null,
11040
+ children: []
11041
+ }));
11042
+ const byId = /* @__PURE__ */ new Map();
11043
+ for (const node of nodes) if (!byId.has(node.id)) byId.set(node.id, node);
11044
+ const parentOf = /* @__PURE__ */ new Map();
11045
+ for (const node of nodes) {
11046
+ const parent = byId.get(readRowValue(parentId, node.row)) ?? null;
11047
+ parentOf.set(node, parent === node ? null : parent);
11048
+ }
11049
+ for (const node of nodes) {
11050
+ const seen = /* @__PURE__ */ new Set([node]);
11051
+ let cursor = parentOf.get(node) ?? null;
11052
+ while (cursor) {
11053
+ if (seen.has(cursor)) {
11054
+ parentOf.set(cursor, null);
11055
+ break;
11056
+ }
11057
+ seen.add(cursor);
11058
+ cursor = parentOf.get(cursor) ?? null;
11059
+ }
11060
+ }
11061
+ const roots = [];
11062
+ for (const node of nodes) {
11063
+ node.parent = parentOf.get(node) ?? null;
11064
+ if (node.parent) node.parent.children.push(node);
11065
+ else roots.push(node);
11066
+ }
11067
+ return { roots, nodes };
11068
+ }
11069
+ function hierarchyBranchIds(rows, rowId, parentId) {
11070
+ return hierarchyForest(rows, rowId, parentId).nodes.filter((node) => node.children.length > 0).map((node) => node.id);
11071
+ }
11072
+ function readRowValue(accessor, row) {
11073
+ return typeof accessor === "function" ? accessor(row) : row?.[accessor];
11074
+ }
11075
+ function normalizeHierarchy(option, columns) {
11076
+ if (!option || typeof option !== "object") return null;
11077
+ const parentId = option.parentId;
11078
+ if (typeof parentId !== "string" && typeof parentId !== "function") return null;
11079
+ const requested = typeof option.column === "string" ? option.column : null;
11080
+ const column = columns.some((candidate) => candidate.id === requested) ? requested : columns[0]?.id;
11081
+ if (!column) return null;
11082
+ return { ...option, parentId, column };
11083
+ }
11084
+ function initialExpanded(option) {
11085
+ return new Set(Array.isArray(option) ? option : []);
11086
+ }
10387
11087
  function normalizeEditMode(mode) {
10388
11088
  if (mode === "cell" || mode === "row") return mode;
10389
11089
  return mode === true ? "cell" : false;
@@ -10425,6 +11125,630 @@ function growStep(growing) {
10425
11125
  return Number.isFinite(step) && step > 0 ? step : null;
10426
11126
  }
10427
11127
 
11128
+ // src/components/launcher/launcher.js
11129
+ var Launcher = class extends Component {
11130
+ static cssName = "launcher";
11131
+ /** @type {Readonly<LauncherOptions>} */
11132
+ static defaults = {
11133
+ items: [],
11134
+ sources: [],
11135
+ query: "",
11136
+ debounce: 250,
11137
+ minQuery: 0,
11138
+ maxResults: 100,
11139
+ placeholder: "Search applications and records",
11140
+ label: "Launcher",
11141
+ closeLabel: "Close launcher",
11142
+ resultsLabel: "Launcher results",
11143
+ emptyText: "No results",
11144
+ loadingText: "Searching\u2026",
11145
+ shortcut: "mod+k",
11146
+ hints: { move: "Move", open: "Open", close: "Close" }
11147
+ };
11148
+ /**
11149
+ * Creates an owned launcher dialog or enhances an existing `<dialog>` target.
11150
+ * @param {HTMLDialogElement|string|null} [target=null] Dialog target, selector, or null.
11151
+ * @param {LauncherOptions} [options={}] Launcher options.
11152
+ */
11153
+ constructor(target = null, options = {}) {
11154
+ super(target, options);
11155
+ }
11156
+ /** @returns {HTMLDialogElement} */
11157
+ render() {
11158
+ if (this.el !== null && !(this.el instanceof HTMLDialogElement)) {
11159
+ throw new TypeError("Launcher target must be a <dialog> element");
11160
+ }
11161
+ this._createdRoot = this.el === null;
11162
+ const dialog = (
11163
+ /** @type {HTMLDialogElement} */
11164
+ this.el ?? h("dialog")
11165
+ );
11166
+ this.el = dialog;
11167
+ this._original = this._createdRoot ? null : {
11168
+ attributes: Array.from(dialog.attributes, (attribute) => [attribute.name, attribute.value]),
11169
+ children: Array.from(dialog.childNodes)
11170
+ };
11171
+ this._items = normalizeItems(this.options.items);
11172
+ this._sources = normalizeSources(this.options.sources);
11173
+ this._sourceItems = /* @__PURE__ */ new Map();
11174
+ this._query = String(this.options.query ?? "");
11175
+ this._renderedQuery = null;
11176
+ this._results = [];
11177
+ this._active = -1;
11178
+ this._request = null;
11179
+ this._requestSequence = 0;
11180
+ this._queryTimer = null;
11181
+ this._busy = false;
11182
+ this._destroyed = false;
11183
+ const listId = uid("zx-launcher-results");
11184
+ const input = h("input", {
11185
+ ref: "input",
11186
+ class: "zx-launcher__input",
11187
+ type: "search",
11188
+ role: "combobox",
11189
+ autocomplete: "off",
11190
+ spellcheck: false,
11191
+ ariaAutocomplete: "list",
11192
+ ariaControls: listId,
11193
+ ariaExpanded: "false",
11194
+ ariaLabel: this.options.label,
11195
+ placeholder: this.options.placeholder,
11196
+ value: this._query
11197
+ });
11198
+ const close = h("button", {
11199
+ ref: "close",
11200
+ class: "zx-icon-btn zx-launcher__close",
11201
+ type: "button",
11202
+ ariaLabel: this.options.closeLabel
11203
+ }, icon("x"));
11204
+ const status = h("div", {
11205
+ ref: "status",
11206
+ class: "zx-launcher__status",
11207
+ role: "status",
11208
+ ariaLive: "polite"
11209
+ });
11210
+ const results = h("div", {
11211
+ ref: "results",
11212
+ class: "zx-launcher__results",
11213
+ id: listId,
11214
+ role: "listbox",
11215
+ ariaLabel: this.options.resultsLabel
11216
+ });
11217
+ const shortcut = this.options.shortcut === "mod+k" ? h("kbd", {
11218
+ class: "zx-launcher__shortcut",
11219
+ ariaHidden: "true"
11220
+ }, launcherShortcutLabel()) : null;
11221
+ const footer = launcherHints(this.options.hints);
11222
+ dialog.setAttribute("aria-label", String(this.options.label));
11223
+ dialog.replaceChildren(
11224
+ h(
11225
+ "div",
11226
+ { class: "zx-launcher__surface" },
11227
+ h("div", {
11228
+ class: "zx-launcher__search",
11229
+ dataset: { shortcut: shortcut ? "true" : "false" }
11230
+ }, icon("search"), input, shortcut, close),
11231
+ status,
11232
+ results,
11233
+ footer
11234
+ )
11235
+ );
11236
+ dialog.dataset.state = "closed";
11237
+ if (this._createdRoot) document.body.append(dialog);
11238
+ this.listen(input, "input", () => {
11239
+ this._query = /** @type {HTMLInputElement} */
11240
+ input.value;
11241
+ this.emit("query", { query: this._query.trim() });
11242
+ this._refresh();
11243
+ });
11244
+ this.listen(input, "keydown", (event) => this._onKeydown(
11245
+ /** @type {KeyboardEvent} */
11246
+ event
11247
+ ));
11248
+ this.listen(close, "click", () => this.close());
11249
+ this.listen(results, "click", (event) => {
11250
+ const option = (
11251
+ /** @type {Element|null} */
11252
+ event.target?.closest?.("[data-launcher-index]")
11253
+ );
11254
+ if (!option || !results.contains(option)) return;
11255
+ this._activate(Number(option.getAttribute("data-launcher-index")), event);
11256
+ });
11257
+ this.listen(results, "pointermove", (event) => {
11258
+ const option = (
11259
+ /** @type {Element|null} */
11260
+ event.target?.closest?.("[data-launcher-index]")
11261
+ );
11262
+ if (!option || !results.contains(option)) return;
11263
+ const index = Number(option.getAttribute("data-launcher-index"));
11264
+ if (!Number.isInteger(index) || this._results[index]?.disabled || index === this._active) return;
11265
+ this._active = index;
11266
+ this._syncActive();
11267
+ });
11268
+ this.listen(dialog, "cancel", (event) => {
11269
+ event.preventDefault();
11270
+ this.close();
11271
+ });
11272
+ this.listen(dialog, "close", () => {
11273
+ dialog.dataset.state = "closed";
11274
+ input.setAttribute("aria-expanded", "false");
11275
+ this._abortRequest();
11276
+ this.emit("close");
11277
+ });
11278
+ this.listen(document, "keydown", (event) => this._onDocumentKeydown(
11279
+ /** @type {KeyboardEvent} */
11280
+ event
11281
+ ));
11282
+ this._renderResults();
11283
+ return dialog;
11284
+ }
11285
+ /** Opens the launcher and focuses/selects its query. @returns {this} @fires Launcher#open */
11286
+ open() {
11287
+ if (this.isOpen() || this._destroyed) return this;
11288
+ this.el.showModal();
11289
+ this.el.dataset.state = "open";
11290
+ this.refs.input.setAttribute("aria-expanded", "true");
11291
+ this._refresh();
11292
+ queueMicrotask(() => {
11293
+ if (!this.isOpen()) return;
11294
+ this.refs.input.focus();
11295
+ this.refs.input.select();
11296
+ });
11297
+ this.emit("open");
11298
+ return this;
11299
+ }
11300
+ /** Closes the launcher. @returns {this} */
11301
+ close() {
11302
+ if (this.isOpen()) this.el.close();
11303
+ return this;
11304
+ }
11305
+ /** Toggles the launcher. @returns {this} */
11306
+ toggle() {
11307
+ return this.isOpen() ? this.close() : this.open();
11308
+ }
11309
+ /** Reports whether the launcher is open. @returns {boolean} */
11310
+ isOpen() {
11311
+ return Boolean(this.el.open);
11312
+ }
11313
+ /** Focuses and selects the query field. @returns {this} */
11314
+ focus() {
11315
+ this.refs.input.focus();
11316
+ this.refs.input.select();
11317
+ return this;
11318
+ }
11319
+ /** Replaces the query and refreshes local and remote results. @param {string} query @returns {this} */
11320
+ setQuery(query) {
11321
+ this._query = String(query ?? "");
11322
+ this.refs.input.value = this._query;
11323
+ this.emit("query", { query: this._query.trim() });
11324
+ this._refresh();
11325
+ return this;
11326
+ }
11327
+ /** Returns the untrimmed query. @returns {string} */
11328
+ getQuery() {
11329
+ return this._query;
11330
+ }
11331
+ /** Replaces local catalogue items. @param {LauncherItem[]} items @returns {this} */
11332
+ setItems(items) {
11333
+ this._items = normalizeItems(items);
11334
+ this._renderResults();
11335
+ return this;
11336
+ }
11337
+ /** Marks one local item as the current destination. @param {string|number|null} id Item id. @returns {this} */
11338
+ setCurrent(id) {
11339
+ const key = id == null ? null : String(id);
11340
+ this._items = this._items.map((item) => ({ ...item, current: key !== null && String(item.id) === key }));
11341
+ this._renderResults();
11342
+ return this;
11343
+ }
11344
+ /** Replaces asynchronous sources and aborts work from the old set. @param {LauncherSource[]} sources @returns {this} */
11345
+ setSources(sources) {
11346
+ this._abortRequest();
11347
+ this._sources = normalizeSources(sources);
11348
+ this._sourceItems.clear();
11349
+ this._refresh();
11350
+ return this;
11351
+ }
11352
+ /** Aborts sources and removes the owned dialog. @returns {void} */
11353
+ destroy() {
11354
+ if (this._destroyed) return;
11355
+ this._destroyed = true;
11356
+ const original = this._original;
11357
+ this._abortRequest();
11358
+ if (this.isOpen()) this.el.close();
11359
+ super.destroy();
11360
+ if (!this._createdRoot && original) {
11361
+ for (const attribute of Array.from(this.el.attributes)) this.el.removeAttribute(attribute.name);
11362
+ for (const [name, value] of original.attributes) this.el.setAttribute(name, value);
11363
+ this.el.replaceChildren(...original.children);
11364
+ }
11365
+ }
11366
+ /** @returns {void} */
11367
+ _refresh() {
11368
+ this._sourceItems.clear();
11369
+ this._renderResults();
11370
+ this._abortRequest();
11371
+ if (this._sources.length === 0) return;
11372
+ const delay = Math.max(0, Number(this.options.debounce) || 0);
11373
+ this._queryTimer = setTimeout(() => this._loadSources(), delay);
11374
+ }
11375
+ /** @returns {Promise<void>} */
11376
+ async _loadSources() {
11377
+ this._queryTimer = null;
11378
+ const query = this._query.trim();
11379
+ const sources = this._sources.filter((source) => visibleForQuery(source.when, query) && query.length >= Math.max(Number(this.options.minQuery) || 0, Number(source.minQuery) || 0));
11380
+ if (sources.length === 0) return;
11381
+ const request = new AbortController();
11382
+ const sequence = ++this._requestSequence;
11383
+ this._request = request;
11384
+ this._setBusy(true);
11385
+ await Promise.all(sources.map(async (source) => {
11386
+ try {
11387
+ const loaded = await source.load(query, { signal: request.signal });
11388
+ if (request.signal.aborted || sequence !== this._requestSequence) return;
11389
+ this._sourceItems.set(source.id, normalizeItems(loaded).map((item, itemOrder) => ({
11390
+ ...item,
11391
+ _source: source.id,
11392
+ _sourceOrder: source.order === "source",
11393
+ group: item.group ?? source.label ?? "",
11394
+ itemOrder: item.itemOrder ?? (source.order === "source" ? itemOrder : void 0)
11395
+ })));
11396
+ this._renderResults();
11397
+ } catch (error) {
11398
+ if (request.signal.aborted || sequence !== this._requestSequence) return;
11399
+ this.emit("error", { source, error });
11400
+ }
11401
+ }));
11402
+ if (request.signal.aborted || sequence !== this._requestSequence) return;
11403
+ this._request = null;
11404
+ this._setBusy(false);
11405
+ }
11406
+ /** @param {boolean} busy @returns {void} */
11407
+ _setBusy(busy) {
11408
+ this._busy = busy;
11409
+ this.el.setAttribute("aria-busy", String(busy));
11410
+ this._renderStatus();
11411
+ }
11412
+ /** @returns {void} */
11413
+ _abortRequest() {
11414
+ if (this._queryTimer !== null) clearTimeout(this._queryTimer);
11415
+ this._queryTimer = null;
11416
+ this._request?.abort();
11417
+ this._request = null;
11418
+ this._requestSequence += 1;
11419
+ if (this._busy) this._setBusy(false);
11420
+ }
11421
+ /** @returns {void} */
11422
+ _renderResults() {
11423
+ const queryKey = this._query;
11424
+ const activeId = this._renderedQuery === queryKey && this._active >= 0 && this._results[this._active] ? String(this._results[this._active].id) : null;
11425
+ const query = this._query.trim();
11426
+ const local = this._items.filter((item) => visibleForQuery(item.when, query));
11427
+ const rankedLocal = rankLauncherItems(local, query, this.options.maxResults);
11428
+ const remote = this._sources.flatMap((source) => {
11429
+ const loaded = this._sourceItems.get(String(source.id)) ?? [];
11430
+ return source.order === "source" ? loaded.filter((item) => visibleForQuery(item.when, query)) : rankLauncherItems(
11431
+ loaded.filter((item) => visibleForQuery(item.when, query)),
11432
+ query,
11433
+ this.options.maxResults
11434
+ );
11435
+ });
11436
+ const seen = /* @__PURE__ */ new Set();
11437
+ const combined = [...rankedLocal.map((item) => ({ ...item, _source: null })), ...remote].filter((item) => {
11438
+ const key = String(item.id);
11439
+ if (seen.has(key)) return false;
11440
+ seen.add(key);
11441
+ return true;
11442
+ }).slice(0, Math.max(0, Number(this.options.maxResults) || 0));
11443
+ const groups = /* @__PURE__ */ new Map();
11444
+ for (const item of combined) {
11445
+ const group = String(item.group ?? "");
11446
+ if (!groups.has(group)) groups.set(group, []);
11447
+ groups.get(group).push(item);
11448
+ }
11449
+ this._results = [...groups.values()].flat();
11450
+ this._renderedQuery = queryKey;
11451
+ const preserved = activeId === null ? -1 : this._results.findIndex((item) => !item.disabled && String(item.id) === activeId);
11452
+ this._active = preserved >= 0 ? preserved : this._results.findIndex((item) => !item.disabled);
11453
+ const nodes = [];
11454
+ let index = 0;
11455
+ for (const [group, items] of groups) {
11456
+ const options = items.map((item) => this._option(item, index++));
11457
+ if (group) nodes.push(h("div", {
11458
+ class: "zx-launcher__group",
11459
+ role: "group",
11460
+ ariaLabel: group,
11461
+ dataset: { layout: items.every((item) => item.kind === "application") ? "grid" : "list" }
11462
+ }, h("div", { class: "zx-launcher__group-label", ariaHidden: "true" }, group), options));
11463
+ else nodes.push(...options);
11464
+ }
11465
+ this.refs.results.replaceChildren(...nodes);
11466
+ this._syncActive();
11467
+ this._renderStatus();
11468
+ }
11469
+ /** @param {RankedLauncherItem} item @param {number} index @returns {HTMLElement} */
11470
+ _option(item, index) {
11471
+ const href = safeHref(item.href);
11472
+ const tag = href ? "a" : "button";
11473
+ const children = [];
11474
+ const visual = launcherItemIcon(item);
11475
+ if (visual) children.push(h("span", {
11476
+ class: "zx-launcher__icon",
11477
+ ariaHidden: "true"
11478
+ }, visual));
11479
+ children.push(h(
11480
+ "span",
11481
+ { class: "zx-launcher__copy" },
11482
+ h("span", { class: "zx-launcher__label" }, item.label),
11483
+ item.description ? h("span", { class: "zx-launcher__description" }, item.description) : null
11484
+ ));
11485
+ if (item.badge != null) children.push(h("span", { class: "zx-launcher__badge" }, String(item.badge)));
11486
+ return (
11487
+ /** @type {HTMLElement} */
11488
+ h(tag, {
11489
+ class: "zx-launcher__option",
11490
+ type: tag === "button" ? "button" : null,
11491
+ href,
11492
+ target: item.target ?? null,
11493
+ rel: tag === "a" ? "noopener" : null,
11494
+ role: "option",
11495
+ id: uid("zx-launcher-option"),
11496
+ tabIndex: -1,
11497
+ ariaSelected: "false",
11498
+ ariaDisabled: item.disabled ? "true" : null,
11499
+ ariaCurrent: item.current ? "page" : null,
11500
+ dataset: {
11501
+ launcherIndex: index,
11502
+ pinned: item.pinned !== void 0 && item.pinned !== false ? "true" : null,
11503
+ kind: normalizeLauncherKind(item.kind)
11504
+ }
11505
+ }, children)
11506
+ );
11507
+ }
11508
+ /** @returns {void} */
11509
+ _renderStatus() {
11510
+ if (this._busy) this.refs.status.textContent = String(this.options.loadingText);
11511
+ else if (this._results.length === 0) this.refs.status.textContent = String(this.options.emptyText);
11512
+ else this.refs.status.textContent = "";
11513
+ this.refs.status.hidden = !this.refs.status.textContent;
11514
+ }
11515
+ /** @returns {void} */
11516
+ _syncActive() {
11517
+ const options = [...this.refs.results.querySelectorAll("[data-launcher-index]")];
11518
+ for (const option of options) {
11519
+ const selected = Number(option.getAttribute("data-launcher-index")) === this._active;
11520
+ option.setAttribute("aria-selected", String(selected));
11521
+ }
11522
+ const active = options.find((option) => Number(option.getAttribute("data-launcher-index")) === this._active);
11523
+ if (active) this.refs.input.setAttribute("aria-activedescendant", active.id);
11524
+ else this.refs.input.removeAttribute("aria-activedescendant");
11525
+ }
11526
+ /** @param {KeyboardEvent} event @returns {void} */
11527
+ _onKeydown(event) {
11528
+ if (event.key === "Escape") {
11529
+ this.close();
11530
+ return;
11531
+ }
11532
+ if (event.key === "Enter") {
11533
+ if (this._active < 0) return;
11534
+ event.preventDefault();
11535
+ const option = this.refs.results.querySelector(`[data-launcher-index="${this._active}"]`);
11536
+ option?.click();
11537
+ return;
11538
+ }
11539
+ const enabled = this._results.map((item, index) => item.disabled ? -1 : index).filter((index) => index >= 0);
11540
+ if (enabled.length === 0) return;
11541
+ if (/^Arrow(?:Left|Right|Up|Down)$/.test(event.key)) {
11542
+ const next = this._spatialIndex(event.key, enabled);
11543
+ if (next !== null) {
11544
+ event.preventDefault();
11545
+ this._active = next;
11546
+ this._syncActive();
11547
+ this.refs.results.querySelector(`[data-launcher-index="${this._active}"]`)?.scrollIntoView({ block: "nearest", inline: "nearest" });
11548
+ return;
11549
+ }
11550
+ if (event.key === "ArrowLeft" || event.key === "ArrowRight") return;
11551
+ }
11552
+ let position2 = enabled.indexOf(this._active);
11553
+ if (event.key === "Home" || event.key === "PageUp") position2 = 0;
11554
+ else if (event.key === "End" || event.key === "PageDown") position2 = enabled.length - 1;
11555
+ else if (event.key === "ArrowDown") position2 = (position2 + 1 + enabled.length) % enabled.length;
11556
+ else if (event.key === "ArrowUp") position2 = (position2 - 1 + enabled.length) % enabled.length;
11557
+ else return;
11558
+ event.preventDefault();
11559
+ this._active = enabled[position2];
11560
+ this._syncActive();
11561
+ this.refs.results.querySelector(`[data-launcher-index="${this._active}"]`)?.scrollIntoView({ block: "nearest" });
11562
+ }
11563
+ /** @param {string} direction @param {number[]} enabled @returns {number|null} */
11564
+ _spatialIndex(direction, enabled) {
11565
+ const current = this.refs.results.querySelector(`[data-launcher-index="${this._active}"]`);
11566
+ if (!(current instanceof HTMLElement)) return null;
11567
+ if ((direction === "ArrowLeft" || direction === "ArrowRight") && this._results[this._active]?.kind !== "application") return null;
11568
+ const candidates = [...this.refs.results.querySelectorAll("[data-launcher-index]")].filter((option) => enabled.includes(Number(option.getAttribute("data-launcher-index"))) && (direction !== "ArrowLeft" && direction !== "ArrowRight" || option.getAttribute("data-kind") === "application"));
11569
+ const next = findSpatialLauncherOption(candidates, current, direction);
11570
+ return next ? Number(next.getAttribute("data-launcher-index")) : null;
11571
+ }
11572
+ /** @param {number} index @param {Event} event @returns {void} */
11573
+ _activate(index, event) {
11574
+ const item = this._results[index];
11575
+ if (!item || item.disabled) {
11576
+ event.preventDefault();
11577
+ return;
11578
+ }
11579
+ const selected = this.emit("select", {
11580
+ item,
11581
+ value: item.value ?? item.id,
11582
+ source: item._source ?? null,
11583
+ query: this._query.trim()
11584
+ });
11585
+ if (selected.defaultPrevented) {
11586
+ event.preventDefault();
11587
+ return;
11588
+ }
11589
+ if (!item.href) {
11590
+ event.preventDefault();
11591
+ item.invoke?.();
11592
+ }
11593
+ this.close();
11594
+ }
11595
+ /** @param {KeyboardEvent} event @returns {void} */
11596
+ _onDocumentKeydown(event) {
11597
+ if (this.options.shortcut !== "mod+k" || event.isComposing || event.key.toLowerCase() !== "k" || Number(event.metaKey) + Number(event.ctrlKey) !== 1 || event.altKey || event.shiftKey) return;
11598
+ const active = document.activeElement;
11599
+ if (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement || active instanceof HTMLSelectElement || active instanceof HTMLElement && active.isContentEditable) return;
11600
+ const modal = document.querySelector("dialog:modal");
11601
+ if (modal && modal !== this.el) return;
11602
+ event.preventDefault();
11603
+ this.open();
11604
+ }
11605
+ };
11606
+ function launcherShortcutLabel() {
11607
+ const platform = String(
11608
+ /** @type {any} */
11609
+ navigator.userAgentData?.platform ?? navigator.platform ?? ""
11610
+ );
11611
+ return /mac|iphone|ipad/i.test(platform) ? "\u2318 K" : "Ctrl K";
11612
+ }
11613
+ function launcherHints(hints) {
11614
+ if (hints === false) return null;
11615
+ const labels = hints && typeof hints === "object" ? hints : {};
11616
+ return h(
11617
+ "footer",
11618
+ { class: "zx-launcher__hints", ariaHidden: "true" },
11619
+ h("span", {}, h("kbd", {}, "\u2191\u2193\u2190\u2192"), labels.move ?? "Move"),
11620
+ h("span", {}, h("kbd", {}, "\u21B5"), labels.open ?? "Open"),
11621
+ h("span", {}, h("kbd", {}, "Esc"), labels.close ?? "Close")
11622
+ );
11623
+ }
11624
+ function launcherItemIcon(item) {
11625
+ let visual = item.icon;
11626
+ if (typeof visual === "function") visual = visual(item);
11627
+ if (typeof visual === "string") return icon(visual);
11628
+ if (visual instanceof Node) return visual.cloneNode(true);
11629
+ if (visual && typeof visual === "object" && typeof /** @type {any} */
11630
+ visual.toElement === "function") {
11631
+ const element = (
11632
+ /** @type {any} */
11633
+ visual.toElement()
11634
+ );
11635
+ return element instanceof Node ? element.cloneNode(true) : null;
11636
+ }
11637
+ return null;
11638
+ }
11639
+ function normalizeLauncherKind(kind) {
11640
+ return kind === "application" || kind === "record" ? kind : "action";
11641
+ }
11642
+ function visibleForQuery(when, query) {
11643
+ if (when === "empty") return query === "";
11644
+ if (when === "query") return query !== "";
11645
+ return true;
11646
+ }
11647
+ function findSpatialLauncherOption(options, current, direction) {
11648
+ const origin = current.getBoundingClientRect();
11649
+ if (origin.width === 0 && origin.height === 0) return null;
11650
+ const originX = origin.left + origin.width / 2;
11651
+ const originY = origin.top + origin.height / 2;
11652
+ let best = null;
11653
+ let bestScore = Infinity;
11654
+ for (const option of options) {
11655
+ if (option === current) continue;
11656
+ const rect = option.getBoundingClientRect();
11657
+ const dx = rect.left + rect.width / 2 - originX;
11658
+ const dy = rect.top + rect.height / 2 - originY;
11659
+ let valid = false;
11660
+ let score = Infinity;
11661
+ if (direction === "ArrowLeft") {
11662
+ valid = dx < -2 && Math.abs(dy) < Math.max(origin.height, rect.height) / 2;
11663
+ score = Math.abs(dx) + Math.abs(dy) * 4;
11664
+ } else if (direction === "ArrowRight") {
11665
+ valid = dx > 2 && Math.abs(dy) < Math.max(origin.height, rect.height) / 2;
11666
+ score = Math.abs(dx) + Math.abs(dy) * 4;
11667
+ } else if (direction === "ArrowUp") {
11668
+ valid = dy < -2;
11669
+ score = Math.abs(dy) * 10 + Math.abs(dx);
11670
+ } else if (direction === "ArrowDown") {
11671
+ valid = dy > 2;
11672
+ score = Math.abs(dy) * 10 + Math.abs(dx);
11673
+ }
11674
+ if (valid && score < bestScore) {
11675
+ best = option;
11676
+ bestScore = score;
11677
+ }
11678
+ }
11679
+ return best;
11680
+ }
11681
+ function rankLauncherItems(items, query, limit = 100) {
11682
+ const needle = searchable(query).trim();
11683
+ const ranked = items.map((item, order) => ({ item, order, score: launcherScore(item, needle) })).filter(({ score }) => score < Infinity);
11684
+ ranked.sort((a, b) => {
11685
+ const aGroup = finiteOrder(a.item.groupOrder);
11686
+ const bGroup = finiteOrder(b.item.groupOrder);
11687
+ if (aGroup !== bGroup) return aGroup - bGroup;
11688
+ if (needle === "") {
11689
+ const aPinned = pinOrder(a.item.pinned, a.order);
11690
+ const bPinned = pinOrder(b.item.pinned, b.order);
11691
+ if (aPinned !== bPinned) return aPinned - bPinned;
11692
+ }
11693
+ return a.score - b.score || finiteOrder(a.item.itemOrder) - finiteOrder(b.item.itemOrder) || compareLauncherText(a.item.label, b.item.label) || a.order - b.order;
11694
+ });
11695
+ const maximum = Math.max(0, Number(limit) || 0);
11696
+ return ranked.slice(0, maximum || 0).map(({ item, score }) => ({ ...item, _score: score }));
11697
+ }
11698
+ function launcherScore(item, needle) {
11699
+ if (!needle) return item.pinned ? -1 : 0;
11700
+ const label = searchable(item.label);
11701
+ const identifier = searchable(item.id);
11702
+ const keywords = searchable(Array.isArray(item.keywords) ? item.keywords.join(" ") : item.keywords ?? "");
11703
+ if (label === needle) return 0;
11704
+ if (label.startsWith(needle)) return 10;
11705
+ if (label.split(/[^\p{L}\p{N}]+/u).some((word) => word.startsWith(needle))) return 20;
11706
+ if (label.includes(needle) || keywords.includes(needle)) return 30;
11707
+ if (identifier.includes(needle)) return 40;
11708
+ const acronym = label.split(/[^\p{L}\p{N}]+/u).filter(Boolean).map((word) => word[0]).join("");
11709
+ if (acronym.startsWith(needle)) return 50;
11710
+ return Infinity;
11711
+ }
11712
+ function pinOrder(pinned, order) {
11713
+ if (typeof pinned === "number" && Number.isFinite(pinned)) return pinned;
11714
+ if (pinned) return order;
11715
+ return Number.MAX_SAFE_INTEGER;
11716
+ }
11717
+ function finiteOrder(value) {
11718
+ return typeof value === "number" && Number.isFinite(value) ? value : Number.MAX_SAFE_INTEGER;
11719
+ }
11720
+ function searchable(value) {
11721
+ return String(value ?? "").normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase();
11722
+ }
11723
+ function compareLauncherText(left, right) {
11724
+ const a = searchable(left);
11725
+ const b = searchable(right);
11726
+ if (a < b) return -1;
11727
+ if (a > b) return 1;
11728
+ const rawA = String(left ?? "").normalize("NFC");
11729
+ const rawB = String(right ?? "").normalize("NFC");
11730
+ return rawA < rawB ? -1 : rawA > rawB ? 1 : 0;
11731
+ }
11732
+ function normalizeItems(items) {
11733
+ if (!Array.isArray(items)) return [];
11734
+ return items.filter((item) => item && typeof item === "object" && item.id != null && item.label != null).map((item) => normalizeLauncherItem(item));
11735
+ }
11736
+ function normalizeLauncherItem(item) {
11737
+ const normalized = { ...item, label: String(item.label) };
11738
+ if (item.href == null) return normalized;
11739
+ const href = safeHref(item.href);
11740
+ if (href !== null) normalized.href = href;
11741
+ else {
11742
+ delete normalized.href;
11743
+ if (typeof normalized.invoke !== "function") normalized.disabled = true;
11744
+ }
11745
+ return normalized;
11746
+ }
11747
+ function normalizeSources(sources) {
11748
+ if (!Array.isArray(sources)) return [];
11749
+ return sources.filter((source) => source && typeof source === "object" && source.id != null && typeof source.load === "function").map((source) => ({ ...source, id: String(source.id) }));
11750
+ }
11751
+
10428
11752
  // src/components/message/message.js
10429
11753
  var MessageQueue = class {
10430
11754
  #limit;
@@ -11534,6 +12858,7 @@ var zeyosModules = Object.freeze({
11534
12858
  weblets: { label: "Weblets", icon: "zeyos-weblets", color: "#4f46e5", fa: "window-restore" }
11535
12859
  });
11536
12860
  var moduleAliases = Object.freeze({
12861
+ main: "zeyos",
11537
12862
  addresses: "contacts",
11538
12863
  clocking: "actionsteps",
11539
12864
  comments: "notes",
@@ -11640,15 +12965,23 @@ function moduleIcon(name, options = {}) {
11640
12965
  return icon(standard ? `fa:${info.fa}` : `kit:${info.icon}`, rest);
11641
12966
  }
11642
12967
  function moduleChip(name, options = {}) {
11643
- const { size = 24, iconSize = "1em", label = null, title = false, standard = false } = options;
12968
+ const {
12969
+ size = 24,
12970
+ iconSize = "1em",
12971
+ label = null,
12972
+ title = false,
12973
+ standard = false,
12974
+ color = null
12975
+ } = options;
11644
12976
  const key = normalizeModuleName(name);
11645
12977
  const info = moduleInfo(name);
12978
+ const background = color || info.color;
11646
12979
  const chip = h("span", {
11647
12980
  class: ["zx-module-icon", options.class].flat().filter(Boolean),
11648
12981
  dataset: { module: key },
11649
12982
  style: {
11650
- "--zx-module-color": info.color,
11651
- "--zx-module-glyph": moduleGlyphColor(info.color),
12983
+ "--zx-module-color": background,
12984
+ "--zx-module-glyph": moduleGlyphColor(background),
11652
12985
  "--zx-module-icon-size": typeof size === "number" ? `${size}px` : String(size)
11653
12986
  },
11654
12987
  title: title ? info.label : null
@@ -11662,6 +12995,307 @@ function moduleChip(name, options = {}) {
11662
12995
  return chip;
11663
12996
  }
11664
12997
 
12998
+ // src/zeyos/launcher.js
12999
+ var ZEYOS_LAUNCHER_APPLICATIONS = Object.freeze([
13000
+ "accounts",
13001
+ "actionsteps",
13002
+ "admin",
13003
+ "auth",
13004
+ "billing",
13005
+ "calendar",
13006
+ "campaigns",
13007
+ "collection",
13008
+ "contacts",
13009
+ "contracts",
13010
+ "control",
13011
+ "coupons",
13012
+ "dev",
13013
+ "documents",
13014
+ "inventory",
13015
+ "mailinglists",
13016
+ "main",
13017
+ "messages",
13018
+ "links",
13019
+ "notes",
13020
+ "objects",
13021
+ "opportunities",
13022
+ "payments",
13023
+ "pricelists",
13024
+ "procurement",
13025
+ "production",
13026
+ "projects",
13027
+ "tasks",
13028
+ "tickets"
13029
+ ]);
13030
+ function buildZeyosLauncherConfig(state = {}, options = {}) {
13031
+ const {
13032
+ standardIcons = false,
13033
+ items: extraItems = [],
13034
+ sources: extraSources = [],
13035
+ ...launcherOptions
13036
+ } = options;
13037
+ const labels = launcherLabels(state.labels);
13038
+ const iconOptions = {
13039
+ standardIcons,
13040
+ renderApplicationIcon: state.renderApplicationIcon,
13041
+ renderWebletIcon: state.renderWebletIcon
13042
+ };
13043
+ const applications = normalizeZeyosLauncherApplications(state, iconOptions);
13044
+ const recent = typeof state.loadRecent === "function" ? [] : normalizeZeyosLauncherRecords(state.recent ?? [], {
13045
+ query: "",
13046
+ labels,
13047
+ resolveRecord: state.resolveRecord,
13048
+ renderRecordIcon: state.renderRecordIcon,
13049
+ standardIcons
13050
+ });
13051
+ const sources = [];
13052
+ if (typeof state.loadRecent === "function") {
13053
+ sources.push({
13054
+ id: "zeyos-recent",
13055
+ minQuery: 0,
13056
+ when: "empty",
13057
+ order: "source",
13058
+ load: async (_query, context) => normalizeZeyosLauncherRecords(
13059
+ await state.loadRecent(context),
13060
+ {
13061
+ query: "",
13062
+ labels,
13063
+ resolveRecord: state.resolveRecord,
13064
+ renderRecordIcon: state.renderRecordIcon,
13065
+ standardIcons
13066
+ }
13067
+ )
13068
+ });
13069
+ }
13070
+ if (typeof state.search === "function") {
13071
+ sources.push({
13072
+ id: "zeyos-records",
13073
+ minQuery: 1,
13074
+ when: "query",
13075
+ order: "source",
13076
+ load: async (query, context) => normalizeZeyosLauncherRecords(
13077
+ await state.search(query, context),
13078
+ {
13079
+ query,
13080
+ labels,
13081
+ resolveRecord: state.resolveRecord,
13082
+ renderRecordIcon: state.renderRecordIcon,
13083
+ standardIcons
13084
+ }
13085
+ )
13086
+ });
13087
+ }
13088
+ sources.push(...extraSources);
13089
+ return {
13090
+ ...launcherOptions,
13091
+ items: [...applications, ...recent, ...extraItems],
13092
+ sources,
13093
+ hints: options.hints ?? {
13094
+ move: labels.move,
13095
+ open: labels.open,
13096
+ close: labels.close
13097
+ }
13098
+ };
13099
+ }
13100
+ function zeyosLauncher(state = {}, options = {}) {
13101
+ return new Launcher(null, buildZeyosLauncherConfig(state, options));
13102
+ }
13103
+ function normalizeZeyosLauncherApplications(state = {}, options = {}) {
13104
+ const labels = launcherLabels(state.labels);
13105
+ const pinned = Array.isArray(state.pinned) ? state.pinned.map(String) : [];
13106
+ const seen = /* @__PURE__ */ new Set();
13107
+ const items = [];
13108
+ const add = (raw2, fallbackId = null, fallbackLabel = null) => {
13109
+ const app = normalizeApplication(raw2, fallbackId, fallbackLabel);
13110
+ if (!app || seen.has(app.identifier)) return;
13111
+ if (app.type === "module" && typeof state.canUseModule === "function" && !state.canUseModule(app.module)) return;
13112
+ seen.add(app.identifier);
13113
+ const resolved = mergeDestination(app.raw, state.resolveApplication);
13114
+ const pin = pinned.indexOf(app.identifier);
13115
+ const icon2 = () => {
13116
+ const custom = typeof options.renderApplicationIcon === "function" ? options.renderApplicationIcon(app.raw, app) : app.type === "weblet" && typeof options.renderWebletIcon === "function" ? options.renderWebletIcon(app.raw, app) : null;
13117
+ return custom ?? moduleChip(app.module, {
13118
+ size: 36,
13119
+ standard: Boolean(options.standardIcons),
13120
+ color: app.color
13121
+ });
13122
+ };
13123
+ items.push({
13124
+ id: app.identifier,
13125
+ label: app.label,
13126
+ keywords: [app.identifier, app.module],
13127
+ group: labels.applications,
13128
+ groupOrder: 0,
13129
+ kind: "application",
13130
+ icon: icon2,
13131
+ pinned: pin >= 0 ? pin : void 0,
13132
+ current: app.identifier === state.activeIdentifier,
13133
+ disabled: !resolved.href && typeof resolved.invoke !== "function",
13134
+ ...resolved,
13135
+ _zeyos: app
13136
+ });
13137
+ };
13138
+ for (const raw2 of Array.isArray(state.menuApps) ? state.menuApps : []) add(raw2);
13139
+ if (Array.isArray(state.modules)) {
13140
+ for (const module of state.modules) {
13141
+ if (typeof module === "string") add([module, moduleInfo(module).label]);
13142
+ else add(module, module?.id ?? module?.module, module?.label);
13143
+ }
13144
+ } else {
13145
+ for (const [module, label] of Object.entries(state.modules ?? {})) add([module, label]);
13146
+ }
13147
+ return items;
13148
+ }
13149
+ function normalizeZeyosLauncherRecords(payload, options = {}) {
13150
+ const query = String(options.query ?? "");
13151
+ const labels = launcherLabels(options.labels);
13152
+ const records = flattenRecords(payload, query);
13153
+ const groupOrders = /* @__PURE__ */ new Map();
13154
+ return records.flatMap(({ raw: raw2, group }, itemOrder) => {
13155
+ const record = normalizeRecord(raw2, group, query);
13156
+ if (!record) return [];
13157
+ const groupName = query === "" ? labels.recent : record.group === "" ? labels.results : labels.entities[record.group] ?? record.group;
13158
+ if (!groupOrders.has(groupName)) groupOrders.set(groupName, groupOrders.size + 1);
13159
+ const resolved = mergeDestination(record.raw, options.resolveRecord);
13160
+ return [{
13161
+ id: `record:${record.entity}:${record.fork ?? ""}:${record.ID}`,
13162
+ label: record.name,
13163
+ description: record.sec,
13164
+ keywords: [record.entity, record.ID, record.fork ?? ""],
13165
+ group: groupName,
13166
+ groupOrder: groupOrders.get(groupName),
13167
+ itemOrder,
13168
+ kind: "record",
13169
+ when: query === "" ? "empty" : "query",
13170
+ icon: () => {
13171
+ const custom = typeof options.renderRecordIcon === "function" ? options.renderRecordIcon(record.raw, record) : null;
13172
+ return custom ?? moduleChip(record.entity, {
13173
+ size: 28,
13174
+ standard: Boolean(options.standardIcons)
13175
+ });
13176
+ },
13177
+ disabled: !resolved.href && typeof resolved.invoke !== "function",
13178
+ ...resolved,
13179
+ _zeyos: record
13180
+ }];
13181
+ });
13182
+ }
13183
+ function zeyosActiveIdentifier(state = {}) {
13184
+ if (state.weblet != null && state.weblet !== "") return `weblet#${state.weblet}`;
13185
+ if (state.fork != null && state.fork !== "") return `fork#${state.fork}`;
13186
+ return String(state.module ?? "");
13187
+ }
13188
+ function normalizeApplication(raw2, fallbackId, fallbackLabel) {
13189
+ if (Array.isArray(raw2)) {
13190
+ if (raw2.length < 2) return null;
13191
+ if (raw2.length === 2) {
13192
+ const module = String(raw2[0]);
13193
+ return { raw: raw2, identifier: module, label: String(raw2[1]), module, type: "module", color: null };
13194
+ }
13195
+ if (raw2.length === 4) {
13196
+ const module = String(raw2[2] || "default");
13197
+ return {
13198
+ raw: raw2,
13199
+ identifier: `fork#${raw2[0]}`,
13200
+ label: String(raw2[1]),
13201
+ module,
13202
+ type: "fork",
13203
+ color: normalizeServerColor(raw2[3])
13204
+ };
13205
+ }
13206
+ return {
13207
+ raw: raw2,
13208
+ identifier: `weblet#${raw2[0]}`,
13209
+ label: String(raw2[1]),
13210
+ module: "weblets",
13211
+ type: "weblet",
13212
+ color: normalizeServerColor(raw2[8])
13213
+ };
13214
+ }
13215
+ if (!raw2 || typeof raw2 !== "object") return null;
13216
+ const identifier = String(raw2.identifier ?? raw2.id ?? fallbackId ?? "");
13217
+ if (!identifier) return null;
13218
+ const type = identifier.startsWith("fork#") ? "fork" : identifier.startsWith("weblet#") ? "weblet" : "module";
13219
+ return {
13220
+ raw: raw2,
13221
+ identifier,
13222
+ label: String(raw2.label ?? fallbackLabel ?? moduleInfo(identifier).label),
13223
+ module: String(raw2.module ?? (type === "module" ? identifier : type === "fork" ? "default" : "weblets")),
13224
+ type,
13225
+ color: normalizeServerColor(raw2.color)
13226
+ };
13227
+ }
13228
+ function flattenRecords(payload, query) {
13229
+ if (Array.isArray(payload)) return payload.map((raw2) => ({ raw: raw2, group: raw2?.group ?? "" }));
13230
+ if (!payload || typeof payload !== "object") return [];
13231
+ const result = [];
13232
+ for (const [group, rows] of Object.entries(payload)) {
13233
+ if (!Array.isArray(rows)) continue;
13234
+ for (const row of rows) {
13235
+ if (!Array.isArray(row)) {
13236
+ result.push({ raw: row, group });
13237
+ continue;
13238
+ }
13239
+ const raw2 = group === "" && row.length >= 5 ? { ID: row[0], fork: row[1], name: row[2], sec: row[3], entity: row[4], group } : { ID: row[0], fork: row[1], name: row[2], sec: row[3], entity: group, group };
13240
+ result.push({ raw: raw2, group });
13241
+ }
13242
+ }
13243
+ return result;
13244
+ }
13245
+ function normalizeRecord(raw2, group, query) {
13246
+ if (Array.isArray(raw2)) {
13247
+ if (query === "" && raw2.length >= 4) {
13248
+ return {
13249
+ raw: raw2,
13250
+ entity: String(raw2[0]),
13251
+ ID: raw2[1],
13252
+ fork: raw2[2] ?? null,
13253
+ name: String(raw2[3]),
13254
+ sec: String(raw2[4] ?? ""),
13255
+ group: ""
13256
+ };
13257
+ }
13258
+ return null;
13259
+ }
13260
+ if (!raw2 || typeof raw2 !== "object" || raw2.ID == null || raw2.name == null) return null;
13261
+ return {
13262
+ raw: raw2,
13263
+ entity: String(raw2.entity ?? group ?? "records"),
13264
+ ID: raw2.ID,
13265
+ fork: raw2.fork ?? null,
13266
+ name: String(raw2.name),
13267
+ sec: String(raw2.sec ?? ""),
13268
+ group: String(raw2.group ?? group ?? "")
13269
+ };
13270
+ }
13271
+ function mergeDestination(raw2, resolver) {
13272
+ const direct = raw2 && typeof raw2 === "object" && !Array.isArray(raw2) ? raw2 : {};
13273
+ const resolved = typeof resolver === "function" ? resolver(raw2) : null;
13274
+ const destination = resolved && typeof resolved === "object" ? resolved : {};
13275
+ return Object.fromEntries(["href", "target", "invoke", "value"].flatMap((key) => {
13276
+ const value = destination[key] ?? direct[key];
13277
+ return value === void 0 ? [] : [[key, value]];
13278
+ }));
13279
+ }
13280
+ function normalizeServerColor(value) {
13281
+ const source = String(value ?? "").trim();
13282
+ if (!source) return null;
13283
+ const color = source.startsWith("#") ? source : `#${source}`;
13284
+ return /^#[\da-f]{3}(?:[\da-f]{3})?$/i.test(color) ? color : null;
13285
+ }
13286
+ function launcherLabels(input) {
13287
+ const labels = input && typeof input === "object" ? input : {};
13288
+ return {
13289
+ applications: String(labels.applications ?? "Applications"),
13290
+ recent: String(labels.recent ?? "Recent"),
13291
+ results: String(labels.results ?? "Top results"),
13292
+ entities: labels.entities && typeof labels.entities === "object" ? labels.entities : {},
13293
+ move: String(labels.move ?? "Move"),
13294
+ open: String(labels.open ?? "Open"),
13295
+ close: String(labels.close ?? "Close")
13296
+ };
13297
+ }
13298
+
11665
13299
  // src/zeyos/select.js
11666
13300
  function buildZeyosSelectConfig(client, resource, opts = {}) {
11667
13301
  const {
@@ -11703,6 +13337,90 @@ function zeyosSelect(client, resource, opts = {}) {
11703
13337
  select.ready = hydrateInitialValue(select, client, resource, requestedValue);
11704
13338
  return select;
11705
13339
  }
13340
+ function buildZeyosEntitySelectConfig(client, resource, opts = {}) {
13341
+ const {
13342
+ fields,
13343
+ labelKey = inferLabelKey(client, resource),
13344
+ valueKey = "ID",
13345
+ searchFields = inferSearchFields(client, resource),
13346
+ subtitleKey = null,
13347
+ moduleKey = () => resource,
13348
+ groupKey = null,
13349
+ none = true,
13350
+ create = false,
13351
+ renderIcon = defaultEntityIcon,
13352
+ renderItem,
13353
+ renderValueAdornment,
13354
+ ...rest
13355
+ } = opts;
13356
+ const projection = fields ?? defaultProjection(valueKey, labelKey, [
13357
+ ...searchFields,
13358
+ ...[subtitleKey, moduleKey, groupKey].filter((reader) => typeof reader === "string")
13359
+ ]);
13360
+ const base = buildZeyosSelectConfig(client, resource, {
13361
+ ...rest,
13362
+ fields: projection,
13363
+ labelKey,
13364
+ valueKey,
13365
+ searchFields,
13366
+ groupKey,
13367
+ clearable: none === false ? Boolean(rest.clearable) : true
13368
+ });
13369
+ const renderModuleIcon = (item, selected = false) => {
13370
+ if (renderIcon === null) return null;
13371
+ const module = readRecord(item, moduleKey) ?? resource;
13372
+ return renderIcon(item, { resource, module: String(module), selected });
13373
+ };
13374
+ base.renderItem = renderItem ?? ((item) => {
13375
+ const visual = renderModuleIcon(item);
13376
+ return h(
13377
+ "span",
13378
+ { class: "zx-select__entity" },
13379
+ visual ? h("span", {
13380
+ class: "zx-select__entity-icon",
13381
+ ariaHidden: "true"
13382
+ }, visual) : null,
13383
+ h(
13384
+ "span",
13385
+ { class: "zx-select__entity-copy" },
13386
+ h("span", { class: "zx-select__entity-label" }, String(readRecord(item, labelKey) ?? "")),
13387
+ subtitleKey && readRecord(item, subtitleKey) != null ? h("span", { class: "zx-select__entity-subtitle" }, String(readRecord(item, subtitleKey))) : null
13388
+ )
13389
+ );
13390
+ });
13391
+ base.renderValueAdornment = renderValueAdornment ?? ((item) => renderModuleIcon(item, true));
13392
+ if (none !== false) {
13393
+ base.noneLabel = typeof none === "string" ? none : rest.noneLabel ?? "No selection";
13394
+ base.renderNone = rest.renderNone ?? ((label) => h(
13395
+ "span",
13396
+ { class: "zx-select__entity-none" },
13397
+ h("span", { class: "zx-select__entity-none-icon", ariaHidden: "true" }),
13398
+ h("span", {}, label)
13399
+ ));
13400
+ }
13401
+ if (create) {
13402
+ const descriptor = create === true ? {} : create;
13403
+ const invoke = typeof descriptor.oninvoke === "function" ? descriptor.oninvoke : null;
13404
+ base.actions = [...Array.isArray(base.actions) ? base.actions : [], {
13405
+ id: descriptor.id ?? "create",
13406
+ label: descriptor.label ?? "Create new\u2026",
13407
+ group: descriptor.group ?? "New",
13408
+ icon: descriptor.icon ?? "plus",
13409
+ invoke: (detail) => invoke?.({ ...detail, resource })
13410
+ }];
13411
+ }
13412
+ return base;
13413
+ }
13414
+ function zeyosEntitySelect(client, resource, opts = {}) {
13415
+ const config2 = buildZeyosEntitySelectConfig(client, resource, opts);
13416
+ const requestedValue = opts.value ?? null;
13417
+ const select = (
13418
+ /** @type {Select & {ready: Promise<Select>}} */
13419
+ new Select(null, config2)
13420
+ );
13421
+ select.ready = hydrateInitialValue(select, client, resource, requestedValue);
13422
+ return select;
13423
+ }
11706
13424
  function normalizeZeyosListResult(client, result) {
11707
13425
  if (typeof client?.normalizeListResult === "function") {
11708
13426
  return normalizeLocalListResult(client.normalizeListResult(result));
@@ -11719,12 +13437,12 @@ function normalizeLocalListResult(result) {
11719
13437
  async function hydrateInitialValue(select, client, resource, value) {
11720
13438
  if (value == null) return select;
11721
13439
  const record = value && typeof value === "object" ? value : await connect(client).get(resource, value);
11722
- const normalized = normalizeRecord(record);
13440
+ const normalized = normalizeRecord2(record);
11723
13441
  select.setItems([normalized]);
11724
13442
  select.set(normalized, { silent: true });
11725
13443
  return select;
11726
13444
  }
11727
- function normalizeRecord(result) {
13445
+ function normalizeRecord2(result) {
11728
13446
  if (result && typeof result === "object" && !Array.isArray(result)) {
11729
13447
  const nested = result.data;
11730
13448
  if (nested && typeof nested === "object" && !Array.isArray(nested)) return nested;
@@ -11732,6 +13450,16 @@ function normalizeRecord(result) {
11732
13450
  }
11733
13451
  throw new TypeError("ZeyOS get operation did not return a record");
11734
13452
  }
13453
+ function readRecord(item, reader) {
13454
+ if (typeof reader === "function") return reader(item);
13455
+ if (item && typeof item === "object" && typeof reader === "string") return item[reader];
13456
+ return null;
13457
+ }
13458
+ function defaultEntityIcon(_item, context) {
13459
+ const chip = moduleChip(String(context.module ?? "default"), { size: 20 });
13460
+ chip.replaceChildren(icon("square", { size: 10 }));
13461
+ return chip;
13462
+ }
11735
13463
  function defaultProjection(valueKey, labelKey, searchFields) {
11736
13464
  const keys = [valueKey, labelKey, ...searchFields].filter((key) => typeof key === "string" && key);
11737
13465
  return keys.length ? [...new Set(keys)] : void 0;
@@ -11811,7 +13539,7 @@ function zeyosForm(client, resource, opts = {}) {
11811
13539
  await Promise.all(config2.entityFields.map(async ({ id, resource: referencedResource2 }) => {
11812
13540
  const value = record[id];
11813
13541
  if (value == null) return;
11814
- const selected = value && typeof value === "object" ? value : normalizeRecord2(await facade.get(referencedResource2, value));
13542
+ const selected = value && typeof value === "object" ? value : normalizeRecord3(await facade.get(referencedResource2, value));
11815
13543
  const select = Select.from(form.getField(id)?.getInput());
11816
13544
  if (select instanceof Select) select.setItems([selected]);
11817
13545
  }));
@@ -11820,7 +13548,7 @@ function zeyosForm(client, resource, opts = {}) {
11820
13548
  };
11821
13549
  const load = async (id) => {
11822
13550
  if (id == null || id === "") throw new TypeError("zeyosForm.load() requires an id");
11823
- const record = normalizeRecord2(await facade.get(resource, id));
13551
+ const record = normalizeRecord3(await facade.get(resource, id));
11824
13552
  currentId = record.ID ?? id;
11825
13553
  await applyRecord(record);
11826
13554
  return record;
@@ -11839,7 +13567,7 @@ function zeyosForm(client, resource, opts = {}) {
11839
13567
  throw error;
11840
13568
  }
11841
13569
  const result = currentId == null ? await facade.create(resource, payload) : await facade.update(resource, { ID: currentId, ...payload });
11842
- const record = normalizeRecord2(result);
13570
+ const record = normalizeRecord3(result);
11843
13571
  currentId = record.ID ?? currentId;
11844
13572
  if (opts.onSaved) {
11845
13573
  try {
@@ -11917,7 +13645,7 @@ function normalizeColumns(value) {
11917
13645
  [1, 2, 3].includes(number) ? number : 2
11918
13646
  );
11919
13647
  }
11920
- function normalizeRecord2(result) {
13648
+ function normalizeRecord3(result) {
11921
13649
  if (result && typeof result === "object" && !Array.isArray(result)) {
11922
13650
  const nested = result.data;
11923
13651
  if (nested && typeof nested === "object" && !Array.isArray(nested)) return nested;
@@ -11951,7 +13679,7 @@ function buildZeyosTableConfig(client, resource, opts = {}) {
11951
13679
  const monetary = isMonetaryField(meta);
11952
13680
  const column = fieldToZxColumn(monetary ? { ...meta, format: "money" } : meta);
11953
13681
  if (monetary) {
11954
- column.render = (row) => formatCurrency(row?.[meta.id], row?.currency);
13682
+ column.render = (row) => formatCurrency2(row?.[meta.id], row?.currency);
11955
13683
  if (schemaHasField(client, resource, "currency")) projection.currency = "currency";
11956
13684
  }
11957
13685
  return column;
@@ -12076,10 +13804,10 @@ function inferLabelKey2(client, resource) {
12076
13804
  return names[0] ?? "ID";
12077
13805
  }
12078
13806
  function isMonetaryField(meta) {
12079
- const format = String(meta.format ?? "").toLowerCase();
12080
- return /money|currency|price/.test(format) || /(?:amount|price)$/.test(String(meta.id).toLowerCase());
13807
+ const format2 = String(meta.format ?? "").toLowerCase();
13808
+ return /money|currency|price/.test(format2) || /(?:amount|price)$/.test(String(meta.id).toLowerCase());
12081
13809
  }
12082
- function formatCurrency(value, currency) {
13810
+ function formatCurrency2(value, currency) {
12083
13811
  if (value == null || value === "") return "";
12084
13812
  const number = Number(value);
12085
13813
  if (!Number.isFinite(number)) return "";
@@ -12112,7 +13840,11 @@ function copyFilters(filters) {
12112
13840
  }
12113
13841
  export {
12114
13842
  ZEYOS_ICON_KIT,
13843
+ ZEYOS_LAUNCHER_APPLICATIONS,
12115
13844
  buildListQuery,
13845
+ buildZeyosEntitySelectConfig,
13846
+ buildZeyosLauncherConfig,
13847
+ buildZeyosSelectConfig,
12116
13848
  connect,
12117
13849
  dataFilterStateToFilters,
12118
13850
  dateToUnixSeconds,
@@ -12127,13 +13859,18 @@ export {
12127
13859
  moduleInfo,
12128
13860
  moduleKeys,
12129
13861
  normalizeModuleName,
13862
+ normalizeZeyosLauncherApplications,
13863
+ normalizeZeyosLauncherRecords,
12130
13864
  registerFieldType,
12131
13865
  registerModules,
12132
13866
  resolveFields,
12133
13867
  tableSortToQuery,
12134
13868
  unixSecondsToDate,
12135
13869
  useZeyosIcons,
13870
+ zeyosActiveIdentifier,
13871
+ zeyosEntitySelect,
12136
13872
  zeyosForm,
13873
+ zeyosLauncher,
12137
13874
  zeyosModules,
12138
13875
  zeyosSelect,
12139
13876
  zeyosTable