@ixfx/components 0.7.0 → 0.7.1

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 (86) hide show
  1. package/bundle/index.d.ts +235 -27
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +799 -124
  4. package/bundle/index.js.map +1 -1
  5. package/dist/ac-text.d.ts +1 -1
  6. package/dist/ac-text.js +1 -1
  7. package/dist/{ac-token-BAa6ke09.d.ts → ac-token--_vCHXWw.d.ts} +2 -2
  8. package/dist/{ac-token-BAa6ke09.d.ts.map → ac-token--_vCHXWw.d.ts.map} +1 -1
  9. package/dist/ac-token.d.ts +1 -1
  10. package/dist/ac-token.js +1 -1
  11. package/dist/{button-Dat2yHAU.js → button-DsFbV4XD.js} +3 -3
  12. package/dist/{button-Dat2yHAU.js.map → button-DsFbV4XD.js.map} +1 -1
  13. package/dist/button.d.ts.map +1 -1
  14. package/dist/button.js +1 -1
  15. package/dist/{colour-picker-BSDBL-QL.js → colour-picker-Ddsk-7E_.js} +2 -2
  16. package/dist/{colour-picker-BSDBL-QL.js.map → colour-picker-Ddsk-7E_.js.map} +1 -1
  17. package/dist/colour-picker.js +1 -1
  18. package/dist/crumbs.d.ts +1 -1
  19. package/dist/crumbs.js +1 -1
  20. package/dist/{data-grid-C8qIo1-u.d.ts → data-grid-Bh4YrQGK.d.ts} +2 -2
  21. package/dist/{data-grid-C8qIo1-u.d.ts.map → data-grid-Bh4YrQGK.d.ts.map} +1 -1
  22. package/dist/data-grid.d.ts +1 -1
  23. package/dist/data-grid.js +1 -1
  24. package/dist/{defaults-BkWCPJFL.js → defaults-BJwDsMH4.js} +26 -2
  25. package/dist/defaults-BJwDsMH4.js.map +1 -0
  26. package/dist/{hex-editor-Bk6rxhfg.js → hex-editor-BiRfwTz1.js} +2 -2
  27. package/dist/{hex-editor-Bk6rxhfg.js.map → hex-editor-BiRfwTz1.js.map} +1 -1
  28. package/dist/hex.js +1 -1
  29. package/dist/{icon-DpA9x2Ve.d.ts → icon-CDGprSdE.d.ts} +1 -12
  30. package/dist/icon-CDGprSdE.d.ts.map +1 -0
  31. package/dist/{icon-CvnPanhA.js → icon-DcEEGQa_.js} +37 -8
  32. package/dist/icon-DcEEGQa_.js.map +1 -0
  33. package/dist/icons.d.ts +3 -3
  34. package/dist/icons.js +4 -4
  35. package/dist/icons.js.map +1 -1
  36. package/dist/{index-CKtLhgYj.d.ts → index-9AhaHIT2.d.ts} +3 -3
  37. package/dist/{index-CKtLhgYj.d.ts.map → index-9AhaHIT2.d.ts.map} +1 -1
  38. package/dist/{index-BtGFYfaK.d.ts → index-CS9yM8L-.d.ts} +14 -4
  39. package/dist/index-CS9yM8L-.d.ts.map +1 -0
  40. package/dist/index.d.ts +223 -23
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +674 -106
  43. package/dist/index.js.map +1 -1
  44. package/dist/{menu-BjRvlgPy.js → menu-CXDFJxNm.js} +3 -3
  45. package/dist/{menu-BjRvlgPy.js.map → menu-CXDFJxNm.js.map} +1 -1
  46. package/dist/{menu-item-Bs2MYyHq.d.ts → menu-item-CZi9vy5O.d.ts} +4 -2
  47. package/dist/menu-item-CZi9vy5O.d.ts.map +1 -0
  48. package/dist/{menu-item-B-zYrTTi.js → menu-item-D2S4i8u1.js} +16 -4
  49. package/dist/menu-item-D2S4i8u1.js.map +1 -0
  50. package/dist/menu.d.ts +2 -2
  51. package/dist/menu.js +2 -2
  52. package/dist/notification-pill-DyZydXoZ.d.ts.map +1 -1
  53. package/dist/notifier.js +2 -2
  54. package/dist/notifier.js.map +1 -1
  55. package/dist/panel.d.ts +1 -1
  56. package/dist/plots.js +1 -1
  57. package/dist/registry-BEtb8_wB.js +46 -0
  58. package/dist/registry-BEtb8_wB.js.map +1 -0
  59. package/dist/{tab-list-MPwq_1Jj.d.ts → tab-list-CGyPt07Z.d.ts} +9 -2
  60. package/dist/tab-list-CGyPt07Z.d.ts.map +1 -0
  61. package/dist/tabs.d.ts +1 -1
  62. package/dist/tabs.js +32 -13
  63. package/dist/tabs.js.map +1 -1
  64. package/dist/{titlebar-DaCfpliv.d.ts → titlebar-Bj5pfdw_.d.ts} +2 -2
  65. package/dist/{titlebar-DaCfpliv.d.ts.map → titlebar-Bj5pfdw_.d.ts.map} +1 -1
  66. package/dist/titlebar.d.ts +1 -1
  67. package/dist/titlebar.js +1 -1
  68. package/dist/tooltip-BlvBUQxx.d.ts.map +1 -1
  69. package/dist/{xy-axis-CxHC8z5E.js → xy-axis-DWizs0bt.js} +3 -3
  70. package/dist/xy-axis-DWizs0bt.js.map +1 -0
  71. package/docs-user/dock.md +166 -15
  72. package/docs-user/icons.md +4 -0
  73. package/docs-user/index.json +2 -2
  74. package/docs-user/tabs.md +20 -0
  75. package/docs-user/user-theming.md +109 -0
  76. package/package.json +1 -1
  77. package/dist/defaults-BkWCPJFL.js.map +0 -1
  78. package/dist/icon-CvnPanhA.js.map +0 -1
  79. package/dist/icon-DpA9x2Ve.d.ts.map +0 -1
  80. package/dist/index-BtGFYfaK.d.ts.map +0 -1
  81. package/dist/menu-item-B-zYrTTi.js.map +0 -1
  82. package/dist/menu-item-Bs2MYyHq.d.ts.map +0 -1
  83. package/dist/registry-CkW7q09Q.js +0 -21
  84. package/dist/registry-CkW7q09Q.js.map +0 -1
  85. package/dist/tab-list-MPwq_1Jj.d.ts.map +0 -1
  86. package/dist/xy-axis-CxHC8z5E.js.map +0 -1
package/bundle/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { customElement, eventOptions, property, query, queryAssignedElements, state } from "lit/decorators.js";
3
+ import { styleMap } from "lit/directives/style-map.js";
3
4
  import { unsafeHTML } from "lit/directives/unsafe-html.js";
4
5
  import { Fzf } from "fzf";
5
6
  import { Colour } from "@ixfx/visual";
@@ -7,7 +8,6 @@ import { createCommandRegistry } from "@clinth/ui-commands";
7
8
  import { Trees } from "@ixfx/collections";
8
9
  import { clamp, clamper, rangeStream, round, scaler } from "@ixfx/numbers";
9
10
  import { repeat } from "lit/directives/repeat.js";
10
- import { styleMap } from "lit/directives/style-map.js";
11
11
  import { Easings } from "@ixfx/modulation";
12
12
  import { classMap } from "lit/directives/class-map.js";
13
13
  import "@clinth/vcanvas";
@@ -1405,18 +1405,43 @@ function safeCustomElement(name) {
1405
1405
  //#endregion
1406
1406
  //#region src/icons/registry.ts
1407
1407
  const _registry = /* @__PURE__ */ new Map();
1408
+ let _iconFont;
1408
1409
  const iconBus = new EventTarget();
1409
1410
  function registerIcon(name, svg) {
1410
- _registry.set(name, svg);
1411
+ _registry.set(name, {
1412
+ kind: `svg`,
1413
+ svg
1414
+ });
1415
+ iconBus.dispatchEvent(new CustomEvent(`ixfx-icon-change`, { detail: { name } }));
1416
+ }
1417
+ function registerFontIcon(name, glyph) {
1418
+ let char;
1419
+ if (typeof glyph === `number`) char = String.fromCodePoint(glyph);
1420
+ else char = glyph;
1421
+ if (char.length === 0) throw new Error(`registerFontIcon: glyph must be non-empty`);
1422
+ _registry.set(name, {
1423
+ kind: `font`,
1424
+ char
1425
+ });
1411
1426
  iconBus.dispatchEvent(new CustomEvent(`ixfx-icon-change`, { detail: { name } }));
1412
1427
  }
1413
1428
  function getIcon(name) {
1429
+ const entry = _registry.get(name);
1430
+ return entry?.kind === `svg` ? entry.svg : void 0;
1431
+ }
1432
+ function getIconEntry(name) {
1414
1433
  return _registry.get(name);
1415
1434
  }
1416
1435
  function hasIcon(name) {
1417
1436
  return _registry.has(name);
1418
1437
  }
1419
- /** Converts an SVG string to a CSS data-URI value, e.g. for use as a mask-image. */
1438
+ function setIconFont(fontFamily) {
1439
+ _iconFont = fontFamily;
1440
+ iconBus.dispatchEvent(new CustomEvent(`ixfx-icon-font-change`, { detail: { name: `` } }));
1441
+ }
1442
+ function getIconFont() {
1443
+ return _iconFont;
1444
+ }
1420
1445
  function svgToDataUri(svg) {
1421
1446
  return `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
1422
1447
  }
@@ -1440,26 +1465,43 @@ let IxfxIconElement = class IxfxIconElement extends i {
1440
1465
  this.#onIconChange = (e) => {
1441
1466
  if (e.detail.name === this.name) this.requestUpdate();
1442
1467
  };
1468
+ this.#onFontChange = () => {
1469
+ if (getIconEntry(this.name)?.kind === `font`) this.requestUpdate();
1470
+ };
1443
1471
  }
1444
1472
  #onIconChange;
1473
+ #onFontChange;
1445
1474
  connectedCallback() {
1446
1475
  super.connectedCallback();
1447
1476
  iconBus.addEventListener(`ixfx-icon-change`, this.#onIconChange);
1477
+ iconBus.addEventListener(`ixfx-icon-font-change`, this.#onFontChange);
1448
1478
  }
1449
1479
  disconnectedCallback() {
1450
1480
  super.disconnectedCallback();
1451
1481
  iconBus.removeEventListener(`ixfx-icon-change`, this.#onIconChange);
1482
+ iconBus.removeEventListener(`ixfx-icon-font-change`, this.#onFontChange);
1452
1483
  }
1453
1484
  render() {
1454
- const svg = getIcon(this.name);
1455
- if (!svg) return A;
1485
+ const entry = getIconEntry(this.name);
1486
+ if (!entry) return A;
1456
1487
  const hasLabel = this.label.length > 0;
1488
+ if (entry.kind === `font`) {
1489
+ const fontFamily = getIconFont();
1490
+ return b`
1491
+ <span
1492
+ class="font-icon"
1493
+ style=${styleMap({ fontFamily: fontFamily ?? `` })}
1494
+ aria-hidden=${hasLabel ? A : `true`}
1495
+ aria-label=${hasLabel ? this.label : A}
1496
+ >${entry.char}</span>
1497
+ `;
1498
+ }
1457
1499
  return b`
1458
1500
  <span
1459
1501
  class="icon-wrapper"
1460
1502
  aria-hidden=${hasLabel ? A : `true`}
1461
1503
  aria-label=${hasLabel ? this.label : A}
1462
- >${unsafeHTML(svg)}</span>
1504
+ >${unsafeHTML(entry.svg)}</span>
1463
1505
  `;
1464
1506
  }
1465
1507
  updated(changed) {
@@ -1492,17 +1534,28 @@ let IxfxIconElement = class IxfxIconElement extends i {
1492
1534
  opacity: var(--prominence-disabled-opacity, 0.5);
1493
1535
  }
1494
1536
 
1495
- .icon-wrapper {
1537
+ .icon-wrapper,
1538
+ .font-icon {
1496
1539
  display: contents;
1497
1540
  }
1498
1541
 
1499
- :host svg {
1542
+ :host svg,
1543
+ :host .font-icon {
1500
1544
  width: 100%;
1501
1545
  height: 100%;
1502
1546
  transition: transform var(--transition, 0.15s ease-out);
1503
1547
  }
1504
1548
 
1505
- :host([rotate]) svg {
1549
+ :host .font-icon {
1550
+ font-family: inherit;
1551
+ line-height: 1;
1552
+ display: inline-flex;
1553
+ align-items: center;
1554
+ justify-content: center;
1555
+ }
1556
+
1557
+ :host([rotate]) svg,
1558
+ :host([rotate]) .font-icon {
1506
1559
  transform: rotate(var(--rotate));
1507
1560
  }
1508
1561
  `];
@@ -3164,6 +3217,11 @@ const ICON_CHECK = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="tr
3164
3217
  */
3165
3218
  const ICON_CLOSE = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>`;
3166
3219
  /**
3220
+ * Plus / add icon. Pairs with `close` for add/remove actions.
3221
+ * fill="currentColor" so it inherits CSS color.
3222
+ */
3223
+ const ICON_ADD = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>`;
3224
+ /**
3167
3225
  * Error / warning icon. Useful for error notifications.
3168
3226
  * fill="currentColor" so it inherits CSS color.
3169
3227
  */
@@ -3188,12 +3246,31 @@ const ICON_CHEVRON_RIGHT = `<svg viewBox="0 0 20 20" fill="currentColor" aria-hi
3188
3246
  * fill="currentColor" so it inherits CSS color.
3189
3247
  */
3190
3248
  const ICON_FOLDER_UP = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 4L4 12h5v8h6v-8h5L12 4z"/></svg>`;
3249
+ /**
3250
+ * Folder. Useful for file explorers and sidebars.
3251
+ * fill="currentColor" so it inherits CSS color.
3252
+ */
3253
+ const ICON_FOLDER = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 1.99-.9 1.99-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>`;
3254
+ /**
3255
+ * Document / file page. Useful for file lists and document tabs.
3256
+ * fill="currentColor" so it inherits CSS color.
3257
+ */
3258
+ const ICON_FILE = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>`;
3259
+ /**
3260
+ * Magnifying glass. Useful for search panels and inputs.
3261
+ * fill="currentColor" so it inherits CSS color.
3262
+ */
3263
+ const ICON_SEARCH = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>`;
3191
3264
  function registerDefaults() {
3192
3265
  registerIcon(`chevron-down`, ICON_CHEVRON_DOWN);
3193
3266
  registerIcon(`chevron-right`, ICON_CHEVRON_RIGHT);
3194
3267
  registerIcon(`caret-right`, ICON_CARET_RIGHT);
3195
3268
  registerIcon(`check`, ICON_CHECK);
3196
3269
  registerIcon(`close`, ICON_CLOSE);
3270
+ registerIcon(`file`, ICON_FILE);
3271
+ registerIcon(`folder`, ICON_FOLDER);
3272
+ registerIcon(`search`, ICON_SEARCH);
3273
+ registerIcon(`add`, ICON_ADD);
3197
3274
  registerIcon(`error`, ICON_ERROR);
3198
3275
  registerIcon(`folder-up`, ICON_FOLDER_UP);
3199
3276
  registerIcon(`info`, ICON_INFO);
@@ -3409,7 +3486,7 @@ let ButtonElement = class ButtonElement extends i {
3409
3486
  _renderIcon() {
3410
3487
  if (this.hideIcon || !this.icon) return A;
3411
3488
  if (this.icon.startsWith(`<svg`)) return b`<span class="button-icon">${unsafeHTML(this.icon)}</span>`;
3412
- if (!getIcon(this.icon)) return A;
3489
+ if (!hasIcon(this.icon)) return A;
3413
3490
  return b`<ixfx-icon class="button-icon" name="${this.icon}"></ixfx-icon>`;
3414
3491
  }
3415
3492
  render() {
@@ -4244,6 +4321,7 @@ let MenuTrigger = class MenuTrigger extends i {
4244
4321
  super(..._args);
4245
4322
  this.mode = `trigger`;
4246
4323
  this.label = `⋮`;
4324
+ this.iconName = ``;
4247
4325
  this.placement = `bottom-start`;
4248
4326
  this.tabStop = true;
4249
4327
  this._isOpen = false;
@@ -4453,11 +4531,18 @@ let MenuTrigger = class MenuTrigger extends i {
4453
4531
 
4454
4532
  .trigger-area {
4455
4533
  anchor-name: var(--anchor-name);
4456
- display: inline-flex;
4534
+ /* Block flex (not inline-flex): as an inline-level box this would join
4535
+ the host's inline formatting context and pick up strut/baseline gaps
4536
+ above and below the button, offsetting the glyph from center. */
4537
+ display: flex;
4457
4538
  align-items: stretch;
4458
4539
  }
4459
4540
 
4460
4541
  .trigger-btn {
4542
+ display: inline-flex;
4543
+ align-items: center;
4544
+ justify-content: center;
4545
+ line-height: 1;
4461
4546
  background: none;
4462
4547
  border: none;
4463
4548
  color: var(--surface-3-text);
@@ -4522,7 +4607,7 @@ let MenuTrigger = class MenuTrigger extends i {
4522
4607
  @click=${this._handleTriggerClick}
4523
4608
  @contextmenu=${this._handleContextMenu}
4524
4609
  >
4525
- ${showButton ? b`<button class="trigger-btn" part="base" tabindex=${this.tabStop ? `0` : `-1`}>${this.label}</button>` : null}
4610
+ ${showButton ? b`<button class="trigger-btn" part="base" tabindex=${this.tabStop ? `0` : `-1`}>${this.iconName ? b`<ixfx-icon name=${this.iconName}></ixfx-icon>` : this.label}</button>` : null}
4526
4611
  <slot name="trigger" @slotchange=${this._handleTriggerSlotChange}></slot>
4527
4612
  </div>
4528
4613
  <div
@@ -4537,6 +4622,10 @@ let MenuTrigger = class MenuTrigger extends i {
4537
4622
  };
4538
4623
  __decorate([property({ type: String })], MenuTrigger.prototype, "mode", void 0);
4539
4624
  __decorate([property({ type: String })], MenuTrigger.prototype, "label", void 0);
4625
+ __decorate([property({
4626
+ type: String,
4627
+ attribute: `icon-name`
4628
+ })], MenuTrigger.prototype, "iconName", void 0);
4540
4629
  __decorate([property({ attribute: false })], MenuTrigger.prototype, "registry", void 0);
4541
4630
  __decorate([property({ attribute: false })], MenuTrigger.prototype, "keyboardManager", void 0);
4542
4631
  __decorate([property({ type: String })], MenuTrigger.prototype, "placement", void 0);
@@ -12807,10 +12896,11 @@ function unregisterDockHost(host) {
12807
12896
  * Begin a dock-panel drag. Called by hosts in response to their own
12808
12897
  * `dock-panel-drag-start` events (titlebar or tab-item handles).
12809
12898
  */
12810
- function startDrag(panelId, sourceHost, pointerId, clientX, clientY) {
12899
+ function startDrag(panelId, sourceHost, pointerId, clientX, clientY, kind) {
12811
12900
  if (activeDrag !== null);
12812
12901
  activeDrag = {
12813
12902
  panelId,
12903
+ kind,
12814
12904
  sourceHost,
12815
12905
  pointerId,
12816
12906
  ghost: createGhostForPanel(sourceHost, panelId),
@@ -12883,7 +12973,7 @@ function onKeyDown(event) {
12883
12973
  }
12884
12974
  function findDropZone(drag, clientX, clientY) {
12885
12975
  for (const host of dragHosts) {
12886
- const zone = host.computeDropZone(clientX, clientY, drag.panelId);
12976
+ const zone = host.computeDropZone(clientX, clientY, drag.panelId, drag.kind);
12887
12977
  if (zone !== null) return zone;
12888
12978
  }
12889
12979
  return null;
@@ -12971,12 +13061,15 @@ function createEmptyLayout() {
12971
13061
  root: createEmptyStack()
12972
13062
  };
12973
13063
  }
12974
- function createStack(panels, active) {
12975
- return {
13064
+ function createStack(panels, active, name) {
13065
+ const stack = {
12976
13066
  kind: `stack`,
12977
13067
  panels: [...panels],
12978
13068
  active: active ?? panels[0] ?? ``
12979
13069
  };
13070
+ const trimmed = name?.trim();
13071
+ if (trimmed) stack.name = trimmed;
13072
+ return stack;
12980
13073
  }
12981
13074
  function createSplit(direction, children, sizes) {
12982
13075
  const mean = children.length > 0 ? 1 / children.length : 1;
@@ -12990,7 +13083,8 @@ function createSplit(direction, children, sizes) {
12990
13083
  /**
12991
13084
  * Runs after every mutation:
12992
13085
  * - stack `active` fixed to a member of `panels` (or `''` when empty)
12993
- * - empty stacks removed from their parent split (and their `sizes` entry)
13086
+ * - empty *unnamed* stacks removed from their parent split (and their
13087
+ * `sizes` entry); named empty stacks are kept so the name stays addressable
12994
13088
  * - splits with ≤1 child replaced by that child (spliced into the parent)
12995
13089
  * - a child split whose direction equals its parent's is flattened into it,
12996
13090
  * spreading its sizes across the slot it vacated
@@ -12999,13 +13093,14 @@ function createSplit(direction, children, sizes) {
12999
13093
  */
13000
13094
  function normalize(root) {
13001
13095
  if (isStack(root)) {
13096
+ sanitizeStackName(root);
13002
13097
  if (!root.panels.includes(root.active)) root.active = root.panels[0] ?? ``;
13003
13098
  return root;
13004
13099
  }
13005
13100
  for (let i = root.children.length - 1; i >= 0; i--) {
13006
13101
  normalize(root.children[i]);
13007
13102
  const child = root.children[i];
13008
- if (isStack(child) && child.panels.length === 0) {
13103
+ if (isStack(child) && child.panels.length === 0 && !child.name) {
13009
13104
  root.children.splice(i, 1);
13010
13105
  root.sizes.splice(i, 1);
13011
13106
  continue;
@@ -13039,17 +13134,36 @@ function replaceNodeContents(target, source) {
13039
13134
  const t = target;
13040
13135
  t.panels = source.panels;
13041
13136
  t.active = source.active;
13137
+ if (source.name !== void 0) t.name = source.name;
13138
+ else delete t.name;
13139
+ if (source.hideLabels !== void 0) t.hideLabels = source.hideLabels;
13140
+ else delete t.hideLabels;
13042
13141
  delete t.collapsed;
13043
13142
  delete t.preCollapseSize;
13143
+ if (source.collapsed !== void 0) t.collapsed = source.collapsed;
13144
+ if (source.preCollapseSize !== void 0) t.preCollapseSize = source.preCollapseSize;
13044
13145
  } else {
13045
13146
  target.kind = `split`;
13046
13147
  const t = target;
13047
13148
  t.direction = source.direction;
13048
13149
  t.children = source.children;
13049
13150
  t.sizes = source.sizes;
13151
+ const stale = t;
13152
+ delete stale.panels;
13153
+ delete stale.active;
13154
+ delete stale.name;
13155
+ delete stale.collapsed;
13156
+ delete stale.preCollapseSize;
13050
13157
  }
13051
13158
  return target;
13052
13159
  }
13160
+ /** Trim `name`; drop it when empty / non-string so lookups stay predictable. */
13161
+ function sanitizeStackName(stack) {
13162
+ if (stack.name === void 0) return;
13163
+ const trimmed = typeof stack.name === `string` ? stack.name.trim() : ``;
13164
+ if (trimmed) stack.name = trimmed;
13165
+ else delete stack.name;
13166
+ }
13053
13167
  function findNode(root, path) {
13054
13168
  let current = root;
13055
13169
  for (const index of path) {
@@ -13083,6 +13197,36 @@ function collectPanelIds(root) {
13083
13197
  return root.children.flatMap(collectPanelIds);
13084
13198
  }
13085
13199
  /**
13200
+ * Path of the first stack (tree order) whose `name` matches, or null.
13201
+ * Names are expected to be unique per layout; duplicates resolve first-wins.
13202
+ */
13203
+ function findStackPathByName(root, name) {
13204
+ const wanted = name.trim();
13205
+ if (!wanted) return null;
13206
+ return findStackPathByNameInner(root, wanted, []);
13207
+ }
13208
+ function findStackPathByNameInner(node, name, path) {
13209
+ if (isStack(node)) return node.name === name ? path : null;
13210
+ for (let i = 0; i < node.children.length; i++) {
13211
+ const found = findStackPathByNameInner(node.children[i], name, [...path, i]);
13212
+ if (found !== null) return found;
13213
+ }
13214
+ return null;
13215
+ }
13216
+ /** Names of every named stack leaf, in tree order. */
13217
+ function collectContainerNames(root) {
13218
+ const out = [];
13219
+ const visit = (node) => {
13220
+ if (isStack(node)) {
13221
+ if (node.name) out.push(node.name);
13222
+ return;
13223
+ }
13224
+ node.children.forEach(visit);
13225
+ };
13226
+ visit(root);
13227
+ return out;
13228
+ }
13229
+ /**
13086
13230
  * Paths of every stack leaf, in tree order. `[]` is the root itself when it
13087
13231
  * is a stack.
13088
13232
  */
@@ -13197,6 +13341,251 @@ function extractStack(root, stackPath, fallbackStack) {
13197
13341
  normalize(root);
13198
13342
  return stack;
13199
13343
  }
13344
+ /** Tolerance for edge-touch comparisons in unit-square geometry. */
13345
+ const GEOM_EPS = 1e-9;
13346
+ function pathsEqual(a, b) {
13347
+ return a.length === b.length && a.every((v, i) => v === b[i]);
13348
+ }
13349
+ /**
13350
+ * Resolve a {@link DockLeafRef} to a stack path. Strings match container
13351
+ * `name` first-wins; arrays must index an existing stack leaf. Returns null
13352
+ * when unresolvable.
13353
+ */
13354
+ function resolveLeafPath(root, ref) {
13355
+ if (typeof ref === `string`) return findStackPathByName(root, ref);
13356
+ if (!Array.isArray(ref)) return null;
13357
+ let current = root;
13358
+ for (const index of ref) {
13359
+ if (!Number.isInteger(index) || index < 0) return null;
13360
+ if (!isSplit(current)) return null;
13361
+ const next = current.children[index];
13362
+ if (!next) return null;
13363
+ current = next;
13364
+ }
13365
+ return isStack(current) ? [...ref] : null;
13366
+ }
13367
+ /** Absolute (unit-square) rect of every stack leaf, in tree order. */
13368
+ function computeLeafGeoms(root) {
13369
+ const out = [];
13370
+ const visit = (node, path, rect) => {
13371
+ if (isStack(node)) {
13372
+ out.push({
13373
+ path,
13374
+ name: node.name ?? null,
13375
+ rect
13376
+ });
13377
+ return;
13378
+ }
13379
+ const total = node.sizes.reduce((sum, w) => sum + w, 0);
13380
+ const safe = total > 0 ? total : node.children.length || 1;
13381
+ let offset = 0;
13382
+ node.children.forEach((child, i) => {
13383
+ const frac = (node.sizes[i] ?? safe / node.children.length) / safe;
13384
+ if (node.direction === `row`) {
13385
+ const w = rect.w * frac;
13386
+ visit(child, [...path, i], {
13387
+ x: rect.x + offset,
13388
+ y: rect.y,
13389
+ w,
13390
+ h: rect.h
13391
+ });
13392
+ offset += w;
13393
+ } else {
13394
+ const h = rect.h * frac;
13395
+ visit(child, [...path, i], {
13396
+ x: rect.x,
13397
+ y: rect.y + offset,
13398
+ w: rect.w,
13399
+ h
13400
+ });
13401
+ offset += h;
13402
+ }
13403
+ });
13404
+ };
13405
+ visit(root, [], {
13406
+ x: 0,
13407
+ y: 0,
13408
+ w: 1,
13409
+ h: 1
13410
+ });
13411
+ return out;
13412
+ }
13413
+ function intervalOverlap(a0, a1, b0, b1) {
13414
+ return Math.min(a1, b1) - Math.max(a0, b0);
13415
+ }
13416
+ /**
13417
+ * Geometric neighbours of a leaf — what the user sees above / below / left /
13418
+ * right of it. Each list holds `{ name, path }` (name is null for unnamed
13419
+ * leaves), ordered most-overlapping first. Returns null when `ref` is
13420
+ * unresolvable.
13421
+ */
13422
+ function getLeafNeighbours(root, ref) {
13423
+ const path = resolveLeafPath(root, ref);
13424
+ if (path === null) return null;
13425
+ const geoms = computeLeafGeoms(root);
13426
+ const self = geoms.find((g) => pathsEqual(g.path, path));
13427
+ if (!self) return null;
13428
+ const s = self.rect;
13429
+ const above = [];
13430
+ const below = [];
13431
+ const left = [];
13432
+ const right = [];
13433
+ geoms.forEach((g, order) => {
13434
+ if (g === self) return;
13435
+ const r = g.rect;
13436
+ const info = {
13437
+ name: g.name,
13438
+ path: [...g.path]
13439
+ };
13440
+ if (Math.abs(r.y + r.h - s.y) < GEOM_EPS) {
13441
+ const o = intervalOverlap(r.x, r.x + r.w, s.x, s.x + s.w);
13442
+ if (o > GEOM_EPS) above.push({
13443
+ info,
13444
+ overlap: o,
13445
+ order
13446
+ });
13447
+ }
13448
+ if (Math.abs(s.y + s.h - r.y) < GEOM_EPS) {
13449
+ const o = intervalOverlap(r.x, r.x + r.w, s.x, s.x + s.w);
13450
+ if (o > GEOM_EPS) below.push({
13451
+ info,
13452
+ overlap: o,
13453
+ order
13454
+ });
13455
+ }
13456
+ if (Math.abs(r.x + r.w - s.x) < GEOM_EPS) {
13457
+ const o = intervalOverlap(r.y, r.y + r.h, s.y, s.y + s.h);
13458
+ if (o > GEOM_EPS) left.push({
13459
+ info,
13460
+ overlap: o,
13461
+ order
13462
+ });
13463
+ }
13464
+ if (Math.abs(s.x + s.w - r.x) < GEOM_EPS) {
13465
+ const o = intervalOverlap(r.y, r.y + r.h, s.y, s.y + s.h);
13466
+ if (o > GEOM_EPS) right.push({
13467
+ info,
13468
+ overlap: o,
13469
+ order
13470
+ });
13471
+ }
13472
+ });
13473
+ const rank = (hits) => hits.sort((a, b) => b.overlap - a.overlap || a.order - b.order).map((h) => h.info);
13474
+ return {
13475
+ above: rank(above),
13476
+ below: rank(below),
13477
+ left: rank(left),
13478
+ right: rank(right)
13479
+ };
13480
+ }
13481
+ /**
13482
+ * Exchange two leaves' positions in the tree, each carrying its size slot so
13483
+ * the remaining structure and proportions are untouched. Returns false when
13484
+ * either path is not a stack leaf.
13485
+ */
13486
+ function swapLeaves(root, pathA, pathB) {
13487
+ if (pathsEqual(pathA, pathB)) return false;
13488
+ const nodeA = findNode(root, pathA);
13489
+ const nodeB = findNode(root, pathB);
13490
+ if (nodeA === null || nodeB === null || !isStack(nodeA) || !isStack(nodeB)) return false;
13491
+ const parentA = findParentOf(root, pathA);
13492
+ const parentB = findParentOf(root, pathB);
13493
+ if (parentA === null || parentB === null) return false;
13494
+ const indexA = pathA[pathA.length - 1];
13495
+ const indexB = pathB[pathB.length - 1];
13496
+ if (parentA === parentB) {
13497
+ const p = parentA;
13498
+ [p.children[indexA], p.children[indexB]] = [p.children[indexB], p.children[indexA]];
13499
+ [p.sizes[indexA], p.sizes[indexB]] = [p.sizes[indexB] ?? 1, p.sizes[indexA] ?? 1];
13500
+ return true;
13501
+ }
13502
+ const carriedNode = parentA.children[indexA];
13503
+ const carriedSize = parentA.sizes[indexA] ?? 1;
13504
+ parentA.children[indexA] = parentB.children[indexB];
13505
+ parentA.sizes[indexA] = parentB.sizes[indexB] ?? 1;
13506
+ parentB.children[indexB] = carriedNode;
13507
+ parentB.sizes[indexB] = carriedSize;
13508
+ return true;
13509
+ }
13510
+ /**
13511
+ * Move a leaf one position in `direction`, swapping with the neighbour it
13512
+ * shares the longest edge with. Returns false when `ref` is unresolvable or
13513
+ * nothing adjoins in that direction. Run `normalize()` afterwards.
13514
+ */
13515
+ function moveLeafStep(root, ref, direction) {
13516
+ const path = resolveLeafPath(root, ref);
13517
+ if (path === null) return false;
13518
+ const neighbours = getLeafNeighbours(root, ref);
13519
+ if (neighbours === null) return false;
13520
+ const list = direction === `up` ? neighbours.above : direction === `down` ? neighbours.below : direction === `left` ? neighbours.left : neighbours.right;
13521
+ if (list.length === 0) return false;
13522
+ return swapLeaves(root, path, list[0].path);
13523
+ }
13524
+ /**
13525
+ * Move a leaf to an edge of the overall layout, keeping the rest of the
13526
+ * tree intact as a single subtree and preserving the leaf's absolute size
13527
+ * along the target axis. Returns false when `ref` is unresolvable or the
13528
+ * layout holds a single leaf. Run `normalize()` afterwards (also safe to
13529
+ * call it twice — the host's commit does).
13530
+ */
13531
+ function moveLeafNodeToEdge(root, ref, edge) {
13532
+ const path = resolveLeafPath(root, ref);
13533
+ if (path === null || path.length === 0) return false;
13534
+ const self = computeLeafGeoms(root).find((g) => pathsEqual(g.path, path));
13535
+ if (!self) return false;
13536
+ const horizontal = edge === `left` || edge === `right`;
13537
+ const frac = Math.min(Math.max(horizontal ? self.rect.w : self.rect.h, .05), .95);
13538
+ if (!(frac > 0) || frac >= 1) return false;
13539
+ const parent = findParentOf(root, path);
13540
+ if (parent === null) return false;
13541
+ const [node] = parent.children.splice(path[path.length - 1], 1);
13542
+ parent.sizes.splice(path[path.length - 1], 1);
13543
+ if (!node) return false;
13544
+ normalize(root);
13545
+ const atStart = edge === `left` || edge === `top`;
13546
+ const direction = horizontal ? `row` : `column`;
13547
+ if (isStack(root)) {
13548
+ const rest = root;
13549
+ const remaining = {
13550
+ kind: `stack`,
13551
+ panels: [...rest.panels],
13552
+ active: rest.active
13553
+ };
13554
+ if (rest.name !== void 0) remaining.name = rest.name;
13555
+ if (rest.collapsed !== void 0) remaining.collapsed = rest.collapsed;
13556
+ if (rest.preCollapseSize !== void 0) remaining.preCollapseSize = rest.preCollapseSize;
13557
+ replaceNodeContents(root, {
13558
+ kind: `split`,
13559
+ direction,
13560
+ children: atStart ? [node, remaining] : [remaining, node],
13561
+ sizes: atStart ? [frac, 1 - frac] : [1 - frac, frac]
13562
+ });
13563
+ return true;
13564
+ }
13565
+ if (isSplit(root) && root.direction === direction) {
13566
+ const size = frac * (root.sizes.reduce((total, w) => total + w, 0) || root.children.length || 1) / (1 - frac);
13567
+ const at = atStart ? 0 : root.children.length;
13568
+ root.children.splice(at, 0, node);
13569
+ root.sizes.splice(at, 0, size);
13570
+ return true;
13571
+ }
13572
+ if (isSplit(root)) {
13573
+ const inner = {
13574
+ kind: `split`,
13575
+ direction: root.direction,
13576
+ children: root.children,
13577
+ sizes: root.sizes
13578
+ };
13579
+ replaceNodeContents(root, {
13580
+ kind: `split`,
13581
+ direction,
13582
+ children: atStart ? [node, inner] : [inner, node],
13583
+ sizes: atStart ? [frac, 1 - frac] : [1 - frac, frac]
13584
+ });
13585
+ return true;
13586
+ }
13587
+ return false;
13588
+ }
13200
13589
  function serializeLayout(root) {
13201
13590
  return {
13202
13591
  version: 1,
@@ -13210,6 +13599,8 @@ function serializeNode(node) {
13210
13599
  panels: [...node.panels],
13211
13600
  active: node.active
13212
13601
  };
13602
+ if (node.name !== void 0) out.name = node.name;
13603
+ if (node.hideLabels !== void 0) out.hideLabels = node.hideLabels;
13213
13604
  if (node.collapsed !== void 0) out.collapsed = node.collapsed;
13214
13605
  if (node.preCollapseSize !== void 0) out.preCollapseSize = node.preCollapseSize;
13215
13606
  return out;
@@ -14259,14 +14650,12 @@ let TabListElement = class TabListElement extends i {
14259
14650
  if (!el) return;
14260
14651
  if (!this.#isVertical) {
14261
14652
  if (event.deltaY === 0 || el.scrollWidth <= el.clientWidth) return;
14262
- const prev = el.scrollLeft;
14263
- el.scrollLeft = prev + event.deltaY;
14264
- if (el.scrollLeft !== prev) event.preventDefault();
14653
+ el.scrollLeft = el.scrollLeft + event.deltaY;
14654
+ event.preventDefault();
14265
14655
  } else {
14266
14656
  if (event.deltaX === 0 || el.scrollHeight <= el.clientHeight) return;
14267
- const prev = el.scrollTop;
14268
- el.scrollTop = prev + event.deltaX;
14269
- if (el.scrollTop !== prev) event.preventDefault();
14657
+ el.scrollTop = el.scrollTop + event.deltaX;
14658
+ event.preventDefault();
14270
14659
  }
14271
14660
  };
14272
14661
  this.#onSlotClick = (event) => {
@@ -14353,8 +14742,11 @@ let TabListElement = class TabListElement extends i {
14353
14742
  return this.orientation === `vertical-stack` || this.orientation === `vertical-side`;
14354
14743
  }
14355
14744
  get #containerClass() {
14356
- const base = this.scrollable ? `${this.orientation} scrollable` : this.orientation;
14357
- return this.dragMode === `reorder` ? `${base} reorder` : base;
14745
+ return [
14746
+ this.scrollable ? `${this.orientation} scrollable` : this.orientation,
14747
+ this.scrollable ? this.#isVertical ? `scroll-fade-y` : `scroll-fade-x` : ``,
14748
+ this.dragMode === `reorder` ? `reorder` : ``
14749
+ ].filter(Boolean).join(` `);
14358
14750
  }
14359
14751
  render() {
14360
14752
  const caretStyle = this.#computeCaretStyle();
@@ -14636,7 +15028,11 @@ let TabListElement = class TabListElement extends i {
14636
15028
  return this.shadowRoot?.getElementById(`container`) ?? null;
14637
15029
  }
14638
15030
  static {
14639
- this.styles = [themeFallbacks, i$3`
15031
+ this.styles = [
15032
+ themeFallbacks,
15033
+ scrollFadeStyles(`x`),
15034
+ scrollFadeStyles(`y`),
15035
+ i$3`
14640
15036
  :host {
14641
15037
  display: inline-block;
14642
15038
  --activated-colour: var(--accent);
@@ -14719,8 +15115,12 @@ let TabListElement = class TabListElement extends i {
14719
15115
  overflow: hidden;
14720
15116
  }
14721
15117
  #container.horizontal.scrollable {
14722
- overflow-x: auto;
15118
+ overflow-x: scroll;
14723
15119
  --allow-overflow: visible;
15120
+ scrollbar-width: none;
15121
+ }
15122
+ #container.horizontal.scrollable::-webkit-scrollbar {
15123
+ display: none;
14724
15124
  }
14725
15125
  #container.horizontal.scrollable ::slotted(ixfx-tab-list-item) {
14726
15126
  flex-shrink: 0;
@@ -14733,9 +15133,13 @@ let TabListElement = class TabListElement extends i {
14733
15133
  flex-direction: column-reverse;
14734
15134
  }
14735
15135
  #container.vertical-stack.scrollable {
14736
- overflow-y: auto;
15136
+ overflow-y: scroll;
14737
15137
  max-height: var(--scroll-max-height);
14738
15138
  --allow-overflow: visible;
15139
+ scrollbar-width: none;
15140
+ }
15141
+ #container.vertical-stack.scrollable::-webkit-scrollbar {
15142
+ display: none;
14739
15143
  }
14740
15144
  #container.vertical-side {
14741
15145
  flex-direction: column;
@@ -14745,9 +15149,13 @@ let TabListElement = class TabListElement extends i {
14745
15149
  flex-direction: column-reverse;
14746
15150
  }
14747
15151
  #container.vertical-side.scrollable {
14748
- overflow-y: auto;
15152
+ overflow-y: scroll;
14749
15153
  max-height: var(--scroll-max-height);
14750
15154
  --allow-overflow: visible;
15155
+ scrollbar-width: none;
15156
+ }
15157
+ #container.vertical-side.scrollable::-webkit-scrollbar {
15158
+ display: none;
14751
15159
  }
14752
15160
 
14753
15161
  ::slotted(ixfx-tab-list-item) {
@@ -14790,7 +15198,8 @@ let TabListElement = class TabListElement extends i {
14790
15198
  ::slotted(.activated) {
14791
15199
  --colour: var(--activated-colour);
14792
15200
  }
14793
- `];
15201
+ `
15202
+ ];
14794
15203
  }
14795
15204
  };
14796
15205
  __decorate([property({ type: String })], TabListElement.prototype, "orientation", void 0);
@@ -14822,9 +15231,13 @@ let TabListItemElement = class TabListItemElement extends i {
14822
15231
  this.registry = void 0;
14823
15232
  this._compact = false;
14824
15233
  this._hasToolbarContent = false;
15234
+ this._labelObserver = null;
14825
15235
  this._onToolbarSlotChange = () => {
14826
15236
  this._updateToolbarContent();
14827
15237
  };
15238
+ this._onLabelSlotChange = () => {
15239
+ this._syncLabelSizer();
15240
+ };
14828
15241
  }
14829
15242
  connectedCallback() {
14830
15243
  super.connectedCallback();
@@ -14833,6 +15246,13 @@ let TabListItemElement = class TabListItemElement extends i {
14833
15246
  });
14834
15247
  this._resizeObserver.observe(this);
14835
15248
  (this.shadowRoot?.querySelector(`slot[name="toolbar"]`))?.addEventListener(`slotchange`, this._onToolbarSlotChange);
15249
+ (this.shadowRoot?.querySelector(`slot:not([name])`))?.addEventListener(`slotchange`, this._onLabelSlotChange);
15250
+ this._labelObserver = new MutationObserver(() => this._syncLabelSizer());
15251
+ this._labelObserver.observe(this, {
15252
+ childList: true,
15253
+ characterData: true,
15254
+ subtree: true
15255
+ });
14836
15256
  }
14837
15257
  disconnectedCallback() {
14838
15258
  super.disconnectedCallback();
@@ -14840,7 +15260,10 @@ let TabListItemElement = class TabListItemElement extends i {
14840
15260
  this._resizeObserver = void 0;
14841
15261
  this._commandCleanup?.();
14842
15262
  this._commandCleanup = void 0;
15263
+ this._labelObserver?.disconnect();
15264
+ this._labelObserver = null;
14843
15265
  (this.shadowRoot?.querySelector(`slot[name="toolbar"]`))?.removeEventListener(`slotchange`, this._onToolbarSlotChange);
15266
+ (this.shadowRoot?.querySelector(`slot:not([name])`))?.removeEventListener(`slotchange`, this._onLabelSlotChange);
14844
15267
  }
14845
15268
  updated(changed) {
14846
15269
  super.updated(changed);
@@ -14863,12 +15286,25 @@ let TabListItemElement = class TabListItemElement extends i {
14863
15286
  if (id === this.commandActive) this.click();
14864
15287
  });
14865
15288
  }
15289
+ /**
15290
+ * Mirror the default-slot text into `.label[data-label]` for the `::after`
15291
+ * width sizer. Slotted toolbar content is excluded.
15292
+ */
15293
+ _syncLabelSizer() {
15294
+ const slot = this.shadowRoot?.querySelector(`slot:not([name])`);
15295
+ const label = this.shadowRoot?.querySelector(`.label`);
15296
+ if (slot === null || slot === void 0 || label === null || label === void 0) return;
15297
+ const text = slot.assignedNodes().filter((node) => !(node instanceof HTMLElement && node.hasAttribute(`slot`))).map((node) => node.textContent ?? ``).join(``);
15298
+ if (text) label.setAttribute(`data-label`, text);
15299
+ else label.removeAttribute(`data-label`);
15300
+ }
14866
15301
  _updateToolbarContent() {
14867
15302
  const slot = this.shadowRoot?.querySelector(`slot[name="toolbar"]`);
14868
15303
  this._hasToolbarContent = (slot?.assignedElements().length ?? 0) > 0;
14869
15304
  }
14870
15305
  firstUpdated() {
14871
15306
  this._updateToolbarContent();
15307
+ this._syncLabelSizer();
14872
15308
  }
14873
15309
  render() {
14874
15310
  const showLabel = !this.hideLabels || !this.iconName;
@@ -14910,15 +15346,21 @@ let TabListItemElement = class TabListItemElement extends i {
14910
15346
  i$3`
14911
15347
  :host {
14912
15348
  --selected-tab-color: var(--ixfx-selected-tab-color, var(--surface-2-text));
15349
+ --selected-tab-icon-color: var(--ixfx-selected-tab-icon-color, var(--accent));
14913
15350
  --selected-tab-border-width: var(--ixfx-selected-tab-border-width, 1px);
14914
15351
  --selected-tab-weight: var(--ixfx-selected-tab-weight, bold);
15352
+ --selected-tab-text-decoration: var(--ixfx-selected-tab-text-decoration, none);
15353
+ --selected-tab-text-decoration-color: var(--ixfx-selected-tab-text-decoration-color, var(--accent));
15354
+ --selected-tab-text-decoration-thickness: var(--ixfx-selected-tab-text-decoration-thickness, 0px);
15355
+ --selected-tab-text-decoration-offset: var(--ixfx-selected-tab-text-decoration-offset, 2px);
14915
15356
  --selected-background: var(--ixfx-tab-selected-background, var(--surface-5));
14916
15357
  --tab-padding: var(--ixfx-tab-padding, var(--space-s));
14917
15358
  display: inline-flex;
14918
15359
  min-width: 0;
14919
15360
  user-select: none;
14920
15361
  box-sizing: border-box;
14921
- font-size: var(--text-xs);
15362
+ font-size: var(--text-m);
15363
+ color: inherit;
14922
15364
  /* No explicit height: items stretch (align-self: stretch via the
14923
15365
  list's flex container) so every tab in a strip shares one height
14924
15366
  even when toolbars give some items taller content. */
@@ -14933,7 +15375,18 @@ let TabListItemElement = class TabListItemElement extends i {
14933
15375
  font-weight: 500;
14934
15376
  border: var(--selected-tab-border-width) solid transparent;
14935
15377
  gap: var(--space-s);
15378
+ transition: background var(--transition, 0.15s);
15379
+ }
14936
15380
 
15381
+ /* Hover ("tickle") background. When --ixfx-tab-hover-background is
15382
+ unset each state keeps its resting background, so existing
15383
+ consumers see no change. */
15384
+ :host(:hover:not([selected])) > div {
15385
+ background: var(--ixfx-tab-hover-background, transparent);
15386
+ }
15387
+
15388
+ :host([selected]:hover) > div {
15389
+ background: var(--ixfx-tab-hover-background, var(--selected-background));
14937
15390
  }
14938
15391
 
14939
15392
  :host([selected]) > div {
@@ -14947,6 +15400,13 @@ let TabListItemElement = class TabListItemElement extends i {
14947
15400
  border-radius:var(--radius-m);
14948
15401
  }
14949
15402
 
15403
+ :host([selected]) .label {
15404
+ text-decoration: var(--selected-tab-text-decoration);
15405
+ text-decoration-color: var(--selected-tab-text-decoration-color);
15406
+ text-decoration-thickness: var(--selected-tab-text-decoration-thickness);
15407
+ text-decoration-offset: var(--selected-tab-text-decoration-offset);
15408
+ }
15409
+
14950
15410
  .label {
14951
15411
  overflow: var(--allow-overflow, hidden);
14952
15412
  text-overflow: ellipsis;
@@ -14955,10 +15415,29 @@ let TabListItemElement = class TabListItemElement extends i {
14955
15415
  flex: 1 1 auto;
14956
15416
  }
14957
15417
 
15418
+ /* Width sizer: an invisible copy of the label, always set at the
15419
+ selected weight, so activating a tab (bold) never shifts the strip.
15420
+ Text is mirrored from the default slot via the data-label attribute
15421
+ (see _syncLabelSizer). */
15422
+ .label::after {
15423
+ content: attr(data-label);
15424
+ display: block;
15425
+ height: 0;
15426
+ overflow: hidden;
15427
+ visibility: hidden;
15428
+ font-weight: var(--selected-tab-weight);
15429
+ }
15430
+
14958
15431
  ixfx-icon {
14959
15432
  flex-shrink: 0;
14960
15433
  }
14961
15434
 
15435
+ /* Leading icon (direct child — not close/toolbar icons) picks up the
15436
+ accent when selected, so icon-only tabs read as active. */
15437
+ :host([selected]) > div > ixfx-icon {
15438
+ color: var(--selected-tab-icon-color);
15439
+ }
15440
+
14962
15441
  .actions {
14963
15442
  display: flex;
14964
15443
  align-items: center;
@@ -15370,6 +15849,7 @@ let DockPanelElement = class DockPanelElement extends i {
15370
15849
  this.iconName = ``;
15371
15850
  this.closable = false;
15372
15851
  this.collapsible = false;
15852
+ this.kind = `any`;
15373
15853
  this.accepts = ``;
15374
15854
  this.presentation = `solo`;
15375
15855
  this._collapsed = false;
@@ -15423,6 +15903,10 @@ let DockPanelElement = class DockPanelElement extends i {
15423
15903
  }
15424
15904
  connectedCallback() {
15425
15905
  super.connectedCallback();
15906
+ if (this.hasAttribute(`title`)) {
15907
+ if (!this.hasAttribute(`label`)) this.title = this.getAttribute(`title`) ?? ``;
15908
+ this.removeAttribute(`title`);
15909
+ }
15426
15910
  this.addEventListener(`pointerdown`, this._onPointerDown);
15427
15911
  }
15428
15912
  disconnectedCallback() {
@@ -15514,13 +15998,20 @@ __decorate([property({
15514
15998
  attribute: `panel-id`,
15515
15999
  reflect: true
15516
16000
  })], DockPanelElement.prototype, "panelId", void 0);
15517
- __decorate([property({ type: String })], DockPanelElement.prototype, "title", void 0);
16001
+ __decorate([property({
16002
+ type: String,
16003
+ attribute: `label`
16004
+ })], DockPanelElement.prototype, "title", void 0);
15518
16005
  __decorate([property({
15519
16006
  type: String,
15520
16007
  attribute: `icon-name`
15521
16008
  })], DockPanelElement.prototype, "iconName", void 0);
15522
16009
  __decorate([property({ type: Boolean })], DockPanelElement.prototype, "closable", void 0);
15523
16010
  __decorate([property({ type: Boolean })], DockPanelElement.prototype, "collapsible", void 0);
16011
+ __decorate([property({
16012
+ type: String,
16013
+ reflect: true
16014
+ })], DockPanelElement.prototype, "kind", void 0);
15524
16015
  __decorate([property({
15525
16016
  type: String,
15526
16017
  attribute: `accepts`
@@ -15552,13 +16043,16 @@ let DockHostElement = class DockHostElement extends i {
15552
16043
  this._dragActive = false;
15553
16044
  this._junctions = [];
15554
16045
  this._toolbarsOut = /* @__PURE__ */ new Map();
16046
+ this._leafContentRules = /* @__PURE__ */ new Map();
15555
16047
  this._tabDragPending = null;
15556
16048
  this._junctionsKey = ``;
15557
16049
  this._junctionsRaf = null;
15558
16050
  this._hostResizeObserver = null;
16051
+ this._toolbarSlots = /* @__PURE__ */ new Set();
16052
+ this._toolbarObserver = null;
15559
16053
  this._onDragStart = (event) => {
15560
16054
  const { panelId, pointerId, clientX, clientY } = event.detail;
15561
- startDrag(panelId, this, pointerId, clientX, clientY);
16055
+ startDrag(panelId, this, pointerId, clientX, clientY, this._findPanelEl(panelId)?.kind);
15562
16056
  };
15563
16057
  this._onPoolSlotChange = () => {
15564
16058
  this._syncPool();
@@ -15605,11 +16099,12 @@ let DockHostElement = class DockHostElement extends i {
15605
16099
  const pathStr = this.shadowRoot?.getElementById(detail.id)?.getAttribute(`data-dock-path`);
15606
16100
  if (pathStr === null || pathStr === void 0) return;
15607
16101
  const path = parsePath(pathStr);
15608
- const paths = collectStackPaths(this._model).filter((p) => p.join(`-`) !== path.join(`-`));
15609
- const fallbackNode = paths.length > 0 ? findNode(this._model, paths[0]) : null;
15610
- const fallback = fallbackNode !== null && isStack(fallbackNode) ? fallbackNode : void 0;
16102
+ const closing = findNode(this._model, path);
16103
+ if (closing === null || !isStack(closing)) return;
16104
+ const moving = [...closing.panels];
15611
16105
  this._returnAllToolbars();
15612
- extractStack(this._model, path, fallback);
16106
+ extractStack(this._model, path);
16107
+ for (const panelId of moving) addPanelToStack(this._firstStackAllowing(this._panelKind(panelId)), panelId);
15613
16108
  this._commit();
15614
16109
  };
15615
16110
  this._onTabChange = (event, path) => {
@@ -15674,11 +16169,21 @@ let DockHostElement = class DockHostElement extends i {
15674
16169
  this.addEventListener(`dock-panel-drag-start`, this._onDragStart);
15675
16170
  this._hostResizeObserver = new ResizeObserver(() => this._scheduleJunctions());
15676
16171
  this._hostResizeObserver.observe(this);
16172
+ this._scanToolbarSlots();
16173
+ this._toolbarObserver = new MutationObserver(() => this._scanToolbarSlots());
16174
+ this._toolbarObserver.observe(this, {
16175
+ childList: true,
16176
+ subtree: true,
16177
+ attributes: true,
16178
+ attributeFilter: [`slot`]
16179
+ });
15677
16180
  }
15678
16181
  disconnectedCallback() {
15679
16182
  super.disconnectedCallback();
15680
16183
  unregisterDockHost(this);
15681
16184
  this.removeEventListener(`dock-panel-drag-start`, this._onDragStart);
16185
+ this._toolbarObserver?.disconnect();
16186
+ this._toolbarObserver = null;
15682
16187
  this._hostResizeObserver?.disconnect();
15683
16188
  this._hostResizeObserver = null;
15684
16189
  if (this._junctionsRaf !== null) {
@@ -15731,6 +16236,19 @@ let DockHostElement = class DockHostElement extends i {
15731
16236
  _scanPanels() {
15732
16237
  return Array.from(this.children).filter((el) => el instanceof HTMLElement && el.tagName === `IXFX-DOCK-PANEL` && Boolean(el.panelId));
15733
16238
  }
16239
+ /** Refresh the set of provided `toolbar-<name>` slots; re-render on change. */
16240
+ _scanToolbarSlots() {
16241
+ const found = /* @__PURE__ */ new Set();
16242
+ for (const child of this.children) {
16243
+ if (!(child instanceof HTMLElement)) continue;
16244
+ const slot = child.getAttribute(`slot`) ?? ``;
16245
+ if (slot.startsWith(`toolbar-`) && slot.length > `toolbar-`.length) found.add(slot);
16246
+ }
16247
+ if (found.size !== this._toolbarSlots.size || [...found].some((s) => !this._toolbarSlots.has(s))) {
16248
+ this._toolbarSlots = found;
16249
+ this.requestUpdate();
16250
+ }
16251
+ }
15734
16252
  _findPanelEl(panelId) {
15735
16253
  return this._pool.find((p) => p.panelId === panelId) ?? this._scanPanels().find((p) => p.panelId === panelId);
15736
16254
  }
@@ -15744,7 +16262,33 @@ let DockHostElement = class DockHostElement extends i {
15744
16262
  return fresh;
15745
16263
  }
15746
16264
  _appendPanelToFirstStack(panelId) {
15747
- addPanelToStack(this._model.kind === `stack` ? this._model : this._firstStack(), panelId);
16265
+ const kind = this._panelKind(panelId);
16266
+ if (this._model.kind === `stack` && this._stackAllowsKind(this._model, kind)) {
16267
+ addPanelToStack(this._model, panelId);
16268
+ return;
16269
+ }
16270
+ addPanelToStack(this._firstStackAllowing(kind), panelId);
16271
+ }
16272
+ /** Normalized content kind of a panel (`'any'` when unknown/unset). */
16273
+ _panelKind(panelId) {
16274
+ return normalizeKind(this._findPanelEl(panelId)?.kind);
16275
+ }
16276
+ /** Whether `kind` may live in `stack` under the current leaf rules. */
16277
+ _stackAllowsKind(stack, kind) {
16278
+ if (stack.name === void 0) return true;
16279
+ const rules = this._leafContentRules.get(stack.name);
16280
+ if (rules === void 0) return true;
16281
+ if (rules.deny.has(kind)) return false;
16282
+ return rules.allow === null || rules.allow.has(kind);
16283
+ }
16284
+ /** First stack (tree order) whose rules admit `kind`, else the first stack. */
16285
+ _firstStackAllowing(kind) {
16286
+ for (const path of collectStackPaths(this._model)) {
16287
+ const node = findNode(this._model, path);
16288
+ if (node !== null && isStack(node) && this._stackAllowsKind(node, kind)) return node;
16289
+ }
16290
+ console.warn(`[ixfx-dock-host] no leaf admits kind "${kind}", using first stack`);
16291
+ return this._firstStack();
15748
16292
  }
15749
16293
  _syncPool() {
15750
16294
  const current = this._scanPanels();
@@ -15786,7 +16330,34 @@ let DockHostElement = class DockHostElement extends i {
15786
16330
  this.requestUpdate();
15787
16331
  this._emitChange();
15788
16332
  }
16333
+ /**
16334
+ * Dock a panel. Returns false (without touching the DOM) when the
16335
+ * explicitly targeted container denies the panel's `kind`.
16336
+ */
15789
16337
  addPanel(panel, target) {
16338
+ const kind = panel instanceof HTMLElement ? normalizeKind(panel.kind) : normalizeKind(panel.kind);
16339
+ const op = target?.op ?? `tabs`;
16340
+ let stackPath = null;
16341
+ let splitTarget = null;
16342
+ if (target?.container !== void 0) {
16343
+ const namedPath = findStackPathByName(this._model, target.container);
16344
+ if (namedPath === null) console.warn(`[ixfx-dock-host] addPanel: unknown container "${target.container}", appending to first admitting stack`);
16345
+ else if (op === `tabs` && !this._stackAllowsKindAt(namedPath, kind)) {
16346
+ console.warn(`[ixfx-dock-host] addPanel: container "${target.container}" does not admit kind "${kind}"`);
16347
+ return false;
16348
+ } else stackPath = namedPath;
16349
+ } else if (target?.stack) {
16350
+ const found = findStackOf(this._model, target.stack);
16351
+ if (found === null) console.warn(`[ixfx-dock-host] addPanel: unknown stack panel "${target.stack}", appending to first admitting stack`);
16352
+ else if (op === `tabs` && !this._stackAllowsKindAt(found, kind)) {
16353
+ console.warn(`[ixfx-dock-host] addPanel: target stack does not admit kind "${kind}"`);
16354
+ return false;
16355
+ } else stackPath = found;
16356
+ }
16357
+ if (stackPath !== null && op !== `tabs`) {
16358
+ splitTarget = stackPath;
16359
+ stackPath = null;
16360
+ }
15790
16361
  let el;
15791
16362
  if (panel instanceof HTMLElement) el = panel;
15792
16363
  else {
@@ -15796,19 +16367,16 @@ let DockHostElement = class DockHostElement extends i {
15796
16367
  if (panel.iconName !== void 0) el.iconName = panel.iconName;
15797
16368
  if (panel.closable !== void 0) el.closable = panel.closable;
15798
16369
  if (panel.collapsible !== void 0) el.collapsible = panel.collapsible;
16370
+ if (panel.kind !== void 0) el.kind = panel.kind;
15799
16371
  }
15800
16372
  if (el.parentElement !== this) this.appendChild(el);
15801
16373
  if (!this._pool.some((p) => p.panelId === el.panelId)) this._pool = [...this._pool, el];
15802
16374
  this._returnAllToolbars();
15803
- const op = target?.op ?? `tabs`;
15804
- if (target?.stack) {
15805
- const stackPath = findStackOf(this._model, target.stack);
15806
- if (stackPath !== null) {
15807
- if (op === `tabs`) {
15808
- const stack = findNode(this._model, stackPath);
15809
- if (stack !== null && isStack(stack)) addPanelToStack(stack, el.panelId, target.index);
15810
- } else insertBeside(this._model, stackPath, opToDirection(op), createStack([el.panelId]), `after`);
15811
- }
16375
+ if (collectPanelIds(this._model).includes(el.panelId)) this._removeFromModel(el.panelId);
16376
+ if (splitTarget !== null) insertBeside(this._model, splitTarget, opToDirection(op), createStack([el.panelId]), `after`);
16377
+ else if (stackPath !== null) {
16378
+ const stack = findNode(this._model, stackPath);
16379
+ if (stack !== null && isStack(stack)) addPanelToStack(stack, el.panelId, target?.index);
15812
16380
  } else this._appendPanelToFirstStack(el.panelId);
15813
16381
  normalize(this._model);
15814
16382
  this.requestUpdate();
@@ -15818,6 +16386,12 @@ let DockHostElement = class DockHostElement extends i {
15818
16386
  composed: true
15819
16387
  }));
15820
16388
  this._emitChange();
16389
+ return true;
16390
+ }
16391
+ /** Whether the stack at `stackPath` admits `kind` under the leaf rules. */
16392
+ _stackAllowsKindAt(stackPath, kind) {
16393
+ const stack = findNode(this._model, stackPath);
16394
+ return stack !== null && isStack(stack) && this._stackAllowsKind(stack, kind);
15821
16395
  }
15822
16396
  removePanel(panelId) {
15823
16397
  if (!collectPanelIds(this._model).includes(panelId)) return false;
@@ -15853,6 +16427,123 @@ let DockHostElement = class DockHostElement extends i {
15853
16427
  getPanelIds() {
15854
16428
  return collectPanelIds(this._model);
15855
16429
  }
16430
+ /** Names of every named stack leaf, in tree order. */
16431
+ getContainerNames() {
16432
+ return collectContainerNames(this._model);
16433
+ }
16434
+ /**
16435
+ * Snapshot of a named container (`{ name, panels, active }`), or null when
16436
+ * no stack carries that name. First match wins when duplicated.
16437
+ */
16438
+ getContainer(name) {
16439
+ const path = findStackPathByName(this._model, name);
16440
+ if (path === null) return null;
16441
+ const node = findNode(this._model, path);
16442
+ if (node === null || !isStack(node) || node.name === void 0) return null;
16443
+ return {
16444
+ name: node.name,
16445
+ panels: [...node.panels],
16446
+ active: node.active
16447
+ };
16448
+ }
16449
+ /** Panel-ids in a named container, in tab order (`[]` when unknown/empty). */
16450
+ getContainerPanels(name) {
16451
+ return this.getContainer(name)?.panels ?? [];
16452
+ }
16453
+ /** Panel elements in a named container, in tab order — for iterating children. */
16454
+ getContainerElements(name) {
16455
+ return this.getContainerPanels(name).map((id) => this._findPanelEl(id)).filter((el) => el !== void 0);
16456
+ }
16457
+ /**
16458
+ * Restrict a named container to only the given panel kinds — e.g.
16459
+ * `setLeafExclusiveContents('editor', ['document'])`. `tabs` drops,
16460
+ * `addPanel`, and re-homing into the leaf then refuse other kinds, while
16461
+ * edge splits beside the leaf stay allowed (they create a new leaf).
16462
+ * Pass `[]` to seal the leaf (nothing admitted). Warns when no stack
16463
+ * currently carries `name`, but the rule is still stored and applies once
16464
+ * one appears. Rules are host policy: kept across `loadLayout()`, never
16465
+ * serialized.
16466
+ */
16467
+ setLeafExclusiveContents(name, kinds) {
16468
+ const key = name.trim();
16469
+ if (!key) {
16470
+ console.warn(`[ixfx-dock-host] setLeafExclusiveContents: empty container name`);
16471
+ return;
16472
+ }
16473
+ const allow = new Set(kinds.map((k) => k.trim()).filter((k) => k.length > 0));
16474
+ const entry = this._leafContentRules.get(key) ?? {
16475
+ allow: null,
16476
+ deny: /* @__PURE__ */ new Set()
16477
+ };
16478
+ entry.allow = allow;
16479
+ this._leafContentRules.set(key, entry);
16480
+ if (findStackPathByName(this._model, key) === null) console.warn(`[ixfx-dock-host] setLeafExclusiveContents: no container named "${key}" yet; rule stored`);
16481
+ }
16482
+ /**
16483
+ * Forbid the given panel kinds in a named container — e.g.
16484
+ * `setLeafDenyContents('footer', ['files', 'document'])`. Everything else
16485
+ * stays admitted. Pass `[]` to lift the denial. See
16486
+ * {@link setLeafExclusiveContents} for scope/ordering notes.
16487
+ */
16488
+ setLeafDenyContents(name, kinds) {
16489
+ const key = name.trim();
16490
+ if (!key) {
16491
+ console.warn(`[ixfx-dock-host] setLeafDenyContents: empty container name`);
16492
+ return;
16493
+ }
16494
+ const deny = new Set(kinds.map((k) => k.trim()).filter((k) => k.length > 0));
16495
+ const entry = this._leafContentRules.get(key) ?? {
16496
+ allow: null,
16497
+ deny: /* @__PURE__ */ new Set()
16498
+ };
16499
+ entry.deny = deny;
16500
+ if (entry.allow === null && entry.deny.size === 0) this._leafContentRules.delete(key);
16501
+ else this._leafContentRules.set(key, entry);
16502
+ if (findStackPathByName(this._model, key) === null) console.warn(`[ixfx-dock-host] setLeafDenyContents: no container named "${key}" yet; rule stored`);
16503
+ }
16504
+ /** Remove every content rule for a named container. */
16505
+ clearLeafContentRestrictions(name) {
16506
+ this._leafContentRules.delete(name.trim());
16507
+ }
16508
+ /** Copy of a container's rules, or null when unrestricted. */
16509
+ getLeafContentRestrictions(name) {
16510
+ const rules = this._leafContentRules.get(name.trim());
16511
+ if (rules === void 0) return null;
16512
+ return {
16513
+ allow: rules.allow === null ? null : [...rules.allow],
16514
+ deny: [...rules.deny]
16515
+ };
16516
+ }
16517
+ /**
16518
+ * Geometric neighbours of a leaf (`name` or numeric `path`) — what sits
16519
+ * above / below / left / right of it on screen. Each entry is
16520
+ * `{ name, path }` (`name` is null for unnamed leaves), ordered
16521
+ * most-overlapping first. Returns null when the leaf is unknown.
16522
+ */
16523
+ getNeighbours(leaf) {
16524
+ return getLeafNeighbours(this._model, leaf);
16525
+ }
16526
+ /**
16527
+ * Move a leaf one position in `direction`, swapping with the neighbour it
16528
+ * shares the longest edge with (both keep the remaining structure intact).
16529
+ * Returns false when the leaf is unknown or nothing adjoins that way.
16530
+ */
16531
+ moveLeaf(leaf, direction) {
16532
+ if (!moveLeafStep(this._model, leaf, direction)) return false;
16533
+ this._commit();
16534
+ return true;
16535
+ }
16536
+ /**
16537
+ * Move a leaf to an edge of the overall layout (e.g. toggle a sidebar from
16538
+ * `left` to `right`). The rest of the layout stays intact as a single
16539
+ * subtree and the leaf keeps its absolute size along the target axis.
16540
+ * Returns false when the leaf is unknown or the layout holds one leaf.
16541
+ */
16542
+ moveLeafToEdge(leaf, edge) {
16543
+ if (!moveLeafNodeToEdge(this._model, leaf, edge)) return false;
16544
+ this._commit();
16545
+ return true;
16546
+ }
15856
16547
  /**
15857
16548
  * Capture a structured-clone-safe {@link DockPanelSnapshot} of a panel
15858
16549
  * (identity, chrome attributes, and a best-effort `innerHTML`). Returns
@@ -15867,6 +16558,7 @@ let DockHostElement = class DockHostElement extends i {
15867
16558
  iconName: el.iconName,
15868
16559
  closable: el.closable,
15869
16560
  collapsible: el.collapsible,
16561
+ kind: normalizeKind(el.kind),
15870
16562
  accepts: el.accepts,
15871
16563
  contentHTML: el.innerHTML
15872
16564
  };
@@ -15890,10 +16582,11 @@ let DockHostElement = class DockHostElement extends i {
15890
16582
  * `accepts` / `canAccept`. Pure query: no model mutation, no overlay.
15891
16583
  *
15892
16584
  * `panelId` is passed to `canAccept`; use any stable id for an external
15893
- * panel (defaults to `"__external__"`).
16585
+ * panel (defaults to `"__external__"`). `kind` covers leaf content rules
16586
+ * for panels unknown to this host (defaults to `'any'`).
15894
16587
  */
15895
- queryDropTarget(clientX, clientY, panelId = `__external__`) {
15896
- const zone = this.computeDropZone(clientX, clientY, panelId);
16588
+ queryDropTarget(clientX, clientY, panelId = `__external__`, kind) {
16589
+ const zone = this.computeDropZone(clientX, clientY, panelId, kind);
15897
16590
  return zone === null ? null : zoneToTarget(zone);
15898
16591
  }
15899
16592
  /**
@@ -15902,9 +16595,9 @@ let DockHostElement = class DockHostElement extends i {
15902
16595
  * one. Call on every pointer update from the outer coordinator, then
15903
16596
  * {@link endExternalDrop} when the drag leaves or ends.
15904
16597
  */
15905
- previewExternalDrop(clientX, clientY, panelId = `__external__`) {
16598
+ previewExternalDrop(clientX, clientY, panelId = `__external__`, kind) {
15906
16599
  this.setDragActive(true);
15907
- const zone = this.computeDropZone(clientX, clientY, panelId);
16600
+ const zone = this.computeDropZone(clientX, clientY, panelId, kind);
15908
16601
  this.setDropZone(zone);
15909
16602
  return zone === null ? null : zoneToTarget(zone);
15910
16603
  }
@@ -15917,8 +16610,9 @@ let DockHostElement = class DockHostElement extends i {
15917
16610
  * Recreate a panel from a {@link DockPanelSnapshot} (or a bare
15918
16611
  * {@link DockPanelInit}) and dock it. With `at` viewport coordinates the
15919
16612
  * panel lands where a drop there would put it (respecting `accepts` /
15920
- * `canAccept`); without them, or when the point misses the host, it goes
15921
- * into the first stack. Returns the created element, or null when a panel
16613
+ * `canAccept` and leaf content rules); without them, or when the point
16614
+ * misses the host, it goes into the first stack admitting its kind.
16615
+ * Returns the created element, or null when a panel
15922
16616
  * with that id is already docked here.
15923
16617
  */
15924
16618
  acceptExternalPanel(snapshot, at) {
@@ -15929,6 +16623,7 @@ let DockHostElement = class DockHostElement extends i {
15929
16623
  if (snapshot.iconName !== void 0) el.iconName = snapshot.iconName;
15930
16624
  if (snapshot.closable !== void 0) el.closable = snapshot.closable;
15931
16625
  if (snapshot.collapsible !== void 0) el.collapsible = snapshot.collapsible;
16626
+ if (`kind` in snapshot && snapshot.kind !== void 0) el.kind = snapshot.kind;
15932
16627
  if (`accepts` in snapshot && snapshot.accepts) el.setAttribute(`accepts`, snapshot.accepts);
15933
16628
  if (`contentHTML` in snapshot && snapshot.contentHTML) el.innerHTML = snapshot.contentHTML;
15934
16629
  this._returnAllToolbars();
@@ -15954,9 +16649,10 @@ let DockHostElement = class DockHostElement extends i {
15954
16649
  * a zone with rects relative to the host, or null when the pointer is
15955
16650
  * outside the host or the candidate zone is disallowed.
15956
16651
  */
15957
- computeDropZone(clientX, clientY, panelId) {
16652
+ computeDropZone(clientX, clientY, panelId, kind) {
15958
16653
  const hostRect = this.getBoundingClientRect();
15959
16654
  if (clientX < hostRect.left || clientX > hostRect.right || clientY < hostRect.top || clientY > hostRect.bottom) return null;
16655
+ const resolvedKind = kind?.trim() ? kind.trim() : this._panelKind(panelId);
15960
16656
  const rel = (r) => ({
15961
16657
  x: r.left - hostRect.left,
15962
16658
  y: r.top - hostRect.top,
@@ -15984,7 +16680,7 @@ let DockHostElement = class DockHostElement extends i {
15984
16680
  rect: rel(leafRect),
15985
16681
  caretRect: this._stripCaretRect(strip, gapIndex, hostRect)
15986
16682
  };
15987
- return this._zoneAllowed(panelId, zone) ? zone : null;
16683
+ return this._zoneAllowed(panelId, zone, resolvedKind) ? zone : null;
15988
16684
  }
15989
16685
  }
15990
16686
  const relX = (clientX - leafRect.left) / Math.max(leafRect.width, 1);
@@ -15998,7 +16694,7 @@ let DockHostElement = class DockHostElement extends i {
15998
16694
  gapIndex: stack.panels.length,
15999
16695
  rect: rel(leafRect)
16000
16696
  };
16001
- return this._zoneAllowed(panelId, zone) ? zone : null;
16697
+ return this._zoneAllowed(panelId, zone, resolvedKind) ? zone : null;
16002
16698
  }
16003
16699
  const r = rel(leafRect);
16004
16700
  let operation;
@@ -16044,7 +16740,7 @@ let DockHostElement = class DockHostElement extends i {
16044
16740
  side,
16045
16741
  rect: zoneRect
16046
16742
  };
16047
- return this._zoneAllowed(panelId, zone) ? zone : null;
16743
+ return this._zoneAllowed(panelId, zone, resolvedKind) ? zone : null;
16048
16744
  }
16049
16745
  const fromLeft = clientX - hostRect.left;
16050
16746
  const fromRight = hostRect.right - clientX;
@@ -16100,15 +16796,18 @@ let DockHostElement = class DockHostElement extends i {
16100
16796
  side,
16101
16797
  rect: zoneRect
16102
16798
  };
16103
- return this._zoneAllowed(panelId, zone) ? zone : null;
16799
+ return this._zoneAllowed(panelId, zone, resolvedKind) ? zone : null;
16104
16800
  }
16105
- _zoneAllowed(panelId, zone) {
16801
+ _zoneAllowed(panelId, zone, kind) {
16106
16802
  if (!tokenAllows(this.accepts, operationToToken(zone.operation))) return false;
16107
16803
  const stack = findNode(this._model, zone.targetPath);
16108
16804
  if (stack !== null && isStack(stack) && stack.panels.length === 1) {
16109
16805
  const target = this._findPanelEl(stack.panels[0]);
16110
16806
  if (target && !panelAllows(target.accepts, operationToToken(zone.operation))) return false;
16111
16807
  }
16808
+ if (zone.operation === `tabs` && stack !== null && isStack(stack)) {
16809
+ if (!this._stackAllowsKind(stack, kind?.trim() ? kind.trim() : this._panelKind(panelId))) return false;
16810
+ }
16112
16811
  if (this.canAccept && !this.canAccept(panelId, zone.operation)) return false;
16113
16812
  return true;
16114
16813
  }
@@ -16123,22 +16822,32 @@ let DockHostElement = class DockHostElement extends i {
16123
16822
  this._appendPanelToFirstStack(panelId);
16124
16823
  normalize(this._model);
16125
16824
  }
16126
- this._applyZone(panelId, zone);
16825
+ this._applyZone(panelId, zone, sourceHost === void 0 || sourceHost === this);
16127
16826
  this._commit();
16128
16827
  }
16129
16828
  /**
16130
16829
  * Move `panelId` (already present somewhere in the model) to the position
16131
16830
  * described by `zone`. Shared by in-page drops and external panel adoption.
16132
16831
  */
16133
- _applyZone(panelId, zone) {
16832
+ _applyZone(panelId, zone, transferName = false) {
16134
16833
  const sourcePath = findStackOf(this._model, panelId);
16834
+ let vacated = null;
16135
16835
  if (sourcePath !== null) {
16136
16836
  const source = findNode(this._model, sourcePath);
16137
- if (source !== null && isStack(source)) removePanelFromStack(source, panelId);
16837
+ if (source !== null && isStack(source)) {
16838
+ removePanelFromStack(source, panelId);
16839
+ if (source.panels.length === 0) vacated = source;
16840
+ }
16138
16841
  }
16139
16842
  if (zone.operation === `tabs`) {
16140
16843
  const target = findNode(this._model, zone.targetPath);
16141
- if (target !== null && isStack(target)) addPanelToStack(target, panelId, zone.gapIndex);
16844
+ if (target !== null && isStack(target)) {
16845
+ addPanelToStack(target, panelId, zone.gapIndex);
16846
+ if (transferName && vacated !== null && vacated !== target && vacated.name !== void 0 && target.name === void 0) {
16847
+ target.name = vacated.name;
16848
+ delete vacated.name;
16849
+ }
16850
+ }
16142
16851
  } else insertBeside(this._model, zone.targetPath, opToDirection(zone.operation), createStack([panelId]), zone.side);
16143
16852
  }
16144
16853
  /**
@@ -16274,16 +16983,6 @@ let DockHostElement = class DockHostElement extends i {
16274
16983
  };
16275
16984
  window.addEventListener(`click`, swallow, true);
16276
16985
  }
16277
- _splitStack(path, op) {
16278
- const stack = findNode(this._model, path);
16279
- if (stack === null || !isStack(stack) || stack.panels.length === 0) return;
16280
- const activeId = stack.active;
16281
- this._returnAllToolbars();
16282
- removePanelFromStack(stack, activeId);
16283
- normalize(this._model);
16284
- insertBeside(this._model, path, opToDirection(op), createStack([activeId]), `after`);
16285
- this._commit();
16286
- }
16287
16986
  _removeFromModel(panelId) {
16288
16987
  const stackPath = findStackOf(this._model, panelId);
16289
16988
  if (stackPath === null) return;
@@ -16414,6 +17113,7 @@ let DockHostElement = class DockHostElement extends i {
16414
17113
  id=${`dock-leaf-${pathStr}`}
16415
17114
  class="leaf"
16416
17115
  data-dock-path=${pathStr}
17116
+ data-dock-name=${stack.name ?? A}
16417
17117
  size=${size === void 0 ? A : String(size)}
16418
17118
  min=${collapsible ? `1px` : A}
16419
17119
  when-small=${collapsible ? `collapse` : A}
@@ -16425,26 +17125,19 @@ let DockHostElement = class DockHostElement extends i {
16425
17125
  <slot name="empty"><span>Drop panels here</span></slot>
16426
17126
  </div>
16427
17127
  `);
17128
+ const toolbarSlot = stack.name !== void 0 && this._toolbarSlots.has(`toolbar-${stack.name}`) ? b`<slot name="toolbar-${stack.name}" slot="toolbar"></slot>` : A;
16428
17129
  return leafAttrs(b`
16429
17130
  <ixfx-tab-list
16430
17131
  class="strip"
16431
17132
  orientation="horizontal"
17133
+ scrollable
17134
+ shrink
17135
+ ?hide-labels=${stack.hideLabels === true}
16432
17136
  @change=${(e) => this._onTabChange(e, path)}
16433
17137
  @pointerdown=${this._onStripPointerDown}
16434
17138
  >
16435
17139
  ${repeat(stack.panels, (id) => id, (id) => this._renderTabItem(stack, id))}
16436
- <div slot="toolbar" class="group-toolbar">
16437
- <button
16438
- class="chrome-btn"
16439
- title="Split horizontally (top/bottom)"
16440
- @click=${() => this._splitStack(path, `split-horizontal`)}
16441
- >↕</button>
16442
- <button
16443
- class="chrome-btn"
16444
- title="Split vertically (left/right)"
16445
- @click=${() => this._splitStack(path, `split-vertical`)}
16446
- >↔</button>
16447
- </div>
17140
+ ${toolbarSlot}
16448
17141
  </ixfx-tab-list>
16449
17142
  <div class="leaf-body">
16450
17143
  <slot name="dock-panel-${stack.active}"></slot>
@@ -16524,9 +17217,14 @@ let DockHostElement = class DockHostElement extends i {
16524
17217
  .strip {
16525
17218
  flex-shrink: 0;
16526
17219
  border: 0;
16527
- /* The leaf background shows through selected tabs; the accent
16528
- underline indicator carries the selection. */
17220
+ /* The leaf background shows through selected tabs; a text underline
17221
+ (not the border indicator) carries the selection. */
16529
17222
  --ixfx-tab-selected-background: transparent;
17223
+ --ixfx-selected-tab-border-width: 0;
17224
+ --ixfx-selected-tab-text-decoration: underline;
17225
+ --ixfx-selected-tab-text-decoration-thickness: 2px;
17226
+ --ixfx-selected-tab-text-decoration-offset: 4px;
17227
+ --ixfx-tab-hover-background: var(--surface-4);
16530
17228
  }
16531
17229
 
16532
17230
  /* Uniform tab height: a panel toolbar button would otherwise inflate
@@ -16535,6 +17233,11 @@ let DockHostElement = class DockHostElement extends i {
16535
17233
  height: var(--dock-tab-height, 30px);
16536
17234
  }
16537
17235
 
17236
+ /* Breathing room between strip toolbars and the leaf edge. */
17237
+ .strip::part(list-toolbar) {
17238
+ padding-right: var(--space-xs);
17239
+ }
17240
+
16538
17241
  .leaf-body {
16539
17242
  flex: 1;
16540
17243
  min-height: 0;
@@ -16556,38 +17259,6 @@ let DockHostElement = class DockHostElement extends i {
16556
17259
  margin: var(--space-s);
16557
17260
  }
16558
17261
 
16559
- .group-toolbar {
16560
- display: flex;
16561
- gap: 2px;
16562
- padding: 0 var(--space-xs);
16563
- }
16564
-
16565
- .chrome-btn {
16566
- display: inline-flex;
16567
- align-items: center;
16568
- justify-content: center;
16569
- width: 1.6em;
16570
- height: 1.6em;
16571
- padding: 0;
16572
- background: none;
16573
- border: none;
16574
- border-radius: var(--radius-s);
16575
- color: inherit;
16576
- cursor: pointer;
16577
- opacity: var(--prominence-idle-opacity);
16578
- transition: background var(--transition), opacity var(--transition);
16579
- }
16580
-
16581
- .chrome-btn:hover {
16582
- background: var(--surface-5);
16583
- opacity: var(--prominence-tickled-opacity);
16584
- }
16585
-
16586
- .chrome-btn:focus-visible {
16587
- outline: 2px solid var(--accent);
16588
- outline-offset: 1px;
16589
- }
16590
-
16591
17262
  .overlay {
16592
17263
  position: absolute;
16593
17264
  inset: 0;
@@ -16704,6 +17375,10 @@ function zoneToTarget(zone) {
16704
17375
  function cloneNode(node) {
16705
17376
  return JSON.parse(JSON.stringify(node));
16706
17377
  }
17378
+ /** Panel content kind, normalized (`'any'` when unset/blank). */
17379
+ function normalizeKind(kind) {
17380
+ return (kind?.trim() ?? ``) || `any`;
17381
+ }
16707
17382
  function parsePath(pathStr) {
16708
17383
  return pathStr.length === 0 ? [] : pathStr.split(`-`).filter((s) => s.length > 0).map(Number);
16709
17384
  }
@@ -26003,7 +26678,7 @@ let NotificationPillElement = class NotificationPillElement extends i {
26003
26678
  #renderIcon() {
26004
26679
  if (!this.icon) return A;
26005
26680
  if (this.icon.startsWith(`<svg`)) return b`<span class="pill-icon">${unsafeHTML(this.icon)}</span>`;
26006
- if (!getIcon(this.icon)) return A;
26681
+ if (!hasIcon(this.icon)) return A;
26007
26682
  return b`<ixfx-icon class="pill-icon" name="${this.icon}"></ixfx-icon>`;
26008
26683
  }
26009
26684
  render() {
@@ -26802,7 +27477,7 @@ let TooltipElement = class TooltipElement extends i {
26802
27477
  #renderIcon() {
26803
27478
  if (!this.icon) return A;
26804
27479
  if (this.icon.startsWith(`<svg`)) return b`<span class="tooltip-icon" style="display:inline-flex;align-items:center;margin-right:0.25em">${unsafeHTML(this.icon)}</span>`;
26805
- if (!getIcon(this.icon)) return A;
27480
+ if (!hasIcon(this.icon)) return A;
26806
27481
  return b`<ixfx-icon class="tooltip-icon" style="margin-right:0.25em" name="${this.icon}"></ixfx-icon>`;
26807
27482
  }
26808
27483
  render() {
@@ -35813,6 +36488,6 @@ function init() {
35813
36488
  console.log(`Init`);
35814
36489
  }
35815
36490
  //#endregion
35816
- export { AcTextElement, AcTokenElement, ButtonElement, Checkbox, ColourPicker, ColourPickerPopup, CrumbNavigationElement, CrumbPathController, CrumbPathElement, DataController, DataDisplayComponent, DataGridElement, DetailListElement, DockHostElement, DockPanelElement, EditableLabel, EditableNumber, FormElement, FormGroupElement, FormPartElement, GroupedItemListerElement, ICON_CARET_RIGHT, ICON_CHECK, ICON_CHEVRON_DOWN, ICON_CLOSE, IncrSearchTreeController as IncrSearchMillerController, IncrSearchTreeController, IxfxHexEditorElement, IxfxIconElement, IxfxTimelineElement, LabelledRadialInput, LabelledRangeInput, LabelledSliderInput, LedElement, menu_exports as Menu, MillerBaseElement, MillerListElement, MillerTreeController, NarrowedTextElement, NotificationManager, NotificationPillElement, PanelElement, PanelGroupElement, PlotDataSeries, PlotHistogram, PlotMultiAxis, PlotSingleAxis, PlotXyAxis, PolarPad, RadialInputElement, RadioGroup, RangeElement, RangeInput, RangeMultiElement, RatingElement, SelectHorizElement, SliderInputElement, SlidingNumberElement, SnackbarElement, SnapContainer, SplitButton, SplitLayoutElement, SplitPanelElement, SwipeElement, TabController, TabListElement, TabListItemElement, TabPanelElement, TabPanelsElement, TextShimmerElement, TimelineController, TimelineTrackTabElement, TitlebarElement, ToolbarElement, ToolbarGroupElement, TooltipElement, TransitoryLabelElement, TreeBaseElement, TreeController, TreeDataModel, TreeListElement, TwoSplitLayoutElement, VerticalListElement, XyPad, addPanelToStack, applyAgeModification, applyHighlights, applySnapshot, attachTimelineInteractions, buildHighlightTargets, buildModel, clearHighlights, collectDataResult, collectPanelIds, collectStackPaths, computeColour, createAcCompletionProvider, createElementIncrSearch, createEmptyLayout, createEmptyStack, createFuzzySearch, createIncrSearch, createPillRenderer, createSplit, createStack, createTreeListAdapter, createVerticalListAdapter, distribute, extractStack, findNode, findParentOf, findStackOf, getActiveDragPanelId, getIcon, hasIcon, iconBus, init, insertBeside, isDragging, isSplit, isStack, monitorIndeterminate, normalise, normalize, parseColourScaleAttr, parseLayout, pointerOverAnyHost, readBaseColour, registerIcon, removePanelFromStack, resolveCssVars, resolveGrid, resolveTracks, serializeLayout, setActive, svgToDataUri, timelineStyles, toSnapshot, walk, wrapRoot };
36491
+ export { AcTextElement, AcTokenElement, ButtonElement, Checkbox, ColourPicker, ColourPickerPopup, CrumbNavigationElement, CrumbPathController, CrumbPathElement, DataController, DataDisplayComponent, DataGridElement, DetailListElement, DockHostElement, DockPanelElement, EditableLabel, EditableNumber, FormElement, FormGroupElement, FormPartElement, GroupedItemListerElement, ICON_CARET_RIGHT, ICON_CHECK, ICON_CHEVRON_DOWN, ICON_CLOSE, IncrSearchTreeController as IncrSearchMillerController, IncrSearchTreeController, IxfxHexEditorElement, IxfxIconElement, IxfxTimelineElement, LabelledRadialInput, LabelledRangeInput, LabelledSliderInput, LedElement, menu_exports as Menu, MillerBaseElement, MillerListElement, MillerTreeController, NarrowedTextElement, NotificationManager, NotificationPillElement, PanelElement, PanelGroupElement, PlotDataSeries, PlotHistogram, PlotMultiAxis, PlotSingleAxis, PlotXyAxis, PolarPad, RadialInputElement, RadioGroup, RangeElement, RangeInput, RangeMultiElement, RatingElement, SelectHorizElement, SliderInputElement, SlidingNumberElement, SnackbarElement, SnapContainer, SplitButton, SplitLayoutElement, SplitPanelElement, SwipeElement, TabController, TabListElement, TabListItemElement, TabPanelElement, TabPanelsElement, TextShimmerElement, TimelineController, TimelineTrackTabElement, TitlebarElement, ToolbarElement, ToolbarGroupElement, TooltipElement, TransitoryLabelElement, TreeBaseElement, TreeController, TreeDataModel, TreeListElement, TwoSplitLayoutElement, VerticalListElement, XyPad, addPanelToStack, applyAgeModification, applyHighlights, applySnapshot, attachTimelineInteractions, buildHighlightTargets, buildModel, clearHighlights, collectContainerNames, collectDataResult, collectPanelIds, collectStackPaths, computeColour, computeLeafGeoms, createAcCompletionProvider, createElementIncrSearch, createEmptyLayout, createEmptyStack, createFuzzySearch, createIncrSearch, createPillRenderer, createSplit, createStack, createTreeListAdapter, createVerticalListAdapter, distribute, extractStack, findNode, findParentOf, findStackOf, findStackPathByName, getActiveDragPanelId, getIcon, getIconEntry, getIconFont, getLeafNeighbours, hasIcon, iconBus, init, insertBeside, isDragging, isSplit, isStack, monitorIndeterminate, moveLeafNodeToEdge, moveLeafStep, normalise, normalize, parseColourScaleAttr, parseLayout, pointerOverAnyHost, readBaseColour, registerFontIcon, registerIcon, removePanelFromStack, resolveCssVars, resolveGrid, resolveLeafPath, resolveTracks, serializeLayout, setActive, setIconFont, svgToDataUri, swapLeaves, timelineStyles, toSnapshot, walk, wrapRoot };
35817
36492
 
35818
36493
  //# sourceMappingURL=index.js.map