@ixfx/components 0.6.2 → 0.7.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 (152) hide show
  1. package/bundle/index.d.ts +1490 -615
  2. package/bundle/index.d.ts.map +1 -1
  3. package/bundle/index.js +20477 -15855
  4. package/bundle/index.js.map +1 -1
  5. package/bundle/style.css +1 -1
  6. package/dist/ac-text.js +3 -2
  7. package/dist/ac-text.js.map +1 -1
  8. package/dist/ac-token.js +3 -2
  9. package/dist/ac-token.js.map +1 -1
  10. package/dist/{button-Bn0BuLpO.js → button-Dat2yHAU.js} +4 -3
  11. package/dist/button-Dat2yHAU.js.map +1 -0
  12. package/dist/button.js +1 -1
  13. package/dist/checkbox.js +1 -1
  14. package/dist/{colour-picker-0stHB90J.js → colour-picker-BSDBL-QL.js} +4 -3
  15. package/dist/{colour-picker-0stHB90J.js.map → colour-picker-BSDBL-QL.js.map} +1 -1
  16. package/dist/colour-picker.js +1 -1
  17. package/dist/crumbs.js +4 -2
  18. package/dist/crumbs.js.map +1 -1
  19. package/dist/data-display.js +1 -1
  20. package/dist/data-grid.js +4 -2
  21. package/dist/data-grid.js.map +1 -1
  22. package/dist/{defaults-BCiDhZbF.js → defaults-BkWCPJFL.js} +3 -20
  23. package/dist/defaults-BkWCPJFL.js.map +1 -0
  24. package/dist/editable-label-rzXkjf1H.d.ts +75 -0
  25. package/dist/editable-label-rzXkjf1H.d.ts.map +1 -0
  26. package/dist/editable-label.d.ts +2 -75
  27. package/dist/editable-label.js +2 -1
  28. package/dist/editable-label.js.map +1 -1
  29. package/dist/{fallbacks-DEE1nCTt.js → fallbacks-DNI9xt0F.js} +2 -2
  30. package/dist/{fallbacks-DEE1nCTt.js.map → fallbacks-DNI9xt0F.js.map} +1 -1
  31. package/dist/{grouped-item-lister-_pMDwIo1.d.ts → grouped-item-lister-DnWB-eFT.d.ts} +2 -2
  32. package/dist/{grouped-item-lister-_pMDwIo1.d.ts.map → grouped-item-lister-DnWB-eFT.d.ts.map} +1 -1
  33. package/dist/grouped-item-lister.d.ts +1 -1
  34. package/dist/{icons-Bm2ByQ3s.js → icon-CvnPanhA.js} +3 -6
  35. package/dist/icon-CvnPanhA.js.map +1 -0
  36. package/dist/icons.js +8 -2
  37. package/dist/icons.js.map +1 -0
  38. package/dist/index-CG2vIgPv.d.ts +101 -0
  39. package/dist/index-CG2vIgPv.d.ts.map +1 -0
  40. package/dist/index.d.ts +655 -65
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +3682 -924
  43. package/dist/index.js.map +1 -1
  44. package/dist/interaction-D5XdKzbR.js +2 -0
  45. package/dist/labelled-radial-input.js +2 -1
  46. package/dist/labelled-radial-input.js.map +1 -1
  47. package/dist/labelled-slider-input-DzigjMwP.js +808 -0
  48. package/dist/labelled-slider-input-DzigjMwP.js.map +1 -0
  49. package/dist/labelled-slider-input.d.ts +2 -0
  50. package/dist/labelled-slider-input.js +2 -0
  51. package/dist/{types-DlVNJez_.d.ts → list-selection-types-DSuRNWpx.d.ts} +10 -2
  52. package/dist/list-selection-types-DSuRNWpx.d.ts.map +1 -0
  53. package/dist/menu-BjRvlgPy.js +825 -0
  54. package/dist/menu-BjRvlgPy.js.map +1 -0
  55. package/dist/menu-item-B-zYrTTi.js +797 -0
  56. package/dist/menu-item-B-zYrTTi.js.map +1 -0
  57. package/dist/menu.js +2 -1
  58. package/dist/miller.d.ts +1 -1
  59. package/dist/miller.js +3 -2
  60. package/dist/miller.js.map +1 -1
  61. package/dist/narrowed-text.js +4 -2
  62. package/dist/narrowed-text.js.map +1 -1
  63. package/dist/notifier.js +3 -2
  64. package/dist/notifier.js.map +1 -1
  65. package/dist/panel.d.ts +13 -13
  66. package/dist/panel.d.ts.map +1 -1
  67. package/dist/panel.js +37 -253
  68. package/dist/panel.js.map +1 -1
  69. package/dist/plots.js +1 -1
  70. package/dist/polar-pad.js +1 -1
  71. package/dist/{radial-input-Dk8Wydp5.js → radial-input-Dp5qz0ly.js} +2 -2
  72. package/dist/{radial-input-Dk8Wydp5.js.map → radial-input-Dp5qz0ly.js.map} +1 -1
  73. package/dist/radial-input.js +1 -1
  74. package/dist/range-input.js +1 -1
  75. package/dist/range.js +1 -1
  76. package/dist/registry-CkW7q09Q.js +21 -0
  77. package/dist/registry-CkW7q09Q.js.map +1 -0
  78. package/dist/select-horiz.js +3 -2
  79. package/dist/select-horiz.js.map +1 -1
  80. package/dist/{slider-input-XppH3vcy.d.ts → slider-input-BfRUW2ZB.d.ts} +16 -3
  81. package/dist/slider-input-BfRUW2ZB.d.ts.map +1 -0
  82. package/dist/{slider-input-CT4gcF-o.js → slider-input-De4eoT11.js} +382 -67
  83. package/dist/slider-input-De4eoT11.js.map +1 -0
  84. package/dist/slider-input.d.ts +1 -1
  85. package/dist/slider-input.js +1 -1
  86. package/dist/snackbar.js +1 -1
  87. package/dist/split-layout-BFSbUZQv.js +969 -0
  88. package/dist/split-layout-BFSbUZQv.js.map +1 -0
  89. package/dist/split-layout-JK3xWVX2.d.ts +179 -0
  90. package/dist/split-layout-JK3xWVX2.d.ts.map +1 -0
  91. package/dist/split-layout.d.ts +2 -60
  92. package/dist/split-layout.js +1 -617
  93. package/dist/style.css +1 -1
  94. package/dist/swipe.js +1 -1
  95. package/dist/{tab-list-D_1cYhB_.d.ts → tab-list-MPwq_1Jj.d.ts} +2 -1
  96. package/dist/tab-list-MPwq_1Jj.d.ts.map +1 -0
  97. package/dist/tabs.d.ts +1 -1
  98. package/dist/tabs.js +96 -14
  99. package/dist/tabs.js.map +1 -1
  100. package/dist/{tickled-controller-BtevrRLV.d.ts → tickled-controller-CRzdIEjJ.d.ts} +7 -1
  101. package/dist/tickled-controller-CRzdIEjJ.d.ts.map +1 -0
  102. package/dist/{tickled-controller-DkAU-0qL.js → tickled-controller-h9GmJ_bU.js} +8 -2
  103. package/dist/tickled-controller-h9GmJ_bU.js.map +1 -0
  104. package/dist/titlebar-DaCfpliv.d.ts +50 -0
  105. package/dist/titlebar-DaCfpliv.d.ts.map +1 -0
  106. package/dist/titlebar.d.ts +2 -0
  107. package/dist/titlebar.js +324 -0
  108. package/dist/titlebar.js.map +1 -0
  109. package/dist/{tree-BO6BXI2b.js → tree-Ch4W8qXQ.js} +4 -3
  110. package/dist/{tree-BO6BXI2b.js.map → tree-Ch4W8qXQ.js.map} +1 -1
  111. package/dist/tree.d.ts +1 -1
  112. package/dist/tree.js +1 -1
  113. package/dist/vertical-list-ClnqTkF3.js +814 -0
  114. package/dist/vertical-list-ClnqTkF3.js.map +1 -0
  115. package/dist/vertical-list.d.ts +8 -9
  116. package/dist/vertical-list.d.ts.map +1 -1
  117. package/dist/vertical-list.js +1 -736
  118. package/dist/{xy-axis-B6yMjckW.js → xy-axis-CxHC8z5E.js} +109 -3
  119. package/dist/xy-axis-CxHC8z5E.js.map +1 -0
  120. package/dist/xy-pad.js +1 -1
  121. package/docs-user/README.md +5 -0
  122. package/docs-user/detail-list.md +398 -0
  123. package/docs-user/dock.md +310 -0
  124. package/docs-user/index.json +27 -2
  125. package/docs-user/labelled-slider-input.md +198 -0
  126. package/docs-user/llms.txt +6 -1
  127. package/docs-user/panel.md +20 -1
  128. package/docs-user/slider-input.md +55 -15
  129. package/docs-user/split-layout.md +136 -170
  130. package/docs-user/tabs.md +34 -0
  131. package/docs-user/titlebar.md +102 -0
  132. package/docs-user/toolbar.md +95 -22
  133. package/docs-user/twosplit-layout.md +254 -0
  134. package/docs-user/user-catalog.md +8 -1
  135. package/llms.txt +6 -1
  136. package/package.json +1 -2
  137. package/dist/button-Bn0BuLpO.js.map +0 -1
  138. package/dist/defaults-BCiDhZbF.js.map +0 -1
  139. package/dist/editable-label.d.ts.map +0 -1
  140. package/dist/icons-Bm2ByQ3s.js.map +0 -1
  141. package/dist/menu-DIfU70AA.js +0 -1609
  142. package/dist/menu-DIfU70AA.js.map +0 -1
  143. package/dist/slider-input-CT4gcF-o.js.map +0 -1
  144. package/dist/slider-input-XppH3vcy.d.ts.map +0 -1
  145. package/dist/split-layout.d.ts.map +0 -1
  146. package/dist/split-layout.js.map +0 -1
  147. package/dist/tab-list-D_1cYhB_.d.ts.map +0 -1
  148. package/dist/tickled-controller-BtevrRLV.d.ts.map +0 -1
  149. package/dist/tickled-controller-DkAU-0qL.js.map +0 -1
  150. package/dist/types-DlVNJez_.d.ts.map +0 -1
  151. package/dist/vertical-list.js.map +0 -1
  152. package/dist/xy-axis-B6yMjckW.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,62 +1,66 @@
1
1
  import { n as buildHighlightTargets, r as clearHighlights, t as applyHighlights } from "./highlight-B-eQrhhb.js";
2
- import { t as themeFallbacks } from "./fallbacks-DEE1nCTt.js";
2
+ import { t as TickledController } from "./tickled-controller-h9GmJ_bU.js";
3
+ import { t as themeFallbacks } from "./fallbacks-DNI9xt0F.js";
3
4
  import { t as popupSurfaceStyles } from "./popup-D80sj2QC.js";
4
5
  import { n as safeCustomElement, t as __decorate } from "./decorate-VRK8rslU.js";
5
- import { c as iconBus, i as ICON_CLOSE, l as registerIcon, n as ICON_CHECK, o as getIcon, r as ICON_CHEVRON_DOWN, s as hasIcon, t as ICON_CARET_RIGHT, u as svgToDataUri } from "./defaults-BCiDhZbF.js";
6
- import { t as IxfxIconElement } from "./icons-Bm2ByQ3s.js";
6
+ import { a as svgToDataUri, i as registerIcon, n as hasIcon, r as iconBus, t as getIcon } from "./registry-CkW7q09Q.js";
7
+ import { t as IxfxIconElement } from "./icon-CvnPanhA.js";
7
8
  import { AcTextElement } from "./ac-text.js";
8
9
  import { t as DataController } from "./data-controller-DOlmqNKb.js";
9
10
  import { n as createIncrSearch, r as createFuzzySearch, t as createElementIncrSearch } from "./element-search-Y8SRfPGM.js";
10
11
  import { n as createAcCompletionProvider, t as IncrSearchTreeController } from "./incr-search-vCzuao-2.js";
11
12
  import { AcTokenElement } from "./ac-token.js";
13
+ import { i as ICON_CLOSE, n as ICON_CHECK, r as ICON_CHEVRON_DOWN, t as ICON_CARET_RIGHT } from "./defaults-BkWCPJFL.js";
14
+ import "./icons.js";
12
15
  import { t as prominenceStyles } from "./prominence-DlFShic3.js";
13
- import { n as buttonStyles, t as ButtonElement } from "./button-Bn0BuLpO.js";
14
- import { t as menu_exports } from "./menu-DIfU70AA.js";
16
+ import { n as buttonStyles, t as ButtonElement } from "./button-Dat2yHAU.js";
17
+ import { n as ListSelectionController, t as VerticalListElement } from "./vertical-list-ClnqTkF3.js";
18
+ import "./menu-item-B-zYrTTi.js";
15
19
  import { Checkbox } from "./checkbox.js";
16
- import { n as ColourPicker, t as ColourPickerPopup } from "./colour-picker-0stHB90J.js";
20
+ import { n as ColourPicker, t as ColourPickerPopup } from "./colour-picker-BSDBL-QL.js";
17
21
  import { LabelledRangeInput } from "./labelled-range-input.js";
18
22
  import { t as collectDataResult } from "./data-provider-35TyWclX.js";
19
- import { i as TreeDataModel, n as TreeListElement, r as TreeBaseElement, t as TreeController } from "./tree-BO6BXI2b.js";
23
+ import { i as TreeDataModel, n as TreeListElement, r as TreeBaseElement, t as TreeController } from "./tree-Ch4W8qXQ.js";
20
24
  import { t as scrollFadeStyles } from "./scroll-fade-CAs1EabA.js";
21
25
  import { NarrowedTextElement } from "./narrowed-text.js";
22
26
  import { CrumbNavigationElement } from "./crumbs.js";
23
27
  import { DataDisplayComponent } from "./data-display.js";
28
+ import { t as createDragGhost } from "./drag-ghost-DtSDvjri.js";
29
+ import { t as menu_exports } from "./menu-BjRvlgPy.js";
24
30
  import { DataGridElement } from "./data-grid.js";
31
+ import { a as distribute, c as toSnapshot, i as buildModel, l as walk, n as SplitPanelElement, o as normalise, r as applySnapshot, s as resolveTracks, t as SplitLayoutElement } from "./split-layout-BFSbUZQv.js";
32
+ import { TabListElement } from "./tabs.js";
33
+ import { TitlebarElement } from "./titlebar.js";
25
34
  import { EditableLabel } from "./editable-label.js";
26
- import { t as resolveAxisStep } from "./keyboard-B5xD7Fz-.js";
27
- import { t as WheelNudgeMixin } from "./wheel-nudge-BlSCe_hD.js";
35
+ import { n as EditableNumber, t as LabelledSliderInput } from "./labelled-slider-input-DzigjMwP.js";
28
36
  import { t as FormElement } from "./form-CsXJX15p.js";
29
37
  import { PanelElement } from "./panel.js";
30
38
  import { GroupedItemListerElement } from "./grouped-item-lister.js";
31
- import { VerticalListElement } from "./vertical-list.js";
32
39
  import { t as IxfxHexEditorElement } from "./hex-editor-Bk6rxhfg.js";
33
- import { t as RadialInputElement } from "./radial-input-Dk8Wydp5.js";
40
+ import { t as RadialInputElement } from "./radial-input-Dp5qz0ly.js";
34
41
  import { LabelledRadialInput } from "./labelled-radial-input.js";
42
+ import { t as SliderInputElement } from "./slider-input-De4eoT11.js";
35
43
  import { LedElement } from "./led.js";
36
44
  import { MillerBaseElement, MillerListElement, MillerTreeController } from "./miller.js";
37
45
  import { NotificationPillElement } from "./notifier.js";
38
- import { a as PlotDataSeries, c as parseColourScaleAttr, i as PlotMultiAxis, l as readBaseColour, n as PlotSingleAxis, o as applyAgeModification, r as PlotHistogram, s as computeColour, t as PlotXyAxis } from "./xy-axis-B6yMjckW.js";
46
+ import { a as TooltipElement, c as computeColour, i as PlotMultiAxis, l as parseColourScaleAttr, n as PlotSingleAxis, o as PlotDataSeries, r as PlotHistogram, s as applyAgeModification, t as PlotXyAxis, u as readBaseColour } from "./xy-axis-CxHC8z5E.js";
39
47
  import { PolarPad } from "./polar-pad.js";
40
48
  import { t as tooltipValueStyles } from "./tooltip-CJt3WLmN.js";
41
49
  import { RangeInput } from "./range-input.js";
42
50
  import { RangeElement } from "./range.js";
43
51
  import { SelectHorizElement } from "./select-horiz.js";
44
- import { t as SliderInputElement } from "./slider-input-CT4gcF-o.js";
45
52
  import { SnackbarElement } from "./snackbar.js";
46
53
  import { SnapContainer } from "./snap-container.js";
47
- import { SplitLayoutElement } from "./split-layout.js";
48
54
  import { SwipeElement } from "./swipe.js";
49
- import { TabListElement } from "./tabs.js";
50
55
  import { a as resolveCssVars, i as attachTimelineInteractions, n as TimelineTrackTabElement, o as resolveGrid, r as timelineStyles, s as TimelineController, t as IxfxTimelineElement } from "./timeline-B2fgDL4U.js";
51
56
  import { TransitoryLabelElement } from "./transitory-label.js";
52
57
  import { XyPad } from "./xy-pad.js";
53
- import { LitElement, css, html, nothing } from "lit";
58
+ import { LitElement, css, html, nothing, render } from "lit";
54
59
  import { customElement, eventOptions, property, query, queryAssignedElements, state } from "lit/decorators.js";
55
60
  import { unsafeHTML } from "lit/directives/unsafe-html.js";
56
61
  import { Trees } from "@ixfx/collections";
57
- import { clamper, scaler } from "@ixfx/numbers";
62
+ import { clamp, clamper, scaler } from "@ixfx/numbers";
58
63
  import { repeat } from "lit/directives/repeat.js";
59
- import { classMap } from "lit/directives/class-map.js";
60
64
  //#region src/button/radio-group.ts
61
65
  let RadioGroup = class RadioGroup extends LitElement {
62
66
  constructor(..._args) {
@@ -1503,606 +1507,3053 @@ __decorate([state()], CrumbPathElement.prototype, "_editStartIndex", void 0);
1503
1507
  __decorate([state()], CrumbPathElement.prototype, "_openCumPath", void 0);
1504
1508
  CrumbPathElement = __decorate([safeCustomElement(`ixfx-crumb-path`)], CrumbPathElement);
1505
1509
  //#endregion
1506
- //#region src/editable-label/editable-number.ts
1507
- const DRAG_THRESHOLD_PX = 3;
1508
- let EditableNumber = class EditableNumber extends WheelNudgeMixin(EditableLabel) {
1509
- #dragStartY;
1510
- #dragStartValue;
1511
- #dragging;
1512
- #hasDragged;
1513
- #dragCurrentValue;
1514
- #lastInputValue;
1515
- #firstUpdateComplete;
1516
- constructor() {
1517
- super();
1518
- this.dragSensitivity = 1;
1519
- this.noDrag = false;
1520
- this.precision = 0;
1521
- this.#dragStartY = 0;
1522
- this.#dragStartValue = 0;
1523
- this.#dragging = false;
1524
- this.#hasDragged = false;
1525
- this.#dragCurrentValue = 0;
1526
- this.#lastInputValue = ``;
1527
- this.#firstUpdateComplete = false;
1528
- this._stopNativeInput = (e) => {
1529
- e.stopPropagation();
1530
- };
1531
- this._handleMouseEnter = () => {
1532
- if (this.displayTextLogic !== `transient` || this._editing) return;
1533
- this._hovering = true;
1534
- this._clearTransientTimer();
1535
- this._transientTimer = window.setTimeout(() => {
1536
- this._transientShowingValue = true;
1537
- this._requestDisplayUpdate();
1538
- }, this.appearDelay);
1539
- };
1540
- this._handleMouseLeave = () => {
1541
- if (this.displayTextLogic !== `transient` || this._editing) return;
1542
- this._hovering = false;
1543
- this._clearTransientTimer();
1544
- if (this._transientShowingValue) this._transientTimer = window.setTimeout(() => {
1545
- if (!this._hovering) {
1546
- this._transientShowingValue = false;
1547
- this._requestDisplayUpdate();
1548
- }
1549
- }, this.disappearDelay);
1510
+ //#region src/detail-list/detail-list.ts
1511
+ const ITEM_SELECTOR = `:scope > li`;
1512
+ const SEARCH_HIGHLIGHT_KEY = `detail-list-search`;
1513
+ const LABEL_SPAN_CSS = `overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0`;
1514
+ let DetailListElement = class DetailListElement extends LitElement {
1515
+ constructor(..._args2) {
1516
+ super(..._args2);
1517
+ this.selectionMode = `single`;
1518
+ this.interactionMode = `standard`;
1519
+ this.sort = `none`;
1520
+ this.sortBy = ``;
1521
+ this.items = [];
1522
+ this.displayProperty = ``;
1523
+ this.iconProperty = ``;
1524
+ this.keyProperty = ``;
1525
+ this.itemWrap = `stop`;
1526
+ this._searchVisible = false;
1527
+ this._searchQuery = ``;
1528
+ this.#itemData = /* @__PURE__ */ new WeakMap();
1529
+ this.#grid = [];
1530
+ this.#positions = /* @__PURE__ */ new Map();
1531
+ this.#geometryScheduled = false;
1532
+ this.#builtFromProvider = [];
1533
+ this.#providerBuilt = 0;
1534
+ this.#data = new DataController(this);
1535
+ this.selection = new ListSelectionController({
1536
+ getOrderedItems: () => this._getVisibleItems(),
1537
+ isSelectable: (el) => this._isCheckable(el),
1538
+ getModes: () => ({
1539
+ selectionMode: this.selectionMode,
1540
+ interactionMode: this.interactionMode
1541
+ }),
1542
+ onChange: (selected, previous) => {
1543
+ this._syncItemStates();
1544
+ this._dispatch(`list-select`, {
1545
+ selected,
1546
+ previous
1547
+ });
1548
+ }
1549
+ });
1550
+ this.tickled = new TickledController(this, {
1551
+ getItems: () => this._getVisibleItems().map((el) => ({
1552
+ id: el,
1553
+ enabled: !el.hasAttribute(`not-checked`)
1554
+ })),
1555
+ onTickle: (id) => {
1556
+ if (id !== void 0) this._dispatch(`list-tickle`, { item: id });
1557
+ },
1558
+ onStateChange: () => this._syncItemStates(),
1559
+ itemWrap: this.itemWrap
1560
+ });
1561
+ this._onPointerOver = (e) => {
1562
+ const item = this._findListItem(e.composedPath());
1563
+ if (item) this.tickled.handlePointerEnter(item);
1550
1564
  };
1551
- this._handleClick = () => {
1552
- if (this.disabled) return;
1553
- this.renderRoot?.querySelector(`.number`)?.focus();
1565
+ this._onPointerLeave = (e) => {
1566
+ this.tickled.handlePointerLeave(e);
1554
1567
  };
1555
- this._handleFocus = () => {};
1556
- this._handleBeforeInput = (e) => {
1557
- if (e.inputType !== `insertText` || !e.data) return;
1558
- if (!this._isAllowedInput(e.data)) {
1568
+ this._onKeyDown = (e) => {
1569
+ if ((e.ctrlKey || e.metaKey) && e.key === `f`) {
1559
1570
  e.preventDefault();
1571
+ this._openSearch();
1560
1572
  return;
1561
1573
  }
1562
- const contentEl = this._contentEl;
1563
- if (!contentEl) return;
1564
- const currentContent = contentEl.textContent ?? ``;
1565
- const insertPosition = contentEl.selectionStart ?? currentContent.length;
1566
- const newValue = currentContent.slice(0, insertPosition) + e.data + currentContent.slice(insertPosition);
1567
- if (newValue !== this.#lastInputValue) {
1568
- this.#lastInputValue = newValue;
1569
- this.dispatchEvent(new CustomEvent(`input`, {
1570
- detail: { value: newValue },
1571
- bubbles: true,
1572
- composed: true
1573
- }));
1574
+ if ((e.ctrlKey || e.metaKey) && e.key === `a`) {
1575
+ if (this.selection.canSelectAll()) {
1576
+ e.preventDefault();
1577
+ this.selection.selectAll();
1578
+ }
1579
+ return;
1574
1580
  }
1575
- };
1576
- this._handleKeydown = (e) => {
1577
- if (!this._editing) return;
1578
- if (e.key === `Enter`) {
1581
+ if (this._searchVisible && e.key === `Escape`) {
1579
1582
  e.preventDefault();
1580
- this._tryCommit();
1581
- if (this.mode === `editor`) this._contentEl?.blur();
1582
- } else if (e.key === `Escape`) {
1583
- e.preventDefault();
1584
- this.dispatchEvent(new CustomEvent(`cancelled`, {
1585
- detail: { value: this._prevValue },
1586
- bubbles: true,
1587
- composed: true
1588
- }));
1589
- this._cancelEdit();
1590
- this.dispatchEvent(new CustomEvent(`change`, {
1591
- detail: { value: this._prevValue },
1592
- bubbles: true,
1593
- composed: true
1594
- }));
1595
- if (this.mode === `editor`) this._contentEl?.blur();
1583
+ this._closeSearch();
1584
+ return;
1596
1585
  }
1597
- };
1598
- this._onKeyDown = (e) => {
1599
- if (this._editing || this.disabled) return;
1600
- if (this._handleArrowKey(e)) return;
1601
- if (e.key.length === 1) {
1602
- this._onUserInteraction();
1603
- this._beginEdit();
1586
+ switch (e.key) {
1587
+ case `ArrowDown`:
1588
+ e.preventDefault();
1589
+ this.#navigateDown();
1590
+ if (e.shiftKey && this.interactionMode === `vscode`) {
1591
+ const cur = this.tickled.tickledId;
1592
+ if (cur) this.selection.rangeTo(cur);
1593
+ }
1594
+ break;
1595
+ case `ArrowUp`:
1596
+ e.preventDefault();
1597
+ this.#navigateUp();
1598
+ if (e.shiftKey && this.interactionMode === `vscode`) {
1599
+ const cur = this.tickled.tickledId;
1600
+ if (cur) this.selection.rangeTo(cur);
1601
+ }
1602
+ break;
1603
+ case `ArrowRight`:
1604
+ e.preventDefault();
1605
+ this.#navigateRight();
1606
+ break;
1607
+ case `ArrowLeft`:
1608
+ e.preventDefault();
1609
+ this.#navigateLeft();
1610
+ break;
1611
+ case `Home`:
1612
+ e.preventDefault();
1613
+ this.#navigateTo(this.#findFirstCheckable());
1614
+ break;
1615
+ case `End`:
1616
+ e.preventDefault();
1617
+ this.#navigateTo(this.#findLastCheckable());
1618
+ break;
1619
+ case `Enter`: {
1620
+ const id = this.tickled.tickledId;
1621
+ if (id) {
1622
+ e.preventDefault();
1623
+ this._dispatch(`list-item-click`, { item: id });
1624
+ this._activateItem(id);
1625
+ }
1626
+ break;
1627
+ }
1628
+ case ` `: {
1629
+ const id = this.tickled.tickledId;
1630
+ if (id) {
1631
+ e.preventDefault();
1632
+ this.selection.handleSpace(id);
1633
+ }
1634
+ break;
1635
+ }
1636
+ case `Escape`: this.selection.clear();
1604
1637
  }
1605
1638
  };
1606
- this._onPointerDown = (e) => {
1607
- if (e.button !== 0) return;
1608
- if (this._editing) return;
1609
- if (this.disabled || this.noDrag) return;
1610
- e.preventDefault();
1611
- this.renderRoot?.querySelector(`.number`)?.focus();
1612
- this.#dragStartY = e.clientY;
1613
- this.#dragStartValue = this._parseValue();
1614
- this.#dragCurrentValue = this.#dragStartValue;
1615
- this.#dragging = false;
1616
- this.#hasDragged = false;
1617
- this.setPointerCapture(e.pointerId);
1639
+ this._onClick = (e) => {
1640
+ const item = this._findListItem(e.composedPath());
1641
+ if (!item) return;
1642
+ this.focus();
1643
+ this._dispatch(`list-item-click`, { item });
1644
+ this.selection.handleClick(item, e);
1618
1645
  };
1619
- this._onPointerMove = (e) => {
1620
- if (!(e.buttons & 1)) return;
1621
- if (this._editing) return;
1622
- if (this.disabled || this.noDrag) return;
1623
- const deltaY = this.#dragStartY - e.clientY;
1624
- if (!this.#dragging && Math.abs(deltaY) < DRAG_THRESHOLD_PX) return;
1625
- this.#dragging = true;
1626
- this.#hasDragged = true;
1627
- this.toggleAttribute(`dragging`, true);
1628
- this._onUserInteraction();
1629
- const raw = this.#dragStartValue + deltaY * this.dragSensitivity * (this.step || 1);
1630
- const snapped = this._clamp(this._snap(raw));
1631
- this.#dragCurrentValue = this.step !== void 0 ? snapped : Number.parseFloat(snapped.toFixed(this.precision));
1632
- const dragValueStr = this.step !== void 0 ? this._formatNumber(this.#dragCurrentValue) : this.#dragCurrentValue.toFixed(this.precision);
1633
- if (dragValueStr !== this.#lastInputValue) {
1634
- this.#lastInputValue = dragValueStr;
1635
- this.dispatchEvent(new CustomEvent(`input`, {
1636
- detail: { value: dragValueStr },
1637
- bubbles: true,
1638
- composed: true
1639
- }));
1640
- }
1641
- this._requestDisplayUpdate();
1642
- this._updateValueCss(this._normalizedValue(this.#dragCurrentValue));
1646
+ this._onDblClick = (e) => {
1647
+ const item = this._findListItem(e.composedPath());
1648
+ if (item) this._activateItem(item);
1643
1649
  };
1644
- this._onPointerUp = (e) => {
1645
- if (this.hasPointerCapture(e.pointerId)) this.releasePointerCapture(e.pointerId);
1646
- if (!this.#dragging) return;
1647
- if (this.disabled || this.noDrag) return;
1648
- this.#dragging = false;
1649
- this.toggleAttribute(`dragging`, false);
1650
- const finalStr = this.step !== void 0 ? this._formatNumber(this.#dragCurrentValue) : this.#dragCurrentValue.toFixed(this.precision);
1651
- const prev = this.value;
1652
- this.value = finalStr;
1653
- if (finalStr !== prev) this.dispatchEvent(new CustomEvent(`change`, {
1654
- detail: { value: finalStr },
1655
- bubbles: true,
1656
- composed: true
1657
- }));
1650
+ this._onWheel = (e) => {
1651
+ e.preventDefault();
1652
+ this._listEl.scrollLeft += e.deltaY;
1658
1653
  };
1659
- this._interceptClick = (e) => {
1660
- if (this.#hasDragged) {
1661
- e.stopImmediatePropagation();
1662
- this.#hasDragged = false;
1654
+ this._onSearchInput = (e) => {
1655
+ const input = e.target;
1656
+ this._searchQuery = input.value;
1657
+ this.#search?.setQuery(this._searchQuery).then(() => this.#recomputeGeometry());
1658
+ };
1659
+ this._onSearchKeydown = (e) => {
1660
+ if (e.key === `Escape`) {
1661
+ e.stopPropagation();
1662
+ this._closeSearch();
1663
+ } else if (e.key === `ArrowDown`) {
1664
+ e.preventDefault();
1665
+ this.focus();
1666
+ this.#navigateDown();
1667
+ } else if (e.key === `ArrowUp`) {
1668
+ e.preventDefault();
1669
+ this.focus();
1670
+ this.#navigateUp();
1671
+ } else if (e.key === `ArrowRight`) e.preventDefault();
1672
+ else if (e.key === `ArrowLeft`) e.preventDefault();
1673
+ else if (e.key === `Enter`) {
1674
+ e.preventDefault();
1675
+ e.stopPropagation();
1676
+ const id = this.tickled.tickledId ?? this.#findFirstCheckable();
1677
+ if (id) {
1678
+ this._dispatch(`list-item-click`, { item: id });
1679
+ this._activateItem(id);
1680
+ this._closeSearch();
1681
+ }
1682
+ } else if (e.key === ` `) {
1683
+ const id = this.tickled.tickledId;
1684
+ if (id) {
1685
+ e.preventDefault();
1686
+ this.selection.handleSpace(id);
1687
+ }
1663
1688
  }
1664
1689
  };
1665
- this.mode = `editor`;
1666
- this.entryMode = `number`;
1667
- this.value = `0`;
1668
- }
1669
- render() {
1670
- return html`
1671
- ${this.fillStyle ? html`<div class="track"></div>` : ``}
1672
- ${this.fillStyle ? html`<div class="value"><div class="bar"></div></div>` : ``}
1673
- <div
1674
- class="number"
1675
- part="content"
1676
- tabindex="${this.disabled ? -1 : 0}"
1677
- contenteditable="${this._editing ? `plaintext-only` : `false`}"
1678
- data-placeholder="${this.placeholder}"
1679
- @click=${this._handleClick}
1680
- @focus=${this._handleFocus}
1681
- @blur=${this._handleBlur}
1682
- @keydown=${this._handleKeydown}
1683
- @beforeinput=${this._handleBeforeInput}
1684
- @input=${this._stopNativeInput}
1685
- ></div>
1686
- `;
1687
1690
  }
1691
+ #search;
1692
+ #itemData;
1693
+ #grid;
1694
+ #positions;
1695
+ #geometryScheduled;
1696
+ #resizeObserver;
1697
+ #mutationObserver;
1698
+ #builtFromProvider;
1699
+ #providerBuilt;
1700
+ #data;
1688
1701
  connectedCallback() {
1689
1702
  super.connectedCallback();
1690
- this.addEventListener(`pointerdown`, this._onPointerDown);
1691
- this.addEventListener(`pointermove`, this._onPointerMove);
1692
- this.addEventListener(`pointerup`, this._onPointerUp);
1703
+ if (!this.hasAttribute(`tabindex`)) this.tabIndex = 0;
1704
+ this.addEventListener(`pointerover`, this._onPointerOver);
1705
+ this.addEventListener(`pointerleave`, this._onPointerLeave);
1693
1706
  this.addEventListener(`keydown`, this._onKeyDown);
1694
- this.addEventListener(`click`, this._interceptClick, { capture: true });
1695
- this.addEventListener(`mouseenter`, this._handleMouseEnter);
1696
- this.addEventListener(`mouseleave`, this._handleMouseLeave);
1707
+ this.addEventListener(`click`, this._onClick);
1708
+ this.addEventListener(`dblclick`, this._onDblClick);
1709
+ this.addEventListener(`wheel`, this._onWheel);
1710
+ this.#resizeObserver = new ResizeObserver(() => this.#scheduleGeometry());
1711
+ this.#resizeObserver.observe(this);
1712
+ this.#mutationObserver = new MutationObserver(() => {
1713
+ this.#processChildren();
1714
+ this.#scheduleGeometry();
1715
+ });
1716
+ this.#mutationObserver.observe(this, {
1717
+ childList: true,
1718
+ subtree: false,
1719
+ attributeFilter: [`hidden`]
1720
+ });
1697
1721
  }
1698
1722
  disconnectedCallback() {
1699
1723
  super.disconnectedCallback();
1700
- this.removeEventListener(`pointerdown`, this._onPointerDown);
1701
- this.removeEventListener(`pointermove`, this._onPointerMove);
1702
- this.removeEventListener(`pointerup`, this._onPointerUp);
1724
+ this.removeEventListener(`pointerover`, this._onPointerOver);
1725
+ this.removeEventListener(`pointerleave`, this._onPointerLeave);
1703
1726
  this.removeEventListener(`keydown`, this._onKeyDown);
1704
- this.removeEventListener(`click`, this._interceptClick);
1705
- this.removeEventListener(`mouseenter`, this._handleMouseEnter);
1706
- this.removeEventListener(`mouseleave`, this._handleMouseLeave);
1707
- }
1708
- /** Override to use .number instead of .content for text display */
1709
- get _contentEl() {
1710
- return this.renderRoot?.querySelector(`.number`) ?? void 0;
1711
- }
1712
- firstUpdated(changedProperties) {
1713
- super.firstUpdated(changedProperties);
1714
- if (this.displayTextLogic === `show`) this._showModeWaitingToHide = true;
1715
- this._updateValueCss(this._normalizedValue());
1716
- this._requestDisplayUpdate();
1717
- }
1718
- _onUserInteraction() {
1719
- if (this.displayTextLogic === `show` && !this._editing) {
1720
- this._clearTransientTimer();
1721
- this._showModeWaitingToHide = false;
1722
- this._requestDisplayUpdate();
1723
- this._transientTimer = window.setTimeout(() => {
1724
- this._showModeWaitingToHide = true;
1725
- this._requestDisplayUpdate();
1726
- }, this.disappearDelay);
1727
+ this.removeEventListener(`click`, this._onClick);
1728
+ this.removeEventListener(`dblclick`, this._onDblClick);
1729
+ this.removeEventListener(`wheel`, this._onWheel);
1730
+ this.#resizeObserver?.disconnect();
1731
+ this.#resizeObserver = void 0;
1732
+ this.#mutationObserver?.disconnect();
1733
+ this.#mutationObserver = void 0;
1734
+ this.#search?.dispose();
1735
+ this.#search = void 0;
1736
+ }
1737
+ firstUpdated() {
1738
+ this.#processChildren();
1739
+ if (this.filterPredicate) this.#applyFilterPredicate();
1740
+ this.#search = createElementIncrSearch({
1741
+ container: this,
1742
+ itemSelector: ITEM_SELECTOR,
1743
+ getLabel: (el) => el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
1744
+ getKey: (el) => el.getAttribute(`data-key`) ?? el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
1745
+ highlightKey: SEARCH_HIGHLIGHT_KEY
1746
+ });
1747
+ this.#recomputeGeometry();
1748
+ }
1749
+ updated(changed) {
1750
+ super.updated(changed);
1751
+ if (changed.has(`provider`)) {
1752
+ this.#builtFromProvider = [];
1753
+ this.#providerBuilt = 0;
1754
+ this.#data.provider = this.provider;
1755
+ queueMicrotask(() => {
1756
+ if (this.provider) this.#data.request(void 0);
1757
+ });
1727
1758
  return;
1728
1759
  }
1729
- if (this.displayTextLogic === `transient` && !this._editing) {
1730
- this._clearTransientTimer();
1731
- this._transientShowingValue = true;
1732
- this._requestDisplayUpdate();
1733
- this._transientTimer = window.setTimeout(() => {
1734
- if (!this._hovering) {
1735
- this._transientShowingValue = false;
1736
- this._requestDisplayUpdate();
1737
- }
1738
- }, this.disappearDelay);
1739
- return;
1760
+ if (changed.has(`items`) && this.provider === void 0 && this.items.length > 0) this.#rebuildFromObjects(this.items);
1761
+ this.#syncFromProvider();
1762
+ if (changed.has(`sort`) || changed.has(`sortBy`) || changed.has(`sortComparator`)) this.#applySort();
1763
+ if (changed.has(`filterPredicate`)) {
1764
+ this.#applyFilterPredicate();
1765
+ this.#scheduleGeometry();
1740
1766
  }
1741
- super._onUserInteraction();
1742
- }
1743
- _onValueChange() {
1744
- if (this.displayTextLogic !== `show` || this._editing) return;
1745
- this._clearTransientTimer();
1746
- this._showModeWaitingToHide = false;
1747
- this._requestDisplayUpdate();
1748
- this._transientTimer = window.setTimeout(() => {
1749
- this._showModeWaitingToHide = true;
1750
- this._requestDisplayUpdate();
1751
- }, this.disappearDelay);
1767
+ if (changed.has(`interactionMode`)) {
1768
+ this.selection.clearAnchor();
1769
+ this._syncItemStates();
1770
+ } else if (changed.has(`checkboxRenderer`) && this.interactionMode === `checked`) this._syncCheckboxes();
1752
1771
  }
1753
- updated(changedProperties) {
1754
- super.updated(changedProperties);
1755
- if (!this._editing) this._updateValueCss(this._normalizedValue());
1756
- }
1757
- _updateValueCss(normalized) {
1758
- this.style.setProperty(`--value`, String(normalized));
1759
- let valueStart = 0;
1760
- let valueEnd = normalized;
1761
- if (this.linearStyle === `bipolar`) {
1762
- if (normalized <= .5) {
1763
- valueStart = normalized;
1764
- valueEnd = .5;
1765
- } else {
1766
- valueStart = .5;
1767
- valueEnd = normalized;
1768
- }
1769
- } else if (this.linearStyle === `unipolar`) {
1770
- valueStart = .5 - normalized * .5;
1771
- valueEnd = .5 + normalized * .5;
1772
- }
1773
- this.style.setProperty(`--value-start`, String(valueStart));
1774
- this.style.setProperty(`--value-end`, String(valueEnd));
1775
- if (this.fillStyle === `circular` && this.circularStyle === `offset`) {
1776
- const h = this.offsetHeight;
1777
- const w = this.offsetWidth;
1778
- const r = h / 2 - 2;
1779
- const leftEdge = 2;
1780
- const rightEdge = w - 2;
1781
- const valueCenterPx = leftEdge + r + normalized * (rightEdge - leftEdge - 2 * r);
1782
- this.style.setProperty(`--radius`, `${r}px`);
1783
- this.style.setProperty(`--value-center-px`, `${valueCenterPx}px`);
1784
- this.style.setProperty(`--component-width-px`, `${w}px`);
1785
- this.style.setProperty(`--component-height-px`, `${h}px`);
1786
- }
1772
+ /** The most recently selected item, or `undefined` if nothing is selected. */
1773
+ get selectedItem() {
1774
+ return [...this.selection.selected].at(-1);
1775
+ }
1776
+ /** The full current selection set (read-only). */
1777
+ get selectedItems() {
1778
+ return this.selection.selected;
1787
1779
  }
1788
- _getDisplayText() {
1789
- if (this._shouldUseDisplayText()) return this.displayText ?? ``;
1790
- if (this.#dragging) {
1791
- const formatted = this.step !== void 0 ? this._formatNumber(this.#dragCurrentValue) : this.#dragCurrentValue.toFixed(this.precision);
1792
- return this.formatter ? this.formatter(formatted) : formatted;
1780
+ /**
1781
+ * Force an immediate geometry recompute. Called automatically on resize,
1782
+ * DOM changes, sort/filter/search changes — only needed when layout is
1783
+ * changed by outside forces (e.g. an external font load).
1784
+ */
1785
+ refreshLayout() {
1786
+ this.#recomputeGeometry();
1787
+ }
1788
+ addItem(item, options) {
1789
+ let el;
1790
+ if (typeof item === `string`) {
1791
+ const li = document.createElement(`li`);
1792
+ this._setLabel(li, item);
1793
+ el = li;
1794
+ } else el = item;
1795
+ if (options?.notChecked) el.setAttribute(`not-checked`, ``);
1796
+ this.appendChild(el);
1797
+ this.#ensureIcon(el);
1798
+ if (this.sort !== `none`) this.#applySort();
1799
+ if (this.interactionMode === `checked`) this._syncCheckboxes();
1800
+ }
1801
+ removeItem(item) {
1802
+ item.remove();
1803
+ }
1804
+ clearItems() {
1805
+ this.replaceChildren();
1806
+ this.selection.reset();
1807
+ this.#builtFromProvider = [];
1808
+ this.#providerBuilt = 0;
1809
+ this._syncItemStates();
1810
+ }
1811
+ /**
1812
+ * Replace the entire item list in one call.
1813
+ * Equivalent to `clearItems()` followed by `addItem()` for each element.
1814
+ */
1815
+ setItems(items) {
1816
+ this.clearItems();
1817
+ for (const item of items) {
1818
+ this.appendChild(item);
1819
+ this.#ensureIcon(item);
1793
1820
  }
1794
- if (this.formatter) {
1795
- const raw = Number.parseFloat(this.value);
1796
- return this.formatter(Number.isNaN(raw) ? this.value : String(raw));
1821
+ if (this.sort !== `none`) this.#applySort();
1822
+ if (this.interactionMode === `checked`) this._syncCheckboxes();
1823
+ }
1824
+ /**
1825
+ * Get the source object behind an `<li>` built from an `items` array or
1826
+ * provider. Returns `undefined` for declarative / programmatic elements.
1827
+ */
1828
+ getItemData(item) {
1829
+ return this.#itemData.get(item);
1830
+ }
1831
+ /** Dispatch a command to this list. No-op by default (grouped-lister parity). */
1832
+ dispatch(_commandId, _args) {}
1833
+ select(item) {
1834
+ this.selection.replace(item);
1835
+ }
1836
+ selectMany(items) {
1837
+ this.selection.selectMany(items);
1838
+ }
1839
+ deselect(item) {
1840
+ this.selection.deselect(item);
1841
+ }
1842
+ clearSelection() {
1843
+ this.selection.clear();
1844
+ }
1845
+ selectAll() {
1846
+ if (this.selection.canSelectAll()) this.selection.selectAll();
1847
+ }
1848
+ *selectedFilter(kind) {
1849
+ const wanted = (el) => {
1850
+ switch (kind) {
1851
+ case `selected`: return el.hasAttribute(`data-selected`);
1852
+ case `checked`: return el.hasAttribute(`data-checked`);
1853
+ case `either`: return el.hasAttribute(`data-selected`) || el.hasAttribute(`data-checked`);
1854
+ }
1855
+ };
1856
+ for (const el of this.querySelectorAll(ITEM_SELECTOR)) {
1857
+ if (el.hidden) continue;
1858
+ if (wanted(el)) yield el;
1797
1859
  }
1798
- return this._formatDisplay();
1799
1860
  }
1800
- _tryCommit() {
1801
- const el = this.renderRoot?.querySelector(`.number`);
1802
- if (el) {
1803
- const n = Number.parseFloat(el.textContent?.trim() ?? ``);
1804
- if (!Number.isNaN(n)) el.textContent = this._formatNumber(this._clamp(this._snap(n)));
1861
+ #rebuildFromObjects(items) {
1862
+ const keyOf = (el) => el.getAttribute(`data-key`) ?? el.getAttribute(`data-search-label`) ?? null;
1863
+ const previousKeys = new Set([...this.selection.selected].map(keyOf).filter((k) => k !== null));
1864
+ const anchorKey = this.selection.anchor ? keyOf(this.selection.anchor) : null;
1865
+ this.replaceChildren(...items.map((item, index) => this.#createItemEl(item, index)));
1866
+ if (previousKeys.size > 0 || anchorKey !== null) {
1867
+ const next = [];
1868
+ let anchor;
1869
+ for (const el of this.querySelectorAll(ITEM_SELECTOR)) {
1870
+ const key = keyOf(el);
1871
+ if (key !== null && previousKeys.has(key)) next.push(el);
1872
+ if (key !== null && key === anchorKey) anchor = el;
1873
+ }
1874
+ this.selection.seed(next, anchor);
1805
1875
  }
1806
- super._tryCommit();
1807
- }
1808
- _handleArrowKey(e) {
1809
- const range = (this.max ?? 100) - (this.min ?? 0);
1810
- if (range === 0) return false;
1811
- const effectiveStep = resolveAxisStep(range, this.stops, this.step, e.shiftKey);
1812
- let delta = 0;
1813
- let handled = false;
1814
- switch (e.key) {
1815
- case `ArrowUp`:
1816
- case `ArrowRight`:
1817
- delta = effectiveStep;
1818
- handled = true;
1819
- break;
1820
- case `ArrowDown`:
1821
- case `ArrowLeft`:
1822
- delta = -effectiveStep;
1823
- handled = true;
1824
- break;
1825
- case `Home`:
1826
- delta = -(this._parseValue() - (this.min ?? 0));
1827
- handled = true;
1828
- break;
1829
- case `End`:
1830
- delta = (this.max ?? 0) - this._parseValue();
1831
- handled = true;
1876
+ if (this.sort !== `none`) this.#applySort();
1877
+ if (this.interactionMode === `checked`) this._syncCheckboxes();
1878
+ this._syncItemStates();
1879
+ this.#scheduleGeometry();
1880
+ }
1881
+ #createItemEl(item, index) {
1882
+ const li = document.createElement(`li`);
1883
+ this.#itemData.set(li, item);
1884
+ const content = this.formatter ? this.formatter(item, index) : this.displayProperty ? this.#propertyValue(item, this.displayProperty) : item;
1885
+ if (typeof content === `string`) {
1886
+ this._setLabel(li, content);
1887
+ if (this.iconProperty) {
1888
+ const icon = this.#propertyValue(item, this.iconProperty);
1889
+ if (typeof icon === `string` && icon.length > 0) li.setAttribute(`data-icon`, icon);
1890
+ }
1891
+ } else if (content instanceof Node) li.appendChild(content);
1892
+ if (this.keyProperty) {
1893
+ const key = this.#propertyValue(item, this.keyProperty);
1894
+ if (key !== void 0 && key !== null) li.setAttribute(`data-key`, String(key));
1832
1895
  }
1833
- if (handled) {
1834
- e.preventDefault();
1835
- this._applyDelta(delta);
1836
- return true;
1896
+ this.#ensureIcon(li);
1897
+ return li;
1898
+ }
1899
+ #propertyValue(item, prop) {
1900
+ if (typeof item !== `object` || item === null || !(prop in item)) return void 0;
1901
+ return item[prop];
1902
+ }
1903
+ #syncFromProvider() {
1904
+ if (this.provider === void 0) return;
1905
+ const items = this.#data.items;
1906
+ if (items === this.#builtFromProvider) return;
1907
+ let prefix = 0;
1908
+ while (prefix < this.#builtFromProvider.length && prefix < items.length && this.#builtFromProvider[prefix] === items[prefix]) prefix++;
1909
+ if (prefix === this.#builtFromProvider.length && prefix <= items.length) {
1910
+ const tail = items.slice(prefix);
1911
+ for (const item of tail) this.appendChild(this.#createItemEl(item, this.#providerBuilt++));
1912
+ if (this.sort !== `none`) this.#applySort();
1913
+ if (this.interactionMode === `checked`) this._syncCheckboxes();
1914
+ this._syncItemStates();
1915
+ this.#scheduleGeometry();
1916
+ } else {
1917
+ this.#providerBuilt = 0;
1918
+ this.#rebuildFromObjects(items);
1837
1919
  }
1838
- return false;
1920
+ this.#builtFromProvider = items;
1839
1921
  }
1840
- /**
1841
- * Apply a signed delta to the current value, clamp/snap/format, and fire
1842
- * `input`/`change`. Shared between arrow-key handler and the wheel handler.
1843
- */
1844
- _applyDelta(delta) {
1845
- const prev = this.value;
1846
- const current = this._parseValue();
1847
- const newValue = this._clamp(this._snap(current + delta));
1848
- const newValueStr = this.step !== void 0 ? this._formatNumber(newValue) : newValue.toFixed(this.precision);
1849
- this.value = newValueStr;
1850
- this._requestDisplayUpdate();
1851
- this._updateValueCss(this._normalizedValue(newValue));
1852
- if (newValueStr !== prev) this.dispatchEvent(new CustomEvent(`input`, {
1853
- detail: { value: newValueStr },
1854
- bubbles: true,
1855
- composed: true
1922
+ #applyFilterPredicate() {
1923
+ if (!this.filterPredicate) {
1924
+ for (const el of this.querySelectorAll(ITEM_SELECTOR)) el.removeAttribute(`hidden`);
1925
+ return;
1926
+ }
1927
+ for (const el of this.querySelectorAll(ITEM_SELECTOR)) if (this.filterPredicate(el)) el.removeAttribute(`hidden`);
1928
+ else el.setAttribute(`hidden`, ``);
1929
+ }
1930
+ #applySort() {
1931
+ if (this.sort === `none`) return;
1932
+ const rows = [...this.querySelectorAll(ITEM_SELECTOR)];
1933
+ if (rows.length < 2) return;
1934
+ const dir = this.sort === `descending` ? -1 : 1;
1935
+ const meta = (el) => ({
1936
+ element: el,
1937
+ data: this.getItemData(el),
1938
+ label: el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``
1939
+ });
1940
+ let cmp;
1941
+ if (this.sortComparator) {
1942
+ const user = this.sortComparator;
1943
+ cmp = (a, b) => user(meta(a), meta(b));
1944
+ } else if (this.sortBy) {
1945
+ const prop = this.sortBy;
1946
+ const key = (el) => {
1947
+ const v = this.#propertyValue(this.getItemData(el), prop);
1948
+ if (typeof v === `number`) return v;
1949
+ return v === void 0 || v === null ? `` : String(v);
1950
+ };
1951
+ cmp = (a, b) => {
1952
+ const ka = key(a);
1953
+ const kb = key(b);
1954
+ if (typeof ka === `number` && typeof kb === `number`) return ka - kb;
1955
+ return String(ka).localeCompare(String(kb), void 0, { numeric: true });
1956
+ };
1957
+ } else cmp = (a, b) => meta(a).label.localeCompare(meta(b).label, void 0, { numeric: true });
1958
+ const indexed = rows.map((el, i) => ({
1959
+ el,
1960
+ i
1856
1961
  }));
1962
+ indexed.sort((a, b) => cmp(a.el, b.el) * dir || a.i - b.i);
1963
+ for (const { el } of indexed) this.appendChild(el);
1964
+ this.#scheduleGeometry();
1965
+ }
1966
+ #scheduleGeometry() {
1967
+ if (this.#geometryScheduled) return;
1968
+ this.#geometryScheduled = true;
1857
1969
  requestAnimationFrame(() => {
1858
- if (this.value !== prev) this.dispatchEvent(new CustomEvent(`change`, {
1859
- detail: { value: this.value },
1860
- bubbles: true,
1861
- composed: true
1862
- }));
1970
+ this.#geometryScheduled = false;
1971
+ this.#recomputeGeometry();
1972
+ });
1973
+ }
1974
+ #recomputeGeometry() {
1975
+ this.#grid = [];
1976
+ this.#positions.clear();
1977
+ if (!this.isConnected) return;
1978
+ const columns = [];
1979
+ for (const el of this._getVisibleItems()) {
1980
+ const rect = el.getBoundingClientRect();
1981
+ if (rect.width === 0 && rect.height === 0) continue;
1982
+ let ci = columns.findIndex((c) => Math.abs(c.left - rect.left) <= 1);
1983
+ if (ci === -1) {
1984
+ columns.push({
1985
+ left: rect.left,
1986
+ items: []
1987
+ });
1988
+ ci = columns.length - 1;
1989
+ }
1990
+ columns[ci].items.push(el);
1991
+ }
1992
+ columns.sort((a, b) => a.left - b.left);
1993
+ for (const col of columns) col.items.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
1994
+ this.#grid = columns.map((c) => c.items);
1995
+ this.#grid.forEach((col, colIdx) => col.forEach((el, rowIdx) => this.#positions.set(el, {
1996
+ col: colIdx,
1997
+ row: rowIdx
1998
+ })));
1999
+ }
2000
+ #positionOf(el) {
2001
+ let pos = this.#positions.get(el);
2002
+ if (pos === void 0) {
2003
+ this.#recomputeGeometry();
2004
+ pos = this.#positions.get(el);
2005
+ }
2006
+ return pos;
2007
+ }
2008
+ #itemAt(col, row) {
2009
+ return this.#grid[col]?.[row];
2010
+ }
2011
+ #gridSize() {
2012
+ if (this.#grid.length === 0) this.#recomputeGeometry();
2013
+ return {
2014
+ cols: this.#grid.length,
2015
+ rows: Math.max(0, ...this.#grid.map((c) => c.length))
2016
+ };
2017
+ }
2018
+ #navigateTo(target) {
2019
+ if (!target) return;
2020
+ this.tickled.handleKeyboardNavigation(target);
2021
+ this.tickled.tickledId?.scrollIntoView({
2022
+ block: `nearest`,
2023
+ inline: `nearest`
1863
2024
  });
1864
2025
  }
1865
- _shouldHandleWheel() {
1866
- if (this._editing) return false;
1867
- return super._shouldHandleWheel();
2026
+ #findNextCheckable(fromCol, fromRow, deltaCol, deltaRow) {
2027
+ const { cols } = this.#gridSize();
2028
+ const maxIterations = this._getVisibleItems().length + 1;
2029
+ let col = deltaCol !== 0 ? fromCol + deltaCol : fromCol;
2030
+ let row = deltaRow !== 0 ? fromRow + deltaRow : fromRow;
2031
+ let wrappedColumns = false;
2032
+ for (let iterations = 0; iterations < maxIterations; iterations++) {
2033
+ const colLength = this.#grid[col]?.length ?? 0;
2034
+ if (row < 0 || row >= colLength) {
2035
+ if (deltaCol !== 0 || wrappedColumns) {
2036
+ if (this.itemWrap === `wrap`) {
2037
+ if (deltaCol < 0) col = cols - 1;
2038
+ else if (deltaCol > 0) col = 0;
2039
+ wrappedColumns = true;
2040
+ } else return;
2041
+ }
2042
+ if (deltaRow > 0) {
2043
+ if (col + 1 < cols) {
2044
+ col = col + 1;
2045
+ row = 0;
2046
+ wrappedColumns = true;
2047
+ } else if (this.itemWrap === `wrap`) {
2048
+ col = 0;
2049
+ row = 0;
2050
+ wrappedColumns = true;
2051
+ } else return;
2052
+ } else if (deltaRow < 0) {
2053
+ if (col - 1 >= 0) {
2054
+ col = col - 1;
2055
+ row = (this.#grid[col]?.length ?? 1) - 1;
2056
+ wrappedColumns = true;
2057
+ } else if (this.itemWrap === `wrap`) {
2058
+ col = cols - 1;
2059
+ row = (this.#grid[col]?.length ?? 1) - 1;
2060
+ wrappedColumns = true;
2061
+ } else return;
2062
+ } else if (deltaCol !== 0) {
2063
+ const targetColLength = this.#grid[col]?.length ?? 0;
2064
+ if (row >= targetColLength && targetColLength > 0) row = targetColLength - 1;
2065
+ }
2066
+ if (col === fromCol && row === fromRow + deltaRow) return;
2067
+ continue;
2068
+ }
2069
+ const item = this.#itemAt(col, row);
2070
+ if (item && this._isCheckable(item)) return item;
2071
+ row += deltaRow;
2072
+ }
2073
+ }
2074
+ #findFirstCheckable() {
2075
+ return this._getVisibleItems().find((item) => this._isCheckable(item));
2076
+ }
2077
+ #findLastCheckable() {
2078
+ const visibleItems = this._getVisibleItems();
2079
+ for (let i = visibleItems.length - 1; i >= 0; i--) if (this._isCheckable(visibleItems[i])) return visibleItems[i];
2080
+ }
2081
+ #navigateDown() {
2082
+ this.#recomputeGeometry();
2083
+ const cur = this.tickled.tickledId ? this.#positionOf(this.tickled.tickledId) : void 0;
2084
+ if (!cur) {
2085
+ this.#navigateTo(this.#findFirstCheckable());
2086
+ return;
2087
+ }
2088
+ const next = this.#findNextCheckable(cur.col, cur.row, 0, 1);
2089
+ if (next) this.#navigateTo(next);
2090
+ }
2091
+ #navigateUp() {
2092
+ this.#recomputeGeometry();
2093
+ const cur = this.tickled.tickledId ? this.#positionOf(this.tickled.tickledId) : void 0;
2094
+ if (!cur) {
2095
+ this.#navigateTo(this.#findLastCheckable());
2096
+ return;
2097
+ }
2098
+ const next = this.#findNextCheckable(cur.col, cur.row, 0, -1);
2099
+ if (next) this.#navigateTo(next);
2100
+ }
2101
+ #navigateRight() {
2102
+ this.#recomputeGeometry();
2103
+ const cur = this.tickled.tickledId ? this.#positionOf(this.tickled.tickledId) : void 0;
2104
+ if (!cur) {
2105
+ this.#navigateTo(this.#findFirstCheckable());
2106
+ return;
2107
+ }
2108
+ const next = this.#findNextCheckable(cur.col, cur.row, 1, 0);
2109
+ if (next) this.#navigateTo(next);
2110
+ else if (this.itemWrap === `wrap`) this.#navigateTo(this.#findFirstCheckable());
2111
+ }
2112
+ #navigateLeft() {
2113
+ this.#recomputeGeometry();
2114
+ const cur = this.tickled.tickledId ? this.#positionOf(this.tickled.tickledId) : void 0;
2115
+ if (!cur) {
2116
+ this.#navigateTo(this.#findFirstCheckable());
2117
+ return;
2118
+ }
2119
+ const next = this.#findNextCheckable(cur.col, cur.row, -1, 0);
2120
+ if (next) this.#navigateTo(next);
2121
+ else if (this.itemWrap === `wrap`) this.#navigateTo(this.#findLastCheckable());
2122
+ }
2123
+ _getVisibleItems() {
2124
+ return Array.from(this.querySelectorAll(ITEM_SELECTOR)).filter((el) => {
2125
+ if (el.hidden) return false;
2126
+ if (this.filterPredicate && !this.filterPredicate(el)) return false;
2127
+ return true;
2128
+ });
1868
2129
  }
1869
- _applyWheelDelta(direction, event) {
1870
- const range = (this.max ?? 100) - (this.min ?? 0);
1871
- if (range === 0) return;
1872
- const effectiveStep = resolveAxisStep(range, this.stops, this.step, event.shiftKey);
1873
- this._applyDelta(direction * effectiveStep);
2130
+ _isCheckable(item) {
2131
+ if (this.interactionMode !== `checked`) return true;
2132
+ return !item.hasAttribute(`not-checked`);
1874
2133
  }
1875
- _parseValue() {
1876
- return Number.parseFloat(this.value) || 0;
2134
+ _setLabel(li, label) {
2135
+ const span = document.createElement(`span`);
2136
+ span.style.cssText = LABEL_SPAN_CSS;
2137
+ span.textContent = label;
2138
+ li.appendChild(span);
2139
+ li.setAttribute(`data-search-label`, label);
1877
2140
  }
1878
2141
  /**
1879
- * Formats the stored value for display, inferring decimal places from the value
1880
- * string itself. This preserves typed precision (e.g. "0.1234" stays "0.1234")
1881
- * and also respects trailing zeros written by drag storage (e.g. "5.0" stays "5.0").
2142
+ * Inject a leading `<ixfx-icon>` into an item carrying `data-icon`.
2143
+ * Idempotent skips when the item already starts with an icon.
1882
2144
  */
1883
- _formatDisplay() {
1884
- const n = Number.parseFloat(this.value);
1885
- if (Number.isNaN(n)) return this.value;
1886
- if (this.precision !== void 0) return n.toFixed(this.precision);
1887
- const dot = this.value.indexOf(`.`);
1888
- const dp = dot >= 0 ? this.value.length - dot - 1 : 0;
1889
- return n.toFixed(dp);
1890
- }
1891
- _clamp(n) {
1892
- if (this.min !== void 0) n = Math.max(this.min, n);
1893
- if (this.max !== void 0) n = Math.min(this.max, n);
1894
- return n;
1895
- }
1896
- _snap(n) {
1897
- if (this.stops && this.stops > 1) {
1898
- const min = this.min ?? 0;
1899
- const stopStep = ((this.max ?? 100) - min) / this.stops;
1900
- return Math.round((n - min) / stopStep) * stopStep + min;
2145
+ #ensureIcon(li) {
2146
+ const name = li.getAttribute(`data-icon`);
2147
+ if (!name) return;
2148
+ if (li.firstElementChild?.tagName === `IXFX-ICON`) return;
2149
+ const icon = document.createElement(`ixfx-icon`);
2150
+ icon.setAttribute(`name`, name);
2151
+ icon.style.marginInlineEnd = `var(--detail-list-icon-gap, 6px)`;
2152
+ icon.style.flexShrink = `0`;
2153
+ li.prepend(icon);
2154
+ }
2155
+ /** Scan light-DOM children for unprocessed `data-icon` items. */
2156
+ #processChildren() {
2157
+ for (const el of this.querySelectorAll(ITEM_SELECTOR)) this.#ensureIcon(el);
2158
+ }
2159
+ _syncItemStates() {
2160
+ for (const el of this.querySelectorAll(ITEM_SELECTOR)) {
2161
+ el.toggleAttribute(`data-tickled`, this.tickled.isTickled(el));
2162
+ el.toggleAttribute(`data-selected`, this.selection.selected.has(el));
2163
+ if (this.interactionMode === `checked`) el.toggleAttribute(`data-checked`, !el.hasAttribute(`not-checked`) && this.selection.selected.has(el));
2164
+ else el.removeAttribute(`data-checked`);
2165
+ }
2166
+ this.dataset.inputMode = this.tickled.inputMode;
2167
+ this._syncCheckboxes();
2168
+ }
2169
+ _syncCheckboxes() {
2170
+ const inCheckedMode = this.interactionMode === `checked`;
2171
+ const wantsCustom = typeof this.checkboxRenderer === `function`;
2172
+ for (const el of this.querySelectorAll(`li`)) {
2173
+ const existing = el.querySelector(`.ixfx-list-checkbox`);
2174
+ if (!inCheckedMode) {
2175
+ if (existing) existing.remove();
2176
+ continue;
2177
+ }
2178
+ if (el.hasAttribute(`not-checked`)) {
2179
+ if (existing) existing.remove();
2180
+ continue;
2181
+ }
2182
+ const existingIsCustom = existing?.classList.contains(`ixfx-list-checkbox-custom`) ?? false;
2183
+ if (existing && existingIsCustom !== wantsCustom) existing.remove();
2184
+ let cb = el.querySelector(`.ixfx-list-checkbox`);
2185
+ if (!cb) {
2186
+ if (wantsCustom) {
2187
+ const opts = this.checkboxRenderer;
2188
+ const margin = opts.margin ?? `0`;
2189
+ const width = opts.width ?? `24px`;
2190
+ cb = this._createCustomCheckbox(el, margin, width);
2191
+ } else cb = this._createNativeCheckboxCell(el);
2192
+ el.prepend(cb);
2193
+ }
2194
+ const checked = this.selection.selected.has(el);
2195
+ if (wantsCustom) {
2196
+ cb.setAttribute(`aria-checked`, String(checked));
2197
+ cb.toggleAttribute(`data-checked`, checked);
2198
+ if (this.checkboxRenderer) {
2199
+ const renderer = typeof this.checkboxRenderer === `function` ? this.checkboxRenderer : this.checkboxRenderer.renderer;
2200
+ render(renderer(checked), cb);
2201
+ }
2202
+ } else {
2203
+ const input = cb.querySelector(`input.ixfx-list-checkbox`);
2204
+ if (input) input.checked = checked;
2205
+ }
1901
2206
  }
1902
- if (!this.step) return n;
1903
- return Math.round(n / this.step) * this.step;
1904
- }
1905
- _inferDecimals(n) {
1906
- if (this.step !== void 0) {
1907
- const s = String(this.step);
1908
- const dot = s.indexOf(`.`);
1909
- return dot >= 0 ? s.length - dot - 1 : 0;
2207
+ }
2208
+ /**
2209
+ * Native checkbox cell: a span that stretches to the row's full cross-axis
2210
+ * (so the entire column height is a click target) with a real `<input>`
2211
+ * centred inside. The span owns the click; the input is purely visual and
2212
+ * has `pointer-events:none` so a single click produces one toggle.
2213
+ */
2214
+ _createNativeCheckboxCell(el) {
2215
+ const cell = document.createElement(`span`);
2216
+ cell.className = `ixfx-list-checkbox ixfx-list-checkbox-native`;
2217
+ cell.style.cssText = `display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;align-self:stretch;width:18px;margin:0 6px 0 0;cursor:pointer;pointer-events:auto`;
2218
+ cell.tabIndex = -1;
2219
+ cell.setAttribute(`role`, `checkbox`);
2220
+ cell.setAttribute(`part`, `checkbox`);
2221
+ cell.addEventListener(`click`, (e) => {
2222
+ e.stopPropagation();
2223
+ this.selection.toggle(el);
2224
+ });
2225
+ const input = document.createElement(`input`);
2226
+ input.type = `checkbox`;
2227
+ input.className = `ixfx-list-checkbox`;
2228
+ input.style.cssText = `pointer-events:none;cursor:pointer;margin:0`;
2229
+ cell.appendChild(input);
2230
+ return cell;
2231
+ }
2232
+ /** Custom checkbox cell: same shape as the native cell; renderer content inside. */
2233
+ _createCustomCheckbox(el, margin, width) {
2234
+ const span = document.createElement(`span`);
2235
+ span.className = `ixfx-list-checkbox ixfx-list-checkbox-custom`;
2236
+ span.style.cssText = `display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;align-self:stretch;width:${width};margin:${margin};cursor:pointer;pointer-events:auto`;
2237
+ span.tabIndex = -1;
2238
+ span.setAttribute(`role`, `checkbox`);
2239
+ span.setAttribute(`part`, `checkbox`);
2240
+ span.addEventListener(`click`, (e) => {
2241
+ e.stopPropagation();
2242
+ this.selection.toggle(el);
2243
+ });
2244
+ return span;
2245
+ }
2246
+ _findListItem(path) {
2247
+ for (const target of path) {
2248
+ if (!(target instanceof Element)) continue;
2249
+ if (target === this) break;
2250
+ if (target.parentElement === this && target.matches(`li`)) return target;
1910
2251
  }
1911
- const s = String(n ?? this._parseValue());
1912
- const dot = s.indexOf(`.`);
1913
- return dot >= 0 ? s.length - dot - 1 : 0;
1914
2252
  }
1915
- _formatNumber(n) {
1916
- return n.toFixed(this._inferDecimals(n));
2253
+ _dispatch(name, detail) {
2254
+ this.dispatchEvent(new CustomEvent(name, {
2255
+ detail,
2256
+ bubbles: true,
2257
+ composed: true
2258
+ }));
2259
+ }
2260
+ _openSearch() {
2261
+ this._searchVisible = true;
2262
+ this.updateComplete.then(() => this._searchInputEl?.focus());
2263
+ }
2264
+ _closeSearch() {
2265
+ this._searchVisible = false;
2266
+ this._searchQuery = ``;
2267
+ this.#search?.clearQuery().then(() => this.#recomputeGeometry());
2268
+ this.focus();
1917
2269
  }
1918
- /** Returns a 0..1 fraction for the given value within [min, max]. */
1919
- _normalizedValue(n = this._parseValue()) {
1920
- if (this.min === void 0 || this.max === void 0) return 0;
1921
- if (this.max === this.min) return 0;
1922
- return Math.max(0, Math.min(1, (n - this.min) / (this.max - this.min)));
2270
+ _activateItem(item) {
2271
+ if (this.interactionMode !== `manual`) this.selection.replace(item);
2272
+ this._dispatch(`list-activate`, { item });
1923
2273
  }
1924
2274
  static {
1925
2275
  this.styles = [
1926
2276
  themeFallbacks,
1927
- prominenceStyles,
1928
- ...EditableLabel.styles,
2277
+ scrollFadeStyles(`x`),
1929
2278
  css`
1930
2279
  :host {
1931
- font-size: var(--text-s,11px);
1932
- --value: 0;
1933
- --value-start: 0;
1934
- --value-end: 0;
1935
- --_fill-color-alpha: color-mix(
1936
- in oklch,
1937
- var(--fill-color, var(--accent, oklch(60% 0.15 140))) calc(var(--fill-opacity, 0.25) * 100%),
1938
- transparent
1939
- );
2280
+ display: block;
2281
+ position: relative;
2282
+ background: var(--detail-list-bg, var(--surface-3));
2283
+ color: var(--detail-list-text, var(--surface-3-text));
2284
+ border-radius: var(--detail-list-radius, var(--radius-s));
2285
+ border: 1px solid var(--detail-list-border, var(--border));
2286
+ width: 100%;
2287
+ height: 100%;
2288
+ overflow: hidden;
2289
+ box-sizing: border-box;
2290
+ outline: none;
2291
+ --ixfx-detail-list-item-padding:
2292
+ var(--ixfx-detail-list-item-padding, var(--detail-list-item-padding, 0 var(--space-m, 8px)));
1940
2293
  }
1941
2294
 
1942
- /* ── Layout (when fill-style is enabled) ── */
1943
- :host([fill-style]) {
1944
- display: grid;
2295
+ :host(:focus-visible) {
2296
+ outline: 2px solid var(--accent);
2297
+ outline-offset: 2px;
2298
+ }
2299
+
2300
+ .container {
1945
2301
  position: relative;
1946
- grid-template-columns: 1fr;
1947
- grid-template-rows: 1fr;
1948
- min-height: 1.5em;
1949
- min-width: 5em;
2302
+ width: 100%;
2303
+ height: 100%;
1950
2304
  }
1951
2305
 
1952
- :host([fill-style]) .track,
1953
- :host([fill-style]) .value {
1954
- grid-area: 1 / 1;
1955
- position: absolute;
1956
- inset: 0;
2306
+ ul {
2307
+ list-style: none;
2308
+ margin: 0;
2309
+ padding: 0;
2310
+ /* keep the last column clear of the scrollbar / right edge */
2311
+ padding-inline-end: calc(var(--detail-list-column-gap, 8px) + 4px);
2312
+ width: 100%;
2313
+ height: 100%;
2314
+ display: flex;
2315
+ flex-flow: column wrap;
2316
+ align-content: flex-start;
2317
+ column-gap: var(--detail-list-column-gap, 8px);
2318
+ overflow-x: auto;
2319
+ overflow-y: hidden;
2320
+ box-sizing: border-box;
2321
+ /* Scrollbar theming: consumers override these CSS variables to control
2322
+ the native scrollbar (scrollbar-width, scrollbar-gutter, scrollbar-color). */
2323
+ scrollbar-width: var(--detail-list-scrollbar-width, auto);
2324
+ scrollbar-gutter: var(--detail-list-scrollbar-gutter, auto);
2325
+ scrollbar-color: var(--detail-list-scrollbar-color, auto);
1957
2326
  }
1958
2327
 
1959
- /* Track background styling */
1960
- :host([fill-style]) .track {
1961
- z-index: 0;
1962
- background: var(--surface-4, oklch(22% 0.02 240));
1963
- border: var(--border, 1px solid oklch(25% 0.02 240));
2328
+ ::slotted([hidden]) {
2329
+ display: none !important;
1964
2330
  }
1965
2331
 
1966
- :host([fill-style]) .value {
1967
- z-index: 1;
2332
+ ::slotted(li) {
2333
+ box-sizing: border-box;
2334
+ display: flex;
2335
+ align-items: center;
2336
+ flex: none;
2337
+ width: var(--detail-list-column-width, 220px);
2338
+ height: var(--detail-list-item-height, 28px);
2339
+ padding: var(--ixfx-detail-list-item-padding);
2340
+ cursor: pointer;
2341
+ user-select: none;
2342
+ transition: background var(--transition, 0.1s ease-out),
2343
+ outline-color var(--transition, 0.1s ease-out);
2344
+ outline: 1px solid transparent;
1968
2345
  }
1969
2346
 
1970
- :host([fill-style="linear"]) .track {
1971
- border-radius: var(--radius-s, 3px);
2347
+ /* In checked mode, only the checkbox column is clickable; the row body
2348
+ gets a default cursor so it doesn't suggest otherwise. */
2349
+ :host([interaction-mode="checked"]) ::slotted(li) {
2350
+ cursor: default;
1972
2351
  }
1973
2352
 
1974
- :host([fill-style="circular"]) .track {
1975
- border-radius: 50%;
1976
- overflow: hidden;
2353
+ ::slotted([data-tickled]) {
2354
+ background: var(--item-bg-tickled);
2355
+ outline: var(--item-border-tickled);
1977
2356
  }
1978
2357
 
1979
- /* Linear: all linear-styles use .bar positioned via --value-start and --value-end */
1980
- :host([fill-style="linear"]) .value {
1981
- overflow: hidden;
2358
+ ::slotted([data-selected]) {
2359
+ background: var(--item-bg-selected);
2360
+ color: var(--item-text-selected);
2361
+ outline: 1px solid transparent;
1982
2362
  }
1983
2363
 
1984
- :host([fill-style="linear"]) .bar {
1985
- position: absolute;
1986
- top: 0;
1987
- bottom: 0;
1988
- left: calc(var(--value-start, 0) * 100%);
1989
- width: calc((var(--value-end, 0) - var(--value-start, 0)) * 100%);
1990
- background: var(--fill-image, var(--_fill-color-alpha));
1991
- }
1992
-
1993
- /* Circular grow: filled circle growing from center */
1994
- :host([fill-style="circular"][circular-style="grow"]) .value,
1995
- :host([fill-style="circular"]:not([circular-style])) .value {
1996
- background: var(--fill-image, var(--_fill-color-alpha));
1997
- clip-path: circle(calc(var(--value) * 50%) at 50% 50%);
1998
- }
1999
-
2000
- /* Circular offset: value circle slides left/right from center.
2001
- Track circle is centered, filling the height.
2002
- Value circle is same size, offset by --value-offset-px.
2003
- At value=0.5: value circle centered on track (invisible when overlaid)
2004
- At value=0: value circle at far left
2005
- At value=1: value circle at far right
2006
- */
2007
- :host([fill-style="circular"][circular-style="offset"]) {
2008
- min-width: 80px;
2009
- min-height: 26px;
2010
- overflow: hidden;
2364
+ ::slotted([data-selected][data-tickled]) {
2365
+ background: var(--item-bg-selected-tickled);
2366
+ outline: 2px dashed var(--surface-3);
2367
+ outline-offset: -2px;
2011
2368
  }
2012
2369
 
2013
- :host([fill-style="circular"][circular-style="offset"]) .track {
2014
- position: absolute;
2015
- top: 50%;
2016
- border-radius: 50%;
2017
- background: var(--surface-4, oklch(22% 0.02 240));
2018
- border: var(--border, 1px solid oklch(25% 0.02 240));
2019
- width: calc(var(--radius, 11px) * 2);
2020
- height: calc(var(--radius, 11px) * 2);
2021
- left: 50%;
2022
- transform: translate(-50%, -50%);
2370
+ /* checked mode: the injected checkbox communicates selection — suppress bg */
2371
+ :host([interaction-mode="checked"]) ::slotted([data-selected]) {
2372
+ background: transparent;
2373
+ color: inherit;
2374
+ outline: 1px solid transparent;
2023
2375
  }
2024
2376
 
2025
- :host([fill-style="circular"][circular-style="offset"]) .value {
2026
- position: absolute;
2027
- top: 50%;
2028
- background: var(--fill-image, var(--_fill-color-alpha));
2029
- border-radius: 50%;
2030
- width: calc(var(--radius, 11px) * 2);
2031
- height: calc(var(--radius, 11px) * 2);
2032
- transform: translate(calc(var(--value-center-px, 13px) - var(--radius, 11px)), -50%);
2377
+ :host([interaction-mode="checked"]) ::slotted([data-selected][data-tickled]) {
2378
+ background: var(--item-bg-tickled);
2379
+ color: inherit;
2380
+ outline: var(--item-border-tickled);
2033
2381
  }
2034
2382
 
2035
- :host([fill-style="circular"][circular-style="offset"]) .number {
2036
- border-radius: 0;
2383
+ .loading {
2384
+ position: absolute;
2385
+ bottom: 8px;
2386
+ right: 8px;
2387
+ z-index: 10;
2388
+ background: var(--surface-4, #2a2a2a);
2389
+ border: 1px solid var(--border, #444);
2390
+ border-radius: var(--radius-m, 4px);
2391
+ padding: 2px 8px;
2392
+ font-size: var(--text-s, 11px);
2393
+ color: var(--surface-muted-text, #888);
2037
2394
  }
2038
2395
 
2039
- /* Circular dot: full circle */
2040
- :host([fill-style="circular"][circular-style="dot"]) .value {
2041
- background: var(--fill-image, var(--_fill-color-alpha));
2396
+ .loading[hidden] {
2397
+ display: none;
2042
2398
  }
2043
2399
 
2044
- /* ── Number (top layer - text display) ── */
2045
- :host([fill-style]) .number {
2400
+ .search-overlay {
2046
2401
  position: absolute;
2047
- inset: 0;
2048
- z-index: 2;
2402
+ top: 8px;
2403
+ right: 8px;
2404
+ z-index: 10;
2049
2405
  display: flex;
2050
2406
  align-items: center;
2051
- justify-content: center;
2052
- font-family: inherit;
2053
- font-size: inherit;
2054
- color: inherit;
2055
- padding: var(--space-xs, 3px) var(--space-s, 4px);
2056
- }
2057
-
2058
- :host([fill-style="circular"]) .number {
2059
- border-radius: 50%;
2407
+ gap: 4px;
2408
+ background: var(--surface-4, #2a2a2a);
2409
+ border: 1px solid var(--border, #444);
2410
+ border-radius: var(--radius-m, 4px);
2411
+ padding: 4px 6px;
2412
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
2060
2413
  }
2061
2414
 
2062
- /* Drag cursor + no text selection while not in text-edit mode */
2063
- :host(:not([disabled]):not([no-drag])) .number:not([contenteditable="plaintext-only"]) {
2064
- cursor: ns-resize;
2065
- user-select: none;
2066
- -webkit-user-select: none;
2067
- touch-action: none;
2415
+ .search-overlay[hidden] {
2416
+ display: none;
2068
2417
  }
2069
2418
 
2070
- /* Focus outline */
2071
- :host .number {
2419
+ .search-input {
2420
+ background: transparent;
2421
+ border: none;
2072
2422
  outline: none;
2423
+ color: inherit;
2424
+ font-size: var(--text-m, 0.875em);
2425
+ min-width: 140px;
2073
2426
  }
2074
2427
 
2075
- :host .number:focus-visible {
2076
- outline: 2px solid var(--accent);
2077
- outline-offset: 2px;
2428
+ .search-close {
2429
+ background: transparent;
2430
+ border: none;
2431
+ color: var(--surface-muted-text, #888);
2432
+ cursor: pointer;
2433
+ font-size: 1.1em;
2434
+ line-height: 1;
2435
+ padding: 0 2px;
2436
+ }
2437
+
2438
+ .search-close:hover {
2439
+ color: inherit;
2078
2440
  }
2441
+ `
2442
+ ];
2443
+ }
2444
+ render() {
2445
+ const loading = this.#data.loading;
2446
+ const progress = this.#data.progress;
2447
+ return html`
2448
+ <div class="container">
2449
+ <ul part="list" class="scroll-fade-x">
2450
+ <slot></slot>
2451
+ </ul>
2452
+ <div class="loading" part="loading" ?hidden=${!loading}>
2453
+ ${loading ? progress !== void 0 ? `${Math.round(progress * 100)}%` : `loading…` : nothing}
2454
+ </div>
2455
+ <div
2456
+ class="search-overlay"
2457
+ part="search-overlay"
2458
+ ?hidden=${!this._searchVisible}
2459
+ >
2460
+ <input
2461
+ type="text"
2462
+ class="search-input"
2463
+ placeholder="Search…"
2464
+ .value=${this._searchQuery}
2465
+ @input=${this._onSearchInput}
2466
+ @keydown=${this._onSearchKeydown}
2467
+ />
2468
+ <button class="search-close" @click=${this._closeSearch} title="Close search (Esc)">×</button>
2469
+ </div>
2470
+ </div>
2471
+ `;
2472
+ }
2473
+ };
2474
+ __decorate([property({
2475
+ type: String,
2476
+ attribute: `selection-mode`
2477
+ })], DetailListElement.prototype, "selectionMode", void 0);
2478
+ __decorate([property({
2479
+ type: String,
2480
+ attribute: `interaction-mode`
2481
+ })], DetailListElement.prototype, "interactionMode", void 0);
2482
+ __decorate([property({ type: String })], DetailListElement.prototype, "sort", void 0);
2483
+ __decorate([property({
2484
+ type: String,
2485
+ attribute: `sort-by`
2486
+ })], DetailListElement.prototype, "sortBy", void 0);
2487
+ __decorate([property({ attribute: false })], DetailListElement.prototype, "sortComparator", void 0);
2488
+ __decorate([property({ attribute: false })], DetailListElement.prototype, "items", void 0);
2489
+ __decorate([property({ attribute: false })], DetailListElement.prototype, "provider", void 0);
2490
+ __decorate([property({
2491
+ type: String,
2492
+ attribute: `display-property`
2493
+ })], DetailListElement.prototype, "displayProperty", void 0);
2494
+ __decorate([property({ attribute: false })], DetailListElement.prototype, "formatter", void 0);
2495
+ __decorate([property({
2496
+ type: String,
2497
+ attribute: `icon-property`
2498
+ })], DetailListElement.prototype, "iconProperty", void 0);
2499
+ __decorate([property({
2500
+ type: String,
2501
+ attribute: `key-property`
2502
+ })], DetailListElement.prototype, "keyProperty", void 0);
2503
+ __decorate([property({ attribute: false })], DetailListElement.prototype, "filterPredicate", void 0);
2504
+ __decorate([property({
2505
+ type: String,
2506
+ attribute: `item-wrap`
2507
+ })], DetailListElement.prototype, "itemWrap", void 0);
2508
+ __decorate([property({ attribute: false })], DetailListElement.prototype, "checkboxRenderer", void 0);
2509
+ __decorate([state()], DetailListElement.prototype, "_searchVisible", void 0);
2510
+ __decorate([state()], DetailListElement.prototype, "_searchQuery", void 0);
2511
+ __decorate([query(`.search-input`)], DetailListElement.prototype, "_searchInputEl", void 0);
2512
+ __decorate([query(`ul`)], DetailListElement.prototype, "_listEl", void 0);
2513
+ DetailListElement = __decorate([safeCustomElement(`ixfx-detail-list`)], DetailListElement);
2514
+ //#endregion
2515
+ //#region src/dock/dock-drag.ts
2516
+ let activeDrag = null;
2517
+ const dragHosts = /* @__PURE__ */ new Set();
2518
+ function registerDockHost(host) {
2519
+ dragHosts.add(host);
2520
+ }
2521
+ function unregisterDockHost(host) {
2522
+ dragHosts.delete(host);
2523
+ if (activeDrag !== null && activeDrag.sourceHost === host);
2524
+ }
2525
+ /**
2526
+ * Begin a dock-panel drag. Called by hosts in response to their own
2527
+ * `dock-panel-drag-start` events (titlebar or tab-item handles).
2528
+ */
2529
+ function startDrag(panelId, sourceHost, pointerId, clientX, clientY) {
2530
+ if (activeDrag !== null);
2531
+ activeDrag = {
2532
+ panelId,
2533
+ sourceHost,
2534
+ pointerId,
2535
+ ghost: createGhostForPanel(sourceHost, panelId),
2536
+ outside: false
2537
+ };
2538
+ for (const host of dragHosts) host.setDragActive(true);
2539
+ activeDrag.ghost?.move(clientX, clientY);
2540
+ window.addEventListener(`pointermove`, onPointerMove);
2541
+ window.addEventListener(`pointerup`, onPointerUp);
2542
+ window.addEventListener(`pointercancel`, onPointerCancel);
2543
+ window.addEventListener(`keydown`, onKeyDown);
2544
+ }
2545
+ function createGhostForPanel(host, panelId) {
2546
+ const panel = Array.from(host.children).find((el) => el instanceof HTMLElement && el.panelId === panelId);
2547
+ if (!panel) return null;
2548
+ const content = document.createElement(`span`);
2549
+ content.style.display = `inline-flex`;
2550
+ content.style.alignItems = `center`;
2551
+ content.style.gap = `var(--space-xs, 4px)`;
2552
+ content.style.minWidth = `0`;
2553
+ if (panel.iconName) {
2554
+ const icon = document.createElement(`ixfx-icon`);
2555
+ icon.name = panel.iconName;
2556
+ content.appendChild(icon);
2557
+ }
2558
+ const label = document.createElement(`span`);
2559
+ label.textContent = panel.title || panelId;
2560
+ label.style.overflow = `hidden`;
2561
+ label.style.textOverflow = `ellipsis`;
2562
+ label.style.whiteSpace = `nowrap`;
2563
+ content.appendChild(label);
2564
+ try {
2565
+ return createDragGhost({
2566
+ anchor: panel,
2567
+ content
2568
+ });
2569
+ } catch {
2570
+ return null;
2571
+ }
2572
+ }
2573
+ function onPointerMove(event) {
2574
+ const drag = activeDrag;
2575
+ if (drag === null || event.pointerId !== drag.pointerId) return;
2576
+ drag.ghost?.move(event.clientX, event.clientY);
2577
+ const zone = findDropZone(drag, event.clientX, event.clientY);
2578
+ for (const host of dragHosts) host.setDropZone(zone !== null && zone.host === host ? zone : null);
2579
+ const outside = !pointerOverAnyHost(event.clientX, event.clientY);
2580
+ if (outside !== drag.outside) {
2581
+ drag.outside = outside;
2582
+ dispatchBoundary(drag, outside ? `dock-panel-drag-out` : `dock-panel-drag-in`, event);
2583
+ }
2584
+ }
2585
+ function onPointerUp(event) {
2586
+ const drag = activeDrag;
2587
+ if (drag === null || event.pointerId !== drag.pointerId) return;
2588
+ const zone = findDropZone(drag, event.clientX, event.clientY);
2589
+ if (zone !== null) zone.host.handleDrop(drag.panelId, zone, drag.sourceHost);
2590
+ else if (!pointerOverAnyHost(event.clientX, event.clientY)) dispatchTearOut(drag, event);
2591
+ endDrag();
2592
+ }
2593
+ function onPointerCancel(event) {
2594
+ const drag = activeDrag;
2595
+ if (drag === null || event.pointerId !== drag.pointerId) return;
2596
+ endDrag();
2597
+ }
2598
+ function onKeyDown(event) {
2599
+ if (activeDrag === null) return;
2600
+ if (event.key !== `Escape`) return;
2601
+ endDrag();
2602
+ }
2603
+ function findDropZone(drag, clientX, clientY) {
2604
+ for (const host of dragHosts) {
2605
+ const zone = host.computeDropZone(clientX, clientY, drag.panelId);
2606
+ if (zone !== null) return zone;
2607
+ }
2608
+ return null;
2609
+ }
2610
+ /** True when the point lies within the bounding box of any registered host. */
2611
+ function pointerOverAnyHost(clientX, clientY) {
2612
+ for (const host of dragHosts) {
2613
+ const r = host.getBoundingClientRect();
2614
+ if (clientX >= r.left && clientX <= r.right && clientY >= r.top && clientY <= r.bottom) return true;
2615
+ }
2616
+ return false;
2617
+ }
2618
+ function dispatchBoundary(drag, type, event) {
2619
+ drag.sourceHost.dispatchEvent(new CustomEvent(type, {
2620
+ detail: {
2621
+ panelId: drag.panelId,
2622
+ clientX: event.clientX,
2623
+ clientY: event.clientY,
2624
+ screenX: event.screenX,
2625
+ screenY: event.screenY
2626
+ },
2627
+ bubbles: true,
2628
+ composed: true
2629
+ }));
2630
+ }
2631
+ function dispatchTearOut(drag, event) {
2632
+ const snapshot = drag.sourceHost.getPanelSnapshot(drag.panelId);
2633
+ if (snapshot === null) return;
2634
+ drag.sourceHost.dispatchEvent(new CustomEvent(`dock-panel-tear-out`, {
2635
+ detail: {
2636
+ panelId: drag.panelId,
2637
+ snapshot,
2638
+ clientX: event.clientX,
2639
+ clientY: event.clientY,
2640
+ screenX: event.screenX,
2641
+ screenY: event.screenY
2642
+ },
2643
+ bubbles: true,
2644
+ composed: true
2645
+ }));
2646
+ }
2647
+ function endDrag() {
2648
+ const drag = activeDrag;
2649
+ if (drag === null) return;
2650
+ for (const host of dragHosts) {
2651
+ host.setDropZone(null);
2652
+ host.setDragActive(false);
2653
+ }
2654
+ drag.ghost?.destroy();
2655
+ activeDrag = null;
2656
+ window.removeEventListener(`pointermove`, onPointerMove);
2657
+ window.removeEventListener(`pointerup`, onPointerUp);
2658
+ window.removeEventListener(`pointercancel`, onPointerCancel);
2659
+ window.removeEventListener(`keydown`, onKeyDown);
2660
+ }
2661
+ function isDragging() {
2662
+ return activeDrag !== null;
2663
+ }
2664
+ function getActiveDragPanelId() {
2665
+ return activeDrag?.panelId ?? null;
2666
+ }
2667
+ //#endregion
2668
+ //#region src/dock/dock-model.ts
2669
+ /**
2670
+ * The dock model is a plain mutable tree owned exclusively by the host
2671
+ * element. Mutators below change the tree in place; the host calls
2672
+ * `normalize(root)` then `requestUpdate()` after batching mutations.
2673
+ */
2674
+ function isSplit(node) {
2675
+ return node.kind === `split`;
2676
+ }
2677
+ function isStack(node) {
2678
+ return node.kind === `stack`;
2679
+ }
2680
+ function createEmptyStack() {
2681
+ return {
2682
+ kind: `stack`,
2683
+ panels: [],
2684
+ active: ``
2685
+ };
2686
+ }
2687
+ function createEmptyLayout() {
2688
+ return {
2689
+ version: 1,
2690
+ root: createEmptyStack()
2691
+ };
2692
+ }
2693
+ function createStack(panels, active) {
2694
+ return {
2695
+ kind: `stack`,
2696
+ panels: [...panels],
2697
+ active: active ?? panels[0] ?? ``
2698
+ };
2699
+ }
2700
+ function createSplit(direction, children, sizes) {
2701
+ const mean = children.length > 0 ? 1 / children.length : 1;
2702
+ return {
2703
+ kind: `split`,
2704
+ direction,
2705
+ children: [...children],
2706
+ sizes: sizes ? [...sizes] : children.map(() => mean)
2707
+ };
2708
+ }
2709
+ /**
2710
+ * Runs after every mutation:
2711
+ * - stack `active` fixed to a member of `panels` (or `''` when empty)
2712
+ * - empty stacks removed from their parent split (and their `sizes` entry)
2713
+ * - splits with ≤1 child replaced by that child (spliced into the parent)
2714
+ * - a child split whose direction equals its parent's is flattened into it,
2715
+ * spreading its sizes across the slot it vacated
2716
+ * - `sizes` kept parallel to `children` (padded with the mean weight)
2717
+ * - an empty root collapses to a single empty stack
2718
+ */
2719
+ function normalize(root) {
2720
+ if (isStack(root)) {
2721
+ if (!root.panels.includes(root.active)) root.active = root.panels[0] ?? ``;
2722
+ return root;
2723
+ }
2724
+ for (let i = root.children.length - 1; i >= 0; i--) {
2725
+ normalize(root.children[i]);
2726
+ const child = root.children[i];
2727
+ if (isStack(child) && child.panels.length === 0) {
2728
+ root.children.splice(i, 1);
2729
+ root.sizes.splice(i, 1);
2730
+ continue;
2731
+ }
2732
+ if (isSplit(child) && child.children.length === 1) {
2733
+ root.children.splice(i, 1, child.children[0]);
2734
+ root.sizes.splice(i, 1, child.sizes[0] ?? root.sizes[i] ?? 1);
2735
+ }
2736
+ }
2737
+ for (let i = root.children.length - 1; i >= 0; i--) {
2738
+ const child = root.children[i];
2739
+ if (isSplit(child) && child.direction === root.direction) {
2740
+ root.children.splice(i, 1, ...child.children);
2741
+ root.sizes.splice(i, 1, ...child.sizes);
2742
+ }
2743
+ }
2744
+ if (root.children.length === 0) return replaceNodeContents(root, createEmptyStack());
2745
+ if (root.children.length === 1) return root.children[0];
2746
+ while (root.sizes.length < root.children.length) root.sizes.push(1 / root.children.length);
2747
+ if (root.sizes.length > root.children.length) root.sizes.length = root.children.length;
2748
+ return root;
2749
+ }
2750
+ /**
2751
+ * Turn `target` (a stack or split node object) into the shape of `source`'s
2752
+ * kind, used when the root itself must dissolve. Mutates `target` in place so
2753
+ * the host's root reference stays valid.
2754
+ */
2755
+ function replaceNodeContents(target, source) {
2756
+ if (isStack(source)) {
2757
+ target.kind = `stack`;
2758
+ const t = target;
2759
+ t.panels = source.panels;
2760
+ t.active = source.active;
2761
+ delete t.collapsed;
2762
+ delete t.preCollapseSize;
2763
+ } else {
2764
+ target.kind = `split`;
2765
+ const t = target;
2766
+ t.direction = source.direction;
2767
+ t.children = source.children;
2768
+ t.sizes = source.sizes;
2769
+ }
2770
+ return target;
2771
+ }
2772
+ function findNode(root, path) {
2773
+ let current = root;
2774
+ for (const index of path) {
2775
+ if (!isSplit(current)) return null;
2776
+ const next = current.children[index];
2777
+ if (!next) return null;
2778
+ current = next;
2779
+ }
2780
+ return current;
2781
+ }
2782
+ /** Parent split of the node at `path`; null when path is empty or malformed. */
2783
+ function findParentOf(root, path) {
2784
+ if (path.length === 0) return null;
2785
+ const parent = findNode(root, path.slice(0, -1));
2786
+ return parent !== null && isSplit(parent) ? parent : null;
2787
+ }
2788
+ /** Path of the stack containing `panelId`, or null. */
2789
+ function findStackOf(root, panelId) {
2790
+ return findStackOfInner(root, panelId, []);
2791
+ }
2792
+ function findStackOfInner(node, panelId, path) {
2793
+ if (isStack(node)) return node.panels.includes(panelId) ? path : null;
2794
+ for (let i = 0; i < node.children.length; i++) {
2795
+ const found = findStackOfInner(node.children[i], panelId, [...path, i]);
2796
+ if (found !== null) return found;
2797
+ }
2798
+ return null;
2799
+ }
2800
+ function collectPanelIds(root) {
2801
+ if (isStack(root)) return [...root.panels];
2802
+ return root.children.flatMap(collectPanelIds);
2803
+ }
2804
+ /**
2805
+ * Paths of every stack leaf, in tree order. `[]` is the root itself when it
2806
+ * is a stack.
2807
+ */
2808
+ function collectStackPaths(root) {
2809
+ const result = [];
2810
+ collectStackPathsInner(root, [], result);
2811
+ return result;
2812
+ }
2813
+ function collectStackPathsInner(node, path, out) {
2814
+ if (isStack(node)) {
2815
+ out.push(path);
2816
+ return;
2817
+ }
2818
+ node.children.forEach((child, i) => collectStackPathsInner(child, [...path, i], out));
2819
+ }
2820
+ function addPanelToStack(stack, panelId, index) {
2821
+ const at = index === void 0 ? stack.panels.length : Math.max(0, Math.min(index, stack.panels.length));
2822
+ stack.panels.splice(at, 0, panelId);
2823
+ stack.active = panelId;
2824
+ }
2825
+ function removePanelFromStack(stack, panelId) {
2826
+ const index = stack.panels.indexOf(panelId);
2827
+ if (index < 0) return;
2828
+ stack.panels.splice(index, 1);
2829
+ if (stack.active === panelId) stack.active = stack.panels[Math.min(index, stack.panels.length - 1)] ?? ``;
2830
+ }
2831
+ function setActive(stack, panelId) {
2832
+ if (!stack.panels.includes(panelId) || stack.active === panelId) return false;
2833
+ stack.active = panelId;
2834
+ return true;
2835
+ }
2836
+ /**
2837
+ * Insert `node` beside the child at `targetPath`, into the parent split when
2838
+ * its direction matches `direction`, otherwise wrap the target in a new split.
2839
+ * `side` selects before/after; the neighbour's weight is split 50/50.
2840
+ * Returns the path of the inserted node, or null.
2841
+ */
2842
+ function insertBeside(root, targetPath, direction, newNode, side) {
2843
+ const target = findNode(root, targetPath);
2844
+ if (target === null) return null;
2845
+ if (targetPath.length === 0) {
2846
+ if (isSplit(root) && root.direction === direction) {
2847
+ const edgeIndex = side === `before` ? 0 : root.children.length;
2848
+ const neighbourIndex = side === `before` ? 0 : root.children.length - 1;
2849
+ const neighbour = root.sizes[neighbourIndex];
2850
+ root.children.splice(edgeIndex, 0, newNode);
2851
+ root.sizes.splice(edgeIndex, 0, neighbour !== void 0 ? neighbour / 2 : 1);
2852
+ if (neighbour !== void 0) root.sizes[side === `before` ? neighbourIndex + 1 : neighbourIndex] = neighbour / 2;
2853
+ return [edgeIndex];
2854
+ }
2855
+ wrapRoot(root, direction, newNode, side);
2856
+ return [side === `before` ? 0 : 1];
2857
+ }
2858
+ const parent = findParentOf(root, targetPath);
2859
+ if (parent === null) return null;
2860
+ const index = targetPath[targetPath.length - 1];
2861
+ if (parent.direction === direction) {
2862
+ const insertAt = side === `before` ? index : index + 1;
2863
+ const w = parent.sizes[index] !== void 0 ? parent.sizes[index] / 2 : 1;
2864
+ parent.sizes[index] = w;
2865
+ parent.children.splice(insertAt, 0, newNode);
2866
+ parent.sizes.splice(insertAt, 0, w);
2867
+ const childPath = [...targetPath.slice(0, -1), insertAt];
2868
+ normalize(root);
2869
+ return childPath;
2870
+ }
2871
+ const wrapper = createSplit(direction, side === `before` ? [newNode, target] : [target, newNode]);
2872
+ parent.children[index] = wrapper;
2873
+ normalize(root);
2874
+ const childIndex = side === `before` ? 0 : 1;
2875
+ return [
2876
+ ...targetPath.slice(0, -1),
2877
+ index,
2878
+ childIndex
2879
+ ];
2880
+ }
2881
+ /**
2882
+ * Wrap the whole root in a new split of `direction`, replacing it in place so
2883
+ * the caller's reference stays valid. Returns true on success.
2884
+ */
2885
+ function wrapRoot(root, direction, newNode, side) {
2886
+ replaceNodeContents(root, createSplit(direction, side === `before` ? [newNode, root] : [root, newNode]));
2887
+ normalize(root);
2888
+ return true;
2889
+ }
2890
+ /**
2891
+ * Remove the stack at `stackPath` from the tree. Its panels are moved into
2892
+ * `fallbackStack` (if given and still attached) so they are not silently
2893
+ * lost; otherwise they are dropped from the model (elements return to the
2894
+ * pool). Returns the removed stack.
2895
+ */
2896
+ function extractStack(root, stackPath, fallbackStack) {
2897
+ const stack = findNode(root, stackPath);
2898
+ if (stack === null || !isStack(stack)) return null;
2899
+ const isRoot = stackPath.length === 0;
2900
+ if (fallbackStack && fallbackStack !== stack) {
2901
+ for (const id of stack.panels) addPanelToStack(fallbackStack, id);
2902
+ if (!isRoot) {
2903
+ stack.panels = [];
2904
+ stack.active = ``;
2905
+ }
2906
+ }
2907
+ if (isRoot) {
2908
+ replaceNodeContents(root, createEmptyStack());
2909
+ return stack;
2910
+ }
2911
+ const parent = findParentOf(root, stackPath);
2912
+ if (parent === null) return null;
2913
+ const index = stackPath[stackPath.length - 1];
2914
+ parent.children.splice(index, 1);
2915
+ parent.sizes.splice(index, 1);
2916
+ normalize(root);
2917
+ return stack;
2918
+ }
2919
+ function serializeLayout(root) {
2920
+ return {
2921
+ version: 1,
2922
+ root: serializeNode(root)
2923
+ };
2924
+ }
2925
+ function serializeNode(node) {
2926
+ if (isStack(node)) {
2927
+ const out = {
2928
+ kind: `stack`,
2929
+ panels: [...node.panels],
2930
+ active: node.active
2931
+ };
2932
+ if (node.collapsed !== void 0) out.collapsed = node.collapsed;
2933
+ if (node.preCollapseSize !== void 0) out.preCollapseSize = node.preCollapseSize;
2934
+ return out;
2935
+ }
2936
+ return {
2937
+ kind: `split`,
2938
+ direction: node.direction,
2939
+ sizes: [...node.sizes],
2940
+ children: node.children.map(serializeNode)
2941
+ };
2942
+ }
2943
+ /**
2944
+ * Parse layout JSON, validate loosely, and normalize. Returns null when the
2945
+ * JSON is malformed or the root is missing/invalid.
2946
+ */
2947
+ function parseLayout(json) {
2948
+ try {
2949
+ const parsed = JSON.parse(json);
2950
+ if (parsed === null || typeof parsed !== `object`) return null;
2951
+ const root = parsed.root;
2952
+ if (root === null || typeof root !== `object` || !(`kind` in root)) return null;
2953
+ return {
2954
+ version: 1,
2955
+ root: normalize(root)
2956
+ };
2957
+ } catch {
2958
+ return null;
2959
+ }
2960
+ }
2961
+ //#endregion
2962
+ //#region src/tabs/tab-list-item.ts
2963
+ let TabListItemElement = class TabListItemElement extends LitElement {
2964
+ constructor(..._args) {
2965
+ super(..._args);
2966
+ this.closeable = false;
2967
+ this.hideLabels = false;
2968
+ this.iconName = void 0;
2969
+ this.description = void 0;
2970
+ this.commandActive = void 0;
2971
+ this.registry = void 0;
2972
+ this._compact = false;
2973
+ this._hasToolbarContent = false;
2974
+ this._onToolbarSlotChange = () => {
2975
+ this._updateToolbarContent();
2976
+ };
2977
+ }
2978
+ connectedCallback() {
2979
+ super.connectedCallback();
2980
+ this._resizeObserver = new ResizeObserver(([entry]) => {
2981
+ this._compact = entry.contentRect.width < 64;
2982
+ });
2983
+ this._resizeObserver.observe(this);
2984
+ (this.shadowRoot?.querySelector(`slot[name="toolbar"]`))?.addEventListener(`slotchange`, this._onToolbarSlotChange);
2985
+ }
2986
+ disconnectedCallback() {
2987
+ super.disconnectedCallback();
2988
+ this._resizeObserver?.disconnect();
2989
+ this._resizeObserver = void 0;
2990
+ this._commandCleanup?.();
2991
+ this._commandCleanup = void 0;
2992
+ (this.shadowRoot?.querySelector(`slot[name="toolbar"]`))?.removeEventListener(`slotchange`, this._onToolbarSlotChange);
2993
+ }
2994
+ updated(changed) {
2995
+ super.updated(changed);
2996
+ this._syncTooltipAttrs();
2997
+ if (changed.has(`registry`) || changed.has(`commandActive`)) this._syncCommandListener();
2998
+ }
2999
+ _syncTooltipAttrs() {
3000
+ if (this.description) {
3001
+ this.setAttribute(`data-tooltip`, this.description);
3002
+ this.setAttribute(`data-tooltip-anchor`, `top`);
3003
+ } else {
3004
+ this.removeAttribute(`data-tooltip`);
3005
+ this.removeAttribute(`data-tooltip-anchor`);
3006
+ }
3007
+ }
3008
+ _syncCommandListener() {
3009
+ this._commandCleanup?.();
3010
+ this._commandCleanup = void 0;
3011
+ if (this.registry && this.commandActive) this._commandCleanup = this.registry.addOnInvokeHandler((id) => {
3012
+ if (id === this.commandActive) this.click();
3013
+ });
3014
+ }
3015
+ _updateToolbarContent() {
3016
+ const slot = this.shadowRoot?.querySelector(`slot[name="toolbar"]`);
3017
+ this._hasToolbarContent = (slot?.assignedElements().length ?? 0) > 0;
3018
+ }
3019
+ firstUpdated() {
3020
+ this._updateToolbarContent();
3021
+ }
3022
+ render() {
3023
+ const showLabel = !this.hideLabels || !this.iconName;
3024
+ const hasActions = this.closeable || this._hasToolbarContent;
3025
+ return html`
3026
+ <div>
3027
+ ${this.iconName ? html`<ixfx-icon name="${this.iconName}"></ixfx-icon>` : nothing}
3028
+ ${showLabel ? html`<span class="label"><slot></slot></span>` : nothing}
3029
+ ${hasActions ? html`
3030
+ <div class="actions">
3031
+ <div class="toolbar"><slot name="toolbar"></slot></div>
3032
+ ${this.closeable ? html`
3033
+ <button
3034
+ class="close-btn"
3035
+ type="button"
3036
+ aria-label="Close tab"
3037
+ @click=${this._onCloseClick}
3038
+ >
3039
+ <ixfx-icon name="close"></ixfx-icon>
3040
+ </button>
3041
+ ` : nothing}
3042
+ </div>
3043
+ ` : nothing}
3044
+ </div>
3045
+ `;
3046
+ }
3047
+ _onCloseClick(e) {
3048
+ e.stopPropagation();
3049
+ this.dispatchEvent(new CustomEvent(`close`, {
3050
+ bubbles: true,
3051
+ composed: true
3052
+ }));
3053
+ this.remove();
3054
+ }
3055
+ static {
3056
+ this.styles = [
3057
+ themeFallbacks,
3058
+ prominenceStyles,
3059
+ css`
3060
+ :host {
3061
+ --selected-tab-color: var(--ixfx-selected-tab-color, var(--surface-2-text));
3062
+ --selected-tab-border-width: var(--ixfx-selected-tab-border-width, 1px);
3063
+ --selected-tab-weight: var(--ixfx-selected-tab-weight, bold);
3064
+ --selected-background: var(--ixfx-tab-selected-background, var(--surface-5));
3065
+ --tab-padding: var(--ixfx-tab-padding, var(--space-s));
3066
+ display: inline-flex;
3067
+ min-width: 0;
3068
+ user-select: none;
3069
+ box-sizing: border-box;
3070
+ font-size: var(--text-xs);
3071
+ /* No explicit height: items stretch (align-self: stretch via the
3072
+ list's flex container) so every tab in a strip shares one height
3073
+ even when toolbars give some items taller content. */
3074
+ }
3075
+
3076
+ :host > div {
3077
+ display: flex;
3078
+ align-items: center;
3079
+ padding: var(--tab-padding) calc(2 * var(--tab-padding-horizontal, var(--tab-padding)));
3080
+ min-width: 0;
3081
+ width: 100%;
3082
+ font-weight: 500;
3083
+ border: var(--selected-tab-border-width) solid transparent;
3084
+ gap: var(--space-s);
3085
+
3086
+ }
3087
+
3088
+ :host([selected]) > div {
3089
+ color: var(--selected-tab-color);
3090
+ border-top-color: var(--indicator-color-top, transparent);
3091
+ border-bottom-color: var(--indicator-color-bottom, transparent);
3092
+ border-left-color: var(--indicator-color-left, transparent);
3093
+ border-right-color: var(--indicator-color-right, transparent);
3094
+ background: var(--selected-background);
3095
+ font-weight: var(--selected-tab-weight);
3096
+ border-radius:var(--radius-m);
3097
+ }
3098
+
3099
+ .label {
3100
+ overflow: var(--allow-overflow, hidden);
3101
+ text-overflow: ellipsis;
3102
+ white-space: nowrap;
3103
+ min-width: 0;
3104
+ flex: 1 1 auto;
3105
+ }
3106
+
3107
+ ixfx-icon {
3108
+ flex-shrink: 0;
3109
+ }
3110
+
3111
+ .actions {
3112
+ display: flex;
3113
+ align-items: center;
3114
+ gap: var(--space-xs);
3115
+ margin-left: auto;
3116
+ flex-shrink: 0;
3117
+ }
3118
+
3119
+ .toolbar {
3120
+ /* Always laid out (display: flex) so the tab's width doesn't shift when
3121
+ the toolbar appears on selection. visibility: hidden keeps the
3122
+ toolbar from receiving pointer events or painting, while preserving
3123
+ the space its contents take up. */
3124
+ display: flex;
3125
+ align-items: center;
3126
+ gap: var(--space-xs);
3127
+ visibility: hidden;
3128
+ }
3129
+
3130
+ :host([selected]) .toolbar {
3131
+ visibility: visible;
3132
+ }
3133
+
3134
+ slot[name="toolbar"] {
3135
+ display: contents;
3136
+ }
3137
+
3138
+ /* Slotted buttons inherit these through the shadow boundary; keeps a
3139
+ toolbar button from inflating the strip height. */
3140
+ .toolbar ::slotted(ixfx-button) {
3141
+ --ixfx-button-padding-block: var(--space-xs);
3142
+ }
3143
+
3144
+ .close-btn {
3145
+ display: inline-flex;
3146
+ align-items: center;
3147
+ justify-content: center;
3148
+ background: none;
3149
+ border: none;
3150
+ padding: var(--space-xs);
3151
+ cursor: pointer;
3152
+ color: inherit;
3153
+ border-radius: var(--radius-s);
3154
+ opacity: var(--prominence-idle-opacity);
3155
+ transition: opacity var(--transition), background var(--transition);
3156
+ }
3157
+
3158
+ .close-btn:hover {
3159
+ background: var(--surface-5);
3160
+ opacity: var(--prominence-tickled-opacity);
3161
+ }
3162
+
3163
+ .close-btn:focus-visible {
3164
+ outline: 2px solid var(--accent);
3165
+ outline-offset: 1px;
3166
+ }
3167
+
3168
+ :host([selected]) .close-btn,
3169
+ :host(:hover) .close-btn {
3170
+ opacity: var(--prominence-tickled-opacity);
3171
+ }
3172
+ `
3173
+ ];
3174
+ }
3175
+ };
3176
+ __decorate([property({ type: Boolean })], TabListItemElement.prototype, "closeable", void 0);
3177
+ __decorate([property({
3178
+ type: Boolean,
3179
+ attribute: `hide-labels`
3180
+ })], TabListItemElement.prototype, "hideLabels", void 0);
3181
+ __decorate([property({
3182
+ type: String,
3183
+ attribute: `icon-name`
3184
+ })], TabListItemElement.prototype, "iconName", void 0);
3185
+ __decorate([property({ type: String })], TabListItemElement.prototype, "description", void 0);
3186
+ __decorate([property({
3187
+ type: String,
3188
+ attribute: `command-active`
3189
+ })], TabListItemElement.prototype, "commandActive", void 0);
3190
+ __decorate([property({ attribute: false })], TabListItemElement.prototype, "registry", void 0);
3191
+ __decorate([state()], TabListItemElement.prototype, "_compact", void 0);
3192
+ __decorate([state()], TabListItemElement.prototype, "_hasToolbarContent", void 0);
3193
+ TabListItemElement = __decorate([safeCustomElement(`ixfx-tab-list-item`)], TabListItemElement);
3194
+ //#endregion
3195
+ //#region src/dock/dock-panel.ts
3196
+ /** Pointer movement threshold before a titlebar press becomes a dock drag. */
3197
+ const DRAG_THRESHOLD_PX$1 = 5;
3198
+ let DockPanelElement = class DockPanelElement extends LitElement {
3199
+ constructor(..._args) {
3200
+ super(..._args);
3201
+ this.panelId = ``;
3202
+ this.title = ``;
3203
+ this.iconName = ``;
3204
+ this.closable = false;
3205
+ this.collapsible = false;
3206
+ this.accepts = ``;
3207
+ this.presentation = `solo`;
3208
+ this._collapsed = false;
3209
+ this._dragPointerId = null;
3210
+ this._dragStartX = 0;
3211
+ this._dragStartY = 0;
3212
+ this._dragStarted = false;
3213
+ this._onPointerDown = (event) => {
3214
+ if (event.button !== 0) return;
3215
+ if (this.presentation === `tabbed`) return;
3216
+ if (!this._isDragHandle(event)) return;
3217
+ this._dragPointerId = event.pointerId;
3218
+ this._dragStartX = event.clientX;
3219
+ this._dragStartY = event.clientY;
3220
+ this._dragStarted = false;
3221
+ window.addEventListener(`pointermove`, this._onPointerMove);
3222
+ window.addEventListener(`pointerup`, this._onPointerUp);
3223
+ window.addEventListener(`pointercancel`, this._onPointerUp);
3224
+ };
3225
+ this._onPointerMove = (event) => {
3226
+ if (this._dragPointerId === null || event.pointerId !== this._dragPointerId) return;
3227
+ if (!this._dragStarted) {
3228
+ if (Math.hypot(event.clientX - this._dragStartX, event.clientY - this._dragStartY) < DRAG_THRESHOLD_PX$1) return;
3229
+ this._dragStarted = true;
3230
+ const pointerId = this._dragPointerId;
3231
+ this._teardownDragListeners();
3232
+ this._dispatchDragStart(pointerId, event.clientX, event.clientY);
3233
+ }
3234
+ };
3235
+ this._onPointerUp = (event) => {
3236
+ if (this._dragPointerId === null || event.pointerId !== this._dragPointerId) return;
3237
+ this._teardownDragListeners();
3238
+ };
3239
+ this._onTitlebarToggle = (event) => {
3240
+ event.stopPropagation();
3241
+ this._collapsed = event.detail.collapsed;
3242
+ this.dispatchEvent(new CustomEvent(`toggle`, {
3243
+ detail: event.detail,
3244
+ bubbles: true,
3245
+ composed: true
3246
+ }));
3247
+ };
3248
+ this._onTitlebarClose = (event) => {
3249
+ event.stopPropagation();
3250
+ this.dispatchEvent(new CustomEvent(`close`, {
3251
+ bubbles: true,
3252
+ composed: true
3253
+ }));
3254
+ this.remove();
3255
+ };
3256
+ }
3257
+ connectedCallback() {
3258
+ super.connectedCallback();
3259
+ this.addEventListener(`pointerdown`, this._onPointerDown);
3260
+ }
3261
+ disconnectedCallback() {
3262
+ super.disconnectedCallback();
3263
+ this._teardownDragListeners();
3264
+ this.removeEventListener(`pointerdown`, this._onPointerDown);
3265
+ }
3266
+ _teardownDragListeners() {
3267
+ this._dragPointerId = null;
3268
+ this._dragStarted = false;
3269
+ window.removeEventListener(`pointermove`, this._onPointerMove);
3270
+ window.removeEventListener(`pointerup`, this._onPointerUp);
3271
+ window.removeEventListener(`pointercancel`, this._onPointerUp);
3272
+ }
3273
+ /** Clicks that begin on the close / caret buttons or the toolbar are not drags. */
3274
+ _isDragHandle(event) {
3275
+ for (const node of event.composedPath()) {
3276
+ if (!(node instanceof HTMLElement)) continue;
3277
+ if (node.classList.contains(`close-btn`) || node.classList.contains(`caret-btn`) || node.classList.contains(`toolbar`)) return false;
3278
+ }
3279
+ return true;
3280
+ }
3281
+ _dispatchDragStart(pointerId, clientX, clientY) {
3282
+ if (!this.panelId) return;
3283
+ this.dispatchEvent(new CustomEvent(`dock-panel-drag-start`, {
3284
+ detail: {
3285
+ panelId: this.panelId,
3286
+ pointerId,
3287
+ clientX,
3288
+ clientY
3289
+ },
3290
+ bubbles: true,
3291
+ composed: true
3292
+ }));
3293
+ }
3294
+ render() {
3295
+ const tabbed = this.presentation === `tabbed`;
3296
+ const collapsed = !tabbed && this._collapsed;
3297
+ return html`
3298
+ ${tabbed ? nothing : html`
3299
+ <ixfx-titlebar
3300
+ .title=${this.title}
3301
+ .iconName=${this.iconName}
3302
+ .closable=${this.closable}
3303
+ .collapsible=${this.collapsible}
3304
+ .collapsed=${collapsed}
3305
+ .toggleInteraction=${this.collapsible ? `implicit` : `disabled`}
3306
+ @toggle=${this._onTitlebarToggle}
3307
+ @close=${this._onTitlebarClose}
3308
+ >
3309
+ <slot name="toolbar" slot="toolbar"></slot>
3310
+ </ixfx-titlebar>
3311
+ `}
3312
+ <div class="content" ?collapsed=${collapsed}>
3313
+ <slot></slot>
3314
+ </div>
3315
+ `;
3316
+ }
3317
+ static {
3318
+ this.styles = [themeFallbacks, css`
3319
+ :host {
3320
+ display: flex;
3321
+ flex-direction: column;
3322
+ width: 100%;
3323
+ height: 100%;
3324
+ min-width: 0;
3325
+ min-height: 0;
3326
+ }
3327
+
3328
+ ixfx-titlebar {
3329
+ flex-shrink: 0;
3330
+ }
3331
+
3332
+ .content {
3333
+ flex: 1;
3334
+ min-height: 0;
3335
+ min-width: 0;
3336
+ overflow: hidden;
3337
+ }
3338
+
3339
+ .content[collapsed] {
3340
+ display: none;
3341
+ }
3342
+ `];
3343
+ }
3344
+ };
3345
+ __decorate([property({
3346
+ type: String,
3347
+ attribute: `panel-id`,
3348
+ reflect: true
3349
+ })], DockPanelElement.prototype, "panelId", void 0);
3350
+ __decorate([property({ type: String })], DockPanelElement.prototype, "title", void 0);
3351
+ __decorate([property({
3352
+ type: String,
3353
+ attribute: `icon-name`
3354
+ })], DockPanelElement.prototype, "iconName", void 0);
3355
+ __decorate([property({ type: Boolean })], DockPanelElement.prototype, "closable", void 0);
3356
+ __decorate([property({ type: Boolean })], DockPanelElement.prototype, "collapsible", void 0);
3357
+ __decorate([property({
3358
+ type: String,
3359
+ attribute: `accepts`
3360
+ })], DockPanelElement.prototype, "accepts", void 0);
3361
+ __decorate([property({
3362
+ type: String,
3363
+ reflect: true
3364
+ })], DockPanelElement.prototype, "presentation", void 0);
3365
+ __decorate([state()], DockPanelElement.prototype, "_collapsed", void 0);
3366
+ DockPanelElement = __decorate([safeCustomElement(`ixfx-dock-panel`)], DockPanelElement);
3367
+ //#endregion
3368
+ //#region src/dock/dock-host.ts
3369
+ /** Fraction of the leaf rect (from each edge) that forms the split bands. */
3370
+ const BAND = .25;
3371
+ /** Pointer movement threshold before a press becomes a dock drag (matches tab-list). */
3372
+ const DRAG_THRESHOLD_PX = 5;
3373
+ /** Tolerance (px) when matching a child gutter endpoint to a parent gutter line. */
3374
+ const JUNCTION_TOL_PX = 6;
3375
+ /** Junction points closer than this merge into one handle (a 4-way cross yields one per side). */
3376
+ const JUNCTION_MERGE_PX = 8;
3377
+ let DockHostElement = class DockHostElement extends LitElement {
3378
+ constructor(..._args) {
3379
+ super(..._args);
3380
+ this.layout = null;
3381
+ this.accepts = `tabs horizontal vertical`;
3382
+ this._model = createStack([]);
3383
+ this._pool = [];
3384
+ this._dropZone = null;
3385
+ this._dragActive = false;
3386
+ this._junctions = [];
3387
+ this._toolbarsOut = /* @__PURE__ */ new Map();
3388
+ this._tabDragPending = null;
3389
+ this._junctionsKey = ``;
3390
+ this._junctionsRaf = null;
3391
+ this._hostResizeObserver = null;
3392
+ this._onDragStart = (event) => {
3393
+ const { panelId, pointerId, clientX, clientY } = event.detail;
3394
+ startDrag(panelId, this, pointerId, clientX, clientY);
3395
+ };
3396
+ this._onPoolSlotChange = () => {
3397
+ this._syncPool();
3398
+ };
3399
+ this._onJunctionPointerDown = (event, junction) => {
3400
+ if (event.button !== 0) return;
3401
+ event.preventDefault();
3402
+ event.stopPropagation();
3403
+ let started = false;
3404
+ for (const target of junction.targets) {
3405
+ if (!target.split.isConnected) continue;
3406
+ if (target.split.beginGutterDrag(target.gutterIndex, event.pointerId, event.clientX, event.clientY)) started = true;
3407
+ }
3408
+ if (!started) return;
3409
+ this._suppressNextClick();
3410
+ const follow = () => this._scheduleJunctions();
3411
+ const stop = () => {
3412
+ window.removeEventListener(`pointermove`, follow);
3413
+ window.removeEventListener(`pointerup`, stop);
3414
+ window.removeEventListener(`pointercancel`, stop);
3415
+ };
3416
+ window.addEventListener(`pointermove`, follow);
3417
+ window.addEventListener(`pointerup`, stop);
3418
+ window.addEventListener(`pointercancel`, stop);
3419
+ };
3420
+ this._onSplitWeightsChanged = () => {
3421
+ this._foldLiveWeights();
3422
+ this._scheduleJunctions();
3423
+ };
3424
+ this._onSplitCollapsed = (event, collapsed) => {
3425
+ const detail = event.detail;
3426
+ if (!detail?.id) return;
3427
+ const pathStr = this.shadowRoot?.getElementById(detail.id)?.getAttribute(`data-dock-path`);
3428
+ if (pathStr === null || pathStr === void 0) return;
3429
+ const node = findNode(this._model, parsePath(pathStr));
3430
+ if (node === null || !isStack(node)) return;
3431
+ if (collapsed) node.collapsed = true;
3432
+ else delete node.collapsed;
3433
+ };
3434
+ this._onSplitClose = (event) => {
3435
+ event.preventDefault();
3436
+ const detail = event.detail;
3437
+ if (!detail?.id) return;
3438
+ const pathStr = this.shadowRoot?.getElementById(detail.id)?.getAttribute(`data-dock-path`);
3439
+ if (pathStr === null || pathStr === void 0) return;
3440
+ const path = parsePath(pathStr);
3441
+ const paths = collectStackPaths(this._model).filter((p) => p.join(`-`) !== path.join(`-`));
3442
+ const fallbackNode = paths.length > 0 ? findNode(this._model, paths[0]) : null;
3443
+ const fallback = fallbackNode !== null && isStack(fallbackNode) ? fallbackNode : void 0;
3444
+ this._returnAllToolbars();
3445
+ extractStack(this._model, path, fallback);
3446
+ this._commit();
3447
+ };
3448
+ this._onTabChange = (event, path) => {
3449
+ const panelId = event.detail.selected;
3450
+ const stack = findNode(this._model, path);
3451
+ if (stack === null || !isStack(stack)) return;
3452
+ if (!setActive(stack, panelId)) return;
3453
+ this.requestUpdate();
3454
+ this.dispatchEvent(new CustomEvent(`dock-panel-activated`, {
3455
+ detail: { panelId },
3456
+ bubbles: true,
3457
+ composed: true
3458
+ }));
3459
+ };
3460
+ this._onStripPointerDown = (event) => {
3461
+ if (event.button !== 0) return;
3462
+ const path = event.composedPath();
3463
+ for (const node of path) {
3464
+ if (!(node instanceof HTMLElement)) continue;
3465
+ if (node.classList.contains(`list-toolbar`) || node.classList.contains(`close-btn`)) return;
3466
+ }
3467
+ const item = path.find((node) => node instanceof HTMLElement && node.tagName === `IXFX-TAB-LIST-ITEM`) ?? null;
3468
+ if (item === null || !item.id) return;
3469
+ this._tabDragPending = {
3470
+ panelId: item.id,
3471
+ pointerId: event.pointerId,
3472
+ startX: event.clientX,
3473
+ startY: event.clientY
3474
+ };
3475
+ window.addEventListener(`pointermove`, this._onTabDragMove);
3476
+ window.addEventListener(`pointerup`, this._onTabDragUp);
3477
+ window.addEventListener(`pointercancel`, this._onTabDragUp);
3478
+ };
3479
+ this._onTabDragMove = (event) => {
3480
+ const pending = this._tabDragPending;
3481
+ if (pending === null || event.pointerId !== pending.pointerId) return;
3482
+ if (Math.hypot(event.clientX - pending.startX, event.clientY - pending.startY) < DRAG_THRESHOLD_PX) return;
3483
+ this._tabDragPending = null;
3484
+ this._teardownTabDragListeners();
3485
+ this._suppressNextClick();
3486
+ this.dispatchEvent(new CustomEvent(`dock-panel-drag-start`, {
3487
+ detail: {
3488
+ panelId: pending.panelId,
3489
+ pointerId: pending.pointerId,
3490
+ clientX: event.clientX,
3491
+ clientY: event.clientY
3492
+ },
3493
+ bubbles: true,
3494
+ composed: true
3495
+ }));
3496
+ };
3497
+ this._onTabDragUp = (event) => {
3498
+ if (this._tabDragPending === null || event.pointerId !== this._tabDragPending.pointerId) return;
3499
+ this._tabDragPending = null;
3500
+ this._teardownTabDragListeners();
3501
+ };
3502
+ }
3503
+ connectedCallback() {
3504
+ super.connectedCallback();
3505
+ this._buildInitialModel();
3506
+ registerDockHost(this);
3507
+ this.addEventListener(`dock-panel-drag-start`, this._onDragStart);
3508
+ this._hostResizeObserver = new ResizeObserver(() => this._scheduleJunctions());
3509
+ this._hostResizeObserver.observe(this);
3510
+ }
3511
+ disconnectedCallback() {
3512
+ super.disconnectedCallback();
3513
+ unregisterDockHost(this);
3514
+ this.removeEventListener(`dock-panel-drag-start`, this._onDragStart);
3515
+ this._hostResizeObserver?.disconnect();
3516
+ this._hostResizeObserver = null;
3517
+ if (this._junctionsRaf !== null) {
3518
+ cancelAnimationFrame(this._junctionsRaf);
3519
+ this._junctionsRaf = null;
3520
+ }
3521
+ this._junctions = [];
3522
+ this._junctionsKey = ``;
3523
+ this._returnAllToolbars();
3524
+ }
3525
+ willUpdate(changed) {
3526
+ super.willUpdate(changed);
3527
+ if (changed.has(`layout`) && this.layout !== null) {
3528
+ const parsed = parseLayout(this.layout);
3529
+ if (parsed) {
3530
+ this._model = parsed.root;
3531
+ this._mergePoolIntoModel();
3532
+ }
3533
+ }
3534
+ }
3535
+ updated(changed) {
3536
+ super.updated(changed);
3537
+ this._syncSlots();
3538
+ this._syncToolbars();
3539
+ this._scheduleJunctions();
3540
+ }
3541
+ _buildInitialModel() {
3542
+ if (this.layout !== null) {
3543
+ const parsed = parseLayout(this.layout);
3544
+ if (parsed) {
3545
+ this._model = parsed.root;
3546
+ this._mergePoolIntoModel();
3547
+ return;
3548
+ }
3549
+ }
3550
+ this._model = createStack([]);
3551
+ this._mergePoolIntoModel();
3552
+ }
3553
+ /** Ensure every pooled panel element appears in the model (appended to the first stack). */
3554
+ _mergePoolIntoModel() {
3555
+ const known = new Set(collectPanelIds(this._model));
3556
+ let changed = false;
3557
+ for (const el of this._scanPanels()) if (!known.has(el.panelId)) {
3558
+ this._appendPanelToFirstStack(el.panelId);
3559
+ known.add(el.panelId);
3560
+ changed = true;
3561
+ }
3562
+ if (changed) normalize(this._model);
3563
+ }
3564
+ _scanPanels() {
3565
+ return Array.from(this.children).filter((el) => el instanceof HTMLElement && el.tagName === `IXFX-DOCK-PANEL` && Boolean(el.panelId));
3566
+ }
3567
+ _findPanelEl(panelId) {
3568
+ return this._pool.find((p) => p.panelId === panelId) ?? this._scanPanels().find((p) => p.panelId === panelId);
3569
+ }
3570
+ _firstStack() {
3571
+ const paths = collectStackPaths(this._model);
3572
+ const node = paths.length > 0 ? findNode(this._model, paths[0]) : null;
3573
+ if (node !== null && isStack(node)) return node;
3574
+ const fresh = createStack([]);
3575
+ wrapRoot(this._model, `row`, fresh, `after`);
3576
+ normalize(this._model);
3577
+ return fresh;
3578
+ }
3579
+ _appendPanelToFirstStack(panelId) {
3580
+ addPanelToStack(this._model.kind === `stack` ? this._model : this._firstStack(), panelId);
3581
+ }
3582
+ _syncPool() {
3583
+ const current = this._scanPanels();
3584
+ const currentIds = new Set(current.map((p) => p.panelId));
3585
+ let structuralChange = false;
3586
+ for (const el of [...this._pool]) if (!currentIds.has(el.panelId)) {
3587
+ this._toolbarsOut.delete(el.panelId);
3588
+ this._removeFromModel(el.panelId);
3589
+ this.dispatchEvent(new CustomEvent(`dock-panel-removed`, {
3590
+ detail: { panelId: el.panelId },
3591
+ bubbles: true,
3592
+ composed: true
3593
+ }));
3594
+ structuralChange = true;
3595
+ }
3596
+ for (const el of current) if (!collectPanelIds(this._model).includes(el.panelId)) {
3597
+ this._appendPanelToFirstStack(el.panelId);
3598
+ normalize(this._model);
3599
+ this.dispatchEvent(new CustomEvent(`dock-panel-added`, {
3600
+ detail: { panelId: el.panelId },
3601
+ bubbles: true,
3602
+ composed: true
3603
+ }));
3604
+ structuralChange = true;
3605
+ }
3606
+ this._pool = current;
3607
+ this.requestUpdate();
3608
+ if (structuralChange) this._emitChange();
3609
+ }
3610
+ /** Serialize the current layout, folding live split weights from the DOM. */
3611
+ saveLayout() {
3612
+ this._foldLiveWeights();
3613
+ return serializeLayout(this._model);
3614
+ }
3615
+ loadLayout(layout) {
3616
+ this._returnAllToolbars();
3617
+ this._model = normalize(cloneNode(layout.root));
3618
+ this._mergePoolIntoModel();
3619
+ this.requestUpdate();
3620
+ this._emitChange();
3621
+ }
3622
+ addPanel(panel, target) {
3623
+ let el;
3624
+ if (panel instanceof HTMLElement) el = panel;
3625
+ else {
3626
+ el = document.createElement(`ixfx-dock-panel`);
3627
+ el.panelId = panel.panelId;
3628
+ if (panel.title !== void 0) el.title = panel.title;
3629
+ if (panel.iconName !== void 0) el.iconName = panel.iconName;
3630
+ if (panel.closable !== void 0) el.closable = panel.closable;
3631
+ if (panel.collapsible !== void 0) el.collapsible = panel.collapsible;
3632
+ }
3633
+ if (el.parentElement !== this) this.appendChild(el);
3634
+ if (!this._pool.some((p) => p.panelId === el.panelId)) this._pool = [...this._pool, el];
3635
+ this._returnAllToolbars();
3636
+ const op = target?.op ?? `tabs`;
3637
+ if (target?.stack) {
3638
+ const stackPath = findStackOf(this._model, target.stack);
3639
+ if (stackPath !== null) {
3640
+ if (op === `tabs`) {
3641
+ const stack = findNode(this._model, stackPath);
3642
+ if (stack !== null && isStack(stack)) addPanelToStack(stack, el.panelId, target.index);
3643
+ } else insertBeside(this._model, stackPath, opToDirection(op), createStack([el.panelId]), `after`);
3644
+ }
3645
+ } else this._appendPanelToFirstStack(el.panelId);
3646
+ normalize(this._model);
3647
+ this.requestUpdate();
3648
+ this.dispatchEvent(new CustomEvent(`dock-panel-added`, {
3649
+ detail: { panelId: el.panelId },
3650
+ bubbles: true,
3651
+ composed: true
3652
+ }));
3653
+ this._emitChange();
3654
+ }
3655
+ removePanel(panelId) {
3656
+ if (!collectPanelIds(this._model).includes(panelId)) return false;
3657
+ this._returnAllToolbars();
3658
+ this._removeFromModel(panelId);
3659
+ this._pool = this._pool.filter((p) => p.panelId !== panelId);
3660
+ this._toolbarsOut.delete(panelId);
3661
+ this._scanPanels().find((p) => p.panelId === panelId)?.remove();
3662
+ normalize(this._model);
3663
+ this.requestUpdate();
3664
+ this.dispatchEvent(new CustomEvent(`dock-panel-removed`, {
3665
+ detail: { panelId },
3666
+ bubbles: true,
3667
+ composed: true
3668
+ }));
3669
+ this._emitChange();
3670
+ return true;
3671
+ }
3672
+ activatePanel(panelId) {
3673
+ const stackPath = findStackOf(this._model, panelId);
3674
+ if (stackPath === null) return false;
3675
+ const stack = findNode(this._model, stackPath);
3676
+ if (stack === null || !isStack(stack)) return false;
3677
+ if (!setActive(stack, panelId)) return false;
3678
+ this.requestUpdate();
3679
+ this.dispatchEvent(new CustomEvent(`dock-panel-activated`, {
3680
+ detail: { panelId },
3681
+ bubbles: true,
3682
+ composed: true
3683
+ }));
3684
+ return true;
3685
+ }
3686
+ getPanelIds() {
3687
+ return collectPanelIds(this._model);
3688
+ }
3689
+ /**
3690
+ * Capture a structured-clone-safe {@link DockPanelSnapshot} of a panel
3691
+ * (identity, chrome attributes, and a best-effort `innerHTML`). Returns
3692
+ * null when no such panel element is known to this host.
3693
+ */
3694
+ getPanelSnapshot(panelId) {
3695
+ const el = this._findPanelEl(panelId);
3696
+ if (el === void 0) return null;
3697
+ return {
3698
+ panelId: el.panelId,
3699
+ title: el.title,
3700
+ iconName: el.iconName,
3701
+ closable: el.closable,
3702
+ collapsible: el.collapsible,
3703
+ accepts: el.accepts,
3704
+ contentHTML: el.innerHTML
3705
+ };
3706
+ }
3707
+ /**
3708
+ * {@link getPanelSnapshot} + {@link removePanel} in one step: take the
3709
+ * snapshot, then drop the panel (and its element) from this host. Call this
3710
+ * from a `dock-panel-tear-out` handler once the receiving window has
3711
+ * recreated the panel.
3712
+ */
3713
+ detachPanel(panelId) {
3714
+ const snapshot = this.getPanelSnapshot(panelId);
3715
+ if (snapshot === null) return null;
3716
+ this.removePanel(panelId);
3717
+ return snapshot;
3718
+ }
3719
+ /**
3720
+ * Hit-test viewport coordinates against this host and return a plain,
3721
+ * serializable {@link DockDropTarget} (no element references) — or null when
3722
+ * the point is outside the host or the resulting zone is disallowed by
3723
+ * `accepts` / `canAccept`. Pure query: no model mutation, no overlay.
3724
+ *
3725
+ * `panelId` is passed to `canAccept`; use any stable id for an external
3726
+ * panel (defaults to `"__external__"`).
3727
+ */
3728
+ queryDropTarget(clientX, clientY, panelId = `__external__`) {
3729
+ const zone = this.computeDropZone(clientX, clientY, panelId);
3730
+ return zone === null ? null : zoneToTarget(zone);
3731
+ }
3732
+ /**
3733
+ * Like {@link queryDropTarget}, but also shows the drop overlay so an
3734
+ * external (cross-window) drag gets the same visual feedback as an in-page
3735
+ * one. Call on every pointer update from the outer coordinator, then
3736
+ * {@link endExternalDrop} when the drag leaves or ends.
3737
+ */
3738
+ previewExternalDrop(clientX, clientY, panelId = `__external__`) {
3739
+ this.setDragActive(true);
3740
+ const zone = this.computeDropZone(clientX, clientY, panelId);
3741
+ this.setDropZone(zone);
3742
+ return zone === null ? null : zoneToTarget(zone);
3743
+ }
3744
+ /** Clear any overlay left by {@link previewExternalDrop}. */
3745
+ endExternalDrop() {
3746
+ this.setDropZone(null);
3747
+ this.setDragActive(false);
3748
+ }
3749
+ /**
3750
+ * Recreate a panel from a {@link DockPanelSnapshot} (or a bare
3751
+ * {@link DockPanelInit}) and dock it. With `at` viewport coordinates the
3752
+ * panel lands where a drop there would put it (respecting `accepts` /
3753
+ * `canAccept`); without them, or when the point misses the host, it goes
3754
+ * into the first stack. Returns the created element, or null when a panel
3755
+ * with that id is already docked here.
3756
+ */
3757
+ acceptExternalPanel(snapshot, at) {
3758
+ if (collectPanelIds(this._model).includes(snapshot.panelId)) return null;
3759
+ const el = document.createElement(`ixfx-dock-panel`);
3760
+ el.panelId = snapshot.panelId;
3761
+ if (snapshot.title !== void 0) el.title = snapshot.title;
3762
+ if (snapshot.iconName !== void 0) el.iconName = snapshot.iconName;
3763
+ if (snapshot.closable !== void 0) el.closable = snapshot.closable;
3764
+ if (snapshot.collapsible !== void 0) el.collapsible = snapshot.collapsible;
3765
+ if (`accepts` in snapshot && snapshot.accepts) el.setAttribute(`accepts`, snapshot.accepts);
3766
+ if (`contentHTML` in snapshot && snapshot.contentHTML) el.innerHTML = snapshot.contentHTML;
3767
+ this._returnAllToolbars();
3768
+ this.appendChild(el);
3769
+ this._pool = [...this._pool, el];
3770
+ this._appendPanelToFirstStack(el.panelId);
3771
+ normalize(this._model);
3772
+ this.endExternalDrop();
3773
+ const zone = at ? this.computeDropZone(at.clientX, at.clientY, el.panelId) : null;
3774
+ if (zone !== null) this._applyZone(el.panelId, zone);
3775
+ normalize(this._model);
3776
+ this.requestUpdate();
3777
+ this.dispatchEvent(new CustomEvent(`dock-panel-added`, {
3778
+ detail: { panelId: el.panelId },
3779
+ bubbles: true,
3780
+ composed: true
3781
+ }));
3782
+ this._emitChange();
3783
+ return el;
3784
+ }
3785
+ /**
3786
+ * Hit-test a pointer position against this host's leaves and edges. Returns
3787
+ * a zone with rects relative to the host, or null when the pointer is
3788
+ * outside the host or the candidate zone is disallowed.
3789
+ */
3790
+ computeDropZone(clientX, clientY, panelId) {
3791
+ const hostRect = this.getBoundingClientRect();
3792
+ if (clientX < hostRect.left || clientX > hostRect.right || clientY < hostRect.top || clientY > hostRect.bottom) return null;
3793
+ const rel = (r) => ({
3794
+ x: r.left - hostRect.left,
3795
+ y: r.top - hostRect.top,
3796
+ w: r.width,
3797
+ h: r.height
3798
+ });
3799
+ for (const path of collectStackPaths(this._model)) {
3800
+ const leaf = this.shadowRoot?.querySelector(`[data-dock-path="${path.join(`-`)}"]`);
3801
+ if (!leaf) continue;
3802
+ const leafRect = leaf.getBoundingClientRect();
3803
+ if (clientX < leafRect.left || clientX > leafRect.right || clientY < leafRect.top || clientY > leafRect.bottom) continue;
3804
+ const stack = findNode(this._model, path);
3805
+ if (stack === null || !isStack(stack)) continue;
3806
+ const strip = leaf.querySelector(`ixfx-tab-list`);
3807
+ if (strip !== null) {
3808
+ const stripRect = strip.getBoundingClientRect();
3809
+ if (clientX >= stripRect.left && clientX <= stripRect.right && clientY >= stripRect.top && clientY <= stripRect.bottom) {
3810
+ const gapIndex = computeStripGapIndex(strip, clientX, clientY);
3811
+ const zone = {
3812
+ host: this,
3813
+ operation: `tabs`,
3814
+ targetPath: path,
3815
+ side: `after`,
3816
+ gapIndex,
3817
+ rect: rel(leafRect),
3818
+ caretRect: this._stripCaretRect(strip, gapIndex, hostRect)
3819
+ };
3820
+ return this._zoneAllowed(panelId, zone) ? zone : null;
3821
+ }
3822
+ }
3823
+ const relX = (clientX - leafRect.left) / Math.max(leafRect.width, 1);
3824
+ const relY = (clientY - leafRect.top) / Math.max(leafRect.height, 1);
3825
+ if (relX >= BAND && relX <= .75 && relY >= BAND && relY <= .75) {
3826
+ const zone = {
3827
+ host: this,
3828
+ operation: `tabs`,
3829
+ targetPath: path,
3830
+ side: `after`,
3831
+ gapIndex: stack.panels.length,
3832
+ rect: rel(leafRect)
3833
+ };
3834
+ return this._zoneAllowed(panelId, zone) ? zone : null;
3835
+ }
3836
+ const r = rel(leafRect);
3837
+ let operation;
3838
+ let side;
3839
+ let zoneRect;
3840
+ if (relX < BAND) {
3841
+ operation = `split-vertical`;
3842
+ side = `before`;
3843
+ zoneRect = {
3844
+ ...r,
3845
+ w: r.w / 2
3846
+ };
3847
+ } else if (relX > .75) {
3848
+ operation = `split-vertical`;
3849
+ side = `after`;
3850
+ zoneRect = {
3851
+ x: r.x + r.w / 2,
3852
+ y: r.y,
3853
+ w: r.w / 2,
3854
+ h: r.h
3855
+ };
3856
+ } else if (relY < BAND) {
3857
+ operation = `split-horizontal`;
3858
+ side = `before`;
3859
+ zoneRect = {
3860
+ ...r,
3861
+ h: r.h / 2
3862
+ };
3863
+ } else {
3864
+ operation = `split-horizontal`;
3865
+ side = `after`;
3866
+ zoneRect = {
3867
+ x: r.x,
3868
+ y: r.y + r.h / 2,
3869
+ w: r.w,
3870
+ h: r.h / 2
3871
+ };
3872
+ }
3873
+ const zone = {
3874
+ host: this,
3875
+ operation,
3876
+ targetPath: path,
3877
+ side,
3878
+ rect: zoneRect
3879
+ };
3880
+ return this._zoneAllowed(panelId, zone) ? zone : null;
3881
+ }
3882
+ const fromLeft = clientX - hostRect.left;
3883
+ const fromRight = hostRect.right - clientX;
3884
+ const fromTop = clientY - hostRect.top;
3885
+ const fromBottom = hostRect.bottom - clientY;
3886
+ const min = Math.min(fromLeft, fromRight, fromTop, fromBottom);
3887
+ const full = {
3888
+ x: 0,
3889
+ y: 0,
3890
+ w: hostRect.width,
3891
+ h: hostRect.height
3892
+ };
3893
+ let operation;
3894
+ let side;
3895
+ let zoneRect;
3896
+ if (min === fromLeft) {
3897
+ operation = `split-vertical`;
3898
+ side = `before`;
3899
+ zoneRect = {
3900
+ ...full,
3901
+ w: full.w / 2
3902
+ };
3903
+ } else if (min === fromRight) {
3904
+ operation = `split-vertical`;
3905
+ side = `after`;
3906
+ zoneRect = {
3907
+ x: full.x + full.w / 2,
3908
+ y: full.y,
3909
+ w: full.w / 2,
3910
+ h: full.h
3911
+ };
3912
+ } else if (min === fromTop) {
3913
+ operation = `split-horizontal`;
3914
+ side = `before`;
3915
+ zoneRect = {
3916
+ ...full,
3917
+ h: full.h / 2
3918
+ };
3919
+ } else {
3920
+ operation = `split-horizontal`;
3921
+ side = `after`;
3922
+ zoneRect = {
3923
+ x: full.x,
3924
+ y: full.y + full.h / 2,
3925
+ w: full.w,
3926
+ h: full.h / 2
3927
+ };
3928
+ }
3929
+ const zone = {
3930
+ host: this,
3931
+ operation,
3932
+ targetPath: [],
3933
+ side,
3934
+ rect: zoneRect
3935
+ };
3936
+ return this._zoneAllowed(panelId, zone) ? zone : null;
3937
+ }
3938
+ _zoneAllowed(panelId, zone) {
3939
+ if (!tokenAllows(this.accepts, operationToToken(zone.operation))) return false;
3940
+ const stack = findNode(this._model, zone.targetPath);
3941
+ if (stack !== null && isStack(stack) && stack.panels.length === 1) {
3942
+ const target = this._findPanelEl(stack.panels[0]);
3943
+ if (target && !panelAllows(target.accepts, operationToToken(zone.operation))) return false;
3944
+ }
3945
+ if (this.canAccept && !this.canAccept(panelId, zone.operation)) return false;
3946
+ return true;
3947
+ }
3948
+ /**
3949
+ * Apply a drop. `sourceHost` differs from `this` when the drag crossed
3950
+ * hosts; the panel element is adopted into this host first.
3951
+ */
3952
+ handleDrop(panelId, zone, sourceHost) {
3953
+ if (sourceHost !== void 0 && sourceHost !== this) sourceHost.releasePanel(panelId, this);
3954
+ if (!collectPanelIds(this._model).includes(panelId)) {
3955
+ if (!this._findPanelEl(panelId)) return;
3956
+ this._appendPanelToFirstStack(panelId);
3957
+ normalize(this._model);
3958
+ }
3959
+ this._applyZone(panelId, zone);
3960
+ this._commit();
3961
+ }
3962
+ /**
3963
+ * Move `panelId` (already present somewhere in the model) to the position
3964
+ * described by `zone`. Shared by in-page drops and external panel adoption.
3965
+ */
3966
+ _applyZone(panelId, zone) {
3967
+ const sourcePath = findStackOf(this._model, panelId);
3968
+ if (sourcePath !== null) {
3969
+ const source = findNode(this._model, sourcePath);
3970
+ if (source !== null && isStack(source)) removePanelFromStack(source, panelId);
3971
+ }
3972
+ if (zone.operation === `tabs`) {
3973
+ const target = findNode(this._model, zone.targetPath);
3974
+ if (target !== null && isStack(target)) addPanelToStack(target, panelId, zone.gapIndex);
3975
+ } else insertBeside(this._model, zone.targetPath, opToDirection(zone.operation), createStack([panelId]), zone.side);
3976
+ }
3977
+ /**
3978
+ * Move a panel element out of this host into `target`, dropping it from
3979
+ * this host's model. Used for cross-host drags.
3980
+ */
3981
+ releasePanel(panelId, target) {
3982
+ this._returnAllToolbars();
3983
+ const el = this._pool.find((p) => p.panelId === panelId);
3984
+ const stackPath = findStackOf(this._model, panelId);
3985
+ if (stackPath !== null) {
3986
+ const stack = findNode(this._model, stackPath);
3987
+ if (stack !== null && isStack(stack)) removePanelFromStack(stack, panelId);
3988
+ }
3989
+ normalize(this._model);
3990
+ this._pool = this._pool.filter((p) => p !== el);
3991
+ if (el !== void 0) target.adoptPanelElement(el);
3992
+ this.requestUpdate();
3993
+ }
3994
+ /** Adopt a panel element from another host (the one sanctioned reparent). */
3995
+ adoptPanelElement(el) {
3996
+ this.appendChild(el);
3997
+ if (!this._pool.some((p) => p.panelId === el.panelId)) this._pool = [...this._pool, el];
3998
+ this._appendPanelToFirstStack(el.panelId);
3999
+ normalize(this._model);
4000
+ this.dispatchEvent(new CustomEvent(`dock-panel-added`, {
4001
+ detail: { panelId: el.panelId },
4002
+ bubbles: true,
4003
+ composed: true
4004
+ }));
4005
+ }
4006
+ setDragActive(active) {
4007
+ if (this._dragActive !== active) {
4008
+ this._dragActive = active;
4009
+ this.requestUpdate();
4010
+ }
4011
+ }
4012
+ setDropZone(zone) {
4013
+ if (this._dropZone !== zone) {
4014
+ this._dropZone = zone;
4015
+ this.requestUpdate();
4016
+ }
4017
+ }
4018
+ _scheduleJunctions() {
4019
+ if (this._junctionsRaf !== null) return;
4020
+ this._junctionsRaf = requestAnimationFrame(() => {
4021
+ this._junctionsRaf = null;
4022
+ this._computeJunctions();
4023
+ });
4024
+ }
4025
+ /**
4026
+ * Find points where a parent split's gutter line meets a nested split's
4027
+ * gutter (T and + crossings). Each junction handle starts both gutter drags
4028
+ * at once, like a corner resize in VS Code.
4029
+ */
4030
+ _computeJunctions() {
4031
+ const shadow = this.shadowRoot;
4032
+ if (shadow === null) return;
4033
+ const hostRect = this.getBoundingClientRect();
4034
+ const found = [];
4035
+ for (const child of shadow.querySelectorAll(`ixfx-split-layout`)) {
4036
+ const parent = child.parentElement?.closest(`ixfx-split-layout`) ?? null;
4037
+ if (parent === null) continue;
4038
+ if (typeof child.getGutterRects !== `function` || typeof parent.getGutterRects !== `function`) continue;
4039
+ const childRect = child.getBoundingClientRect();
4040
+ for (const [pi, pg] of parent.getGutterRects().entries()) for (const [ci, cg] of child.getGutterRects().entries()) {
4041
+ const pgVertical = pg.width <= pg.height;
4042
+ if (pgVertical === cg.width <= cg.height) continue;
4043
+ let x;
4044
+ let y;
4045
+ if (pgVertical) {
4046
+ x = pg.left + pg.width / 2;
4047
+ y = cg.top + cg.height / 2;
4048
+ if (Math.abs(x - childRect.left) > JUNCTION_TOL_PX && Math.abs(x - childRect.right) > JUNCTION_TOL_PX) continue;
4049
+ if (y < pg.top || y > pg.bottom) continue;
4050
+ } else {
4051
+ y = pg.top + pg.height / 2;
4052
+ x = cg.left + cg.width / 2;
4053
+ if (Math.abs(y - childRect.top) > JUNCTION_TOL_PX && Math.abs(y - childRect.bottom) > JUNCTION_TOL_PX) continue;
4054
+ if (x < pg.left || x > pg.right) continue;
4055
+ }
4056
+ found.push({
4057
+ x: x - hostRect.left,
4058
+ y: y - hostRect.top,
4059
+ targets: [{
4060
+ split: parent,
4061
+ gutterIndex: pi
4062
+ }, {
4063
+ split: child,
4064
+ gutterIndex: ci
4065
+ }]
4066
+ });
4067
+ }
4068
+ }
4069
+ const merged = [];
4070
+ for (const j of found) {
4071
+ const near = merged.find((m) => Math.abs(m.x - j.x) <= JUNCTION_MERGE_PX && Math.abs(m.y - j.y) <= JUNCTION_MERGE_PX);
4072
+ if (near === void 0) {
4073
+ merged.push(j);
4074
+ continue;
4075
+ }
4076
+ for (const t of j.targets) if (!near.targets.some((o) => o.split === t.split && o.gutterIndex === t.gutterIndex)) near.targets.push(t);
4077
+ }
4078
+ const key = merged.map((j) => `${Math.round(j.x)},${Math.round(j.y)}:${j.targets.map((t) => `${t.split.getAttribute(`data-dock-path`) ?? ``}#${t.gutterIndex}`).join(`|`)}`).join(`;`);
4079
+ if (key === this._junctionsKey) return;
4080
+ this._junctionsKey = key;
4081
+ this._junctions = merged;
4082
+ }
4083
+ /** Fold live weights from each rendered `ixfx-split-layout` into the model. */
4084
+ _foldLiveWeights() {
4085
+ const splits = this.shadowRoot?.querySelectorAll(`ixfx-split-layout[data-dock-path]`);
4086
+ if (splits === void 0) return;
4087
+ for (const el of splits) {
4088
+ const pathStr = el.getAttribute(`data-dock-path`);
4089
+ if (pathStr === null) continue;
4090
+ const node = findNode(this._model, parsePath(pathStr));
4091
+ if (node === null || !isSplit(node)) continue;
4092
+ const live = Object.values(el.getSizes());
4093
+ if (live.length === node.children.length && !weightsEqual(live, node.sizes)) node.sizes = live;
4094
+ }
4095
+ }
4096
+ _teardownTabDragListeners() {
4097
+ window.removeEventListener(`pointermove`, this._onTabDragMove);
4098
+ window.removeEventListener(`pointerup`, this._onTabDragUp);
4099
+ window.removeEventListener(`pointercancel`, this._onTabDragUp);
4100
+ }
4101
+ /** After a dock drag from a tab/titlebar, swallow the trailing click so it cannot re-select or toggle. */
4102
+ _suppressNextClick() {
4103
+ const swallow = (e) => {
4104
+ e.preventDefault();
4105
+ e.stopPropagation();
4106
+ window.removeEventListener(`click`, swallow, true);
4107
+ };
4108
+ window.addEventListener(`click`, swallow, true);
4109
+ }
4110
+ _splitStack(path, op) {
4111
+ const stack = findNode(this._model, path);
4112
+ if (stack === null || !isStack(stack) || stack.panels.length === 0) return;
4113
+ const activeId = stack.active;
4114
+ this._returnAllToolbars();
4115
+ removePanelFromStack(stack, activeId);
4116
+ normalize(this._model);
4117
+ insertBeside(this._model, path, opToDirection(op), createStack([activeId]), `after`);
4118
+ this._commit();
4119
+ }
4120
+ _removeFromModel(panelId) {
4121
+ const stackPath = findStackOf(this._model, panelId);
4122
+ if (stackPath === null) return;
4123
+ const stack = findNode(this._model, stackPath);
4124
+ if (stack !== null && isStack(stack)) removePanelFromStack(stack, panelId);
4125
+ normalize(this._model);
4126
+ }
4127
+ /** Normalize, re-render, and announce the structural change. */
4128
+ _commit() {
4129
+ normalize(this._model);
4130
+ this.requestUpdate();
4131
+ this._emitChange();
4132
+ }
4133
+ _syncSlots() {
4134
+ const inModel = /* @__PURE__ */ new Set();
4135
+ const visit = (node) => {
4136
+ if (isStack(node)) for (const id of node.panels) {
4137
+ inModel.add(id);
4138
+ const el = this._findPanelEl(id);
4139
+ if (el === void 0) continue;
4140
+ el.setAttribute(`slot`, `dock-panel-${id}`);
4141
+ el.presentation = `tabbed`;
4142
+ }
4143
+ else node.children.forEach(visit);
4144
+ };
4145
+ visit(this._model);
4146
+ for (const el of this._pool) if (!inModel.has(el.panelId)) {
4147
+ el.setAttribute(`slot`, ``);
4148
+ el.presentation = `solo`;
4149
+ }
4150
+ }
4151
+ /**
4152
+ * Per-panel toolbar routing: docked panels render content-only, so the
4153
+ * active panel's toolbar nodes live in its generated tab item (shown
4154
+ * right-aligned while active). Node moves preserve element state. Panels
4155
+ * outside the model keep their toolbar in their own (solo) chrome.
4156
+ */
4157
+ _syncToolbars() {
4158
+ const wanted = /* @__PURE__ */ new Map();
4159
+ const visit = (node) => {
4160
+ if (isStack(node)) {
4161
+ if (node.panels.length > 0 && node.active) {
4162
+ const el = this._findPanelEl(node.active);
4163
+ if (el !== void 0) wanted.set(node.active, toolbarNodesOf(el));
4164
+ }
4165
+ } else node.children.forEach(visit);
4166
+ };
4167
+ visit(this._model);
4168
+ for (const [panelId, nodes] of this._toolbarsOut) if (!wanted.has(panelId)) this._returnToolbar(panelId, nodes);
4169
+ for (const [panelId, nodes] of wanted) {
4170
+ if (this._toolbarsOut.has(panelId) || nodes.length === 0) continue;
4171
+ const item = this.shadowRoot?.getElementById(panelId);
4172
+ if (item === null || item === void 0) continue;
4173
+ for (const node of nodes) item.appendChild(node);
4174
+ this._toolbarsOut.set(panelId, nodes);
4175
+ }
4176
+ }
4177
+ _returnToolbar(panelId, nodes) {
4178
+ const el = this._findPanelEl(panelId);
4179
+ if (el !== void 0) {
4180
+ for (const node of nodes) if (node.isConnected) el.appendChild(node);
4181
+ }
4182
+ this._toolbarsOut.delete(panelId);
4183
+ }
4184
+ _returnAllToolbars() {
4185
+ for (const [panelId, nodes] of [...this._toolbarsOut]) this._returnToolbar(panelId, nodes);
4186
+ }
4187
+ _emitChange() {
4188
+ this.dispatchEvent(new CustomEvent(`dock-change`, {
4189
+ detail: { layout: this.saveLayout() },
4190
+ bubbles: true,
4191
+ composed: true
4192
+ }));
4193
+ }
4194
+ render() {
4195
+ return html`
4196
+ <div class="root">
4197
+ ${this._renderNode(this._model, [])}
4198
+ </div>
4199
+ <div class="overlay" ?hidden=${!this._dragActive}>
4200
+ ${this._dropZone === null ? nothing : html`
4201
+ <div class="zone-rect" style=${rectStyle(this._dropZone.rect)}></div>
4202
+ ${this._dropZone.caretRect === void 0 ? nothing : html`<div class="zone-caret" style=${rectStyle(this._dropZone.caretRect)}></div>`}
4203
+ `}
4204
+ </div>
4205
+ <div class="junctions" ?hidden=${this._dragActive}>
4206
+ ${this._junctions.map((junction) => html`
4207
+ <div
4208
+ class="junction-handle"
4209
+ style=${`left:${junction.x}px; top:${junction.y}px;`}
4210
+ role="separator"
4211
+ aria-label="Resize adjacent panels"
4212
+ @pointerdown=${(e) => this._onJunctionPointerDown(e, junction)}
4213
+ ></div>
4214
+ `)}
4215
+ </div>
4216
+ <slot @slotchange=${this._onPoolSlotChange}></slot>
4217
+ `;
4218
+ }
4219
+ _renderNode(node, path, size) {
4220
+ if (isStack(node)) return this._renderLeaf(node, path, size);
4221
+ return this._renderSplit(node, path, size);
4222
+ }
4223
+ _renderSplit(node, path, size) {
4224
+ const pathStr = path.join(`-`);
4225
+ return html`
4226
+ <ixfx-split-layout
4227
+ class="split"
4228
+ direction=${node.direction}
4229
+ size=${size === void 0 ? nothing : String(size)}
4230
+ style=${`--split-gutter-size: var(--dock-split-bar-size, 2px)`}
4231
+ data-dock-path=${pathStr}
4232
+ @ixfx-split-resize=${this._onSplitWeightsChanged}
4233
+ @ixfx-split-change=${this._onSplitWeightsChanged}
4234
+ @ixfx-split-collapse=${(e) => this._onSplitCollapsed(e, true)}
4235
+ @ixfx-split-expand=${(e) => this._onSplitCollapsed(e, false)}
4236
+ @ixfx-split-close=${this._onSplitClose}
4237
+ >
4238
+ ${node.children.map((child, i) => this._renderNode(child, [...path, i], node.sizes[i]))}
4239
+ </ixfx-split-layout>
4240
+ `;
4241
+ }
4242
+ _renderLeaf(stack, path, size) {
4243
+ const pathStr = path.join(`-`);
4244
+ const collapsible = stack.panels.length === 1 ? this._findPanelEl(stack.panels[0])?.collapsible ?? false : false;
4245
+ const leafAttrs = (content) => html`
4246
+ <div
4247
+ id=${`dock-leaf-${pathStr}`}
4248
+ class="leaf"
4249
+ data-dock-path=${pathStr}
4250
+ size=${size === void 0 ? nothing : String(size)}
4251
+ min=${collapsible ? `1px` : nothing}
4252
+ when-small=${collapsible ? `collapse` : nothing}
4253
+ ?collapsed=${stack.collapsed === true}
4254
+ >${content}</div>
4255
+ `;
4256
+ if (stack.panels.length === 0) return leafAttrs(html`
4257
+ <div class="empty" part="empty">
4258
+ <slot name="empty"><span>Drop panels here</span></slot>
4259
+ </div>
4260
+ `);
4261
+ return leafAttrs(html`
4262
+ <ixfx-tab-list
4263
+ class="strip"
4264
+ orientation="horizontal"
4265
+ @change=${(e) => this._onTabChange(e, path)}
4266
+ @pointerdown=${this._onStripPointerDown}
4267
+ >
4268
+ ${repeat(stack.panels, (id) => id, (id) => this._renderTabItem(stack, id))}
4269
+ <div slot="toolbar" class="group-toolbar">
4270
+ <button
4271
+ class="chrome-btn"
4272
+ title="Split horizontally (top/bottom)"
4273
+ @click=${() => this._splitStack(path, `split-horizontal`)}
4274
+ >↕</button>
4275
+ <button
4276
+ class="chrome-btn"
4277
+ title="Split vertically (left/right)"
4278
+ @click=${() => this._splitStack(path, `split-vertical`)}
4279
+ >↔</button>
4280
+ </div>
4281
+ </ixfx-tab-list>
4282
+ <div class="leaf-body">
4283
+ <slot name="dock-panel-${stack.active}"></slot>
4284
+ </div>
4285
+ `);
4286
+ }
4287
+ _renderTabItem(stack, panelId) {
4288
+ const el = this._findPanelEl(panelId);
4289
+ return html`
4290
+ <ixfx-tab-list-item
4291
+ id=${panelId}
4292
+ ?selected=${panelId === stack.active}
4293
+ .iconName=${el?.iconName ?? ``}
4294
+ .closeable=${el?.closable ?? false}
4295
+ @close=${() => this.removePanel(panelId)}
4296
+ >${el?.title ?? panelId}</ixfx-tab-list-item>
4297
+ `;
4298
+ }
4299
+ _stripCaretRect(strip, gapIndex, hostRect) {
4300
+ const items = Array.from(strip.querySelectorAll(`ixfx-tab-list-item`));
4301
+ if (items.length === 0) return void 0;
4302
+ const index = Math.max(0, Math.min(gapIndex ?? items.length, items.length));
4303
+ const edge = index < items.length ? items[index].getBoundingClientRect().left : items[items.length - 1].getBoundingClientRect().right;
4304
+ const stripRect = strip.getBoundingClientRect();
4305
+ return {
4306
+ x: edge - hostRect.left - 1,
4307
+ y: stripRect.top - hostRect.top,
4308
+ w: 2,
4309
+ h: stripRect.height
4310
+ };
4311
+ }
4312
+ static {
4313
+ this.styles = [themeFallbacks, css`
4314
+ :host {
4315
+ display: block;
4316
+ width: 100%;
4317
+ height: 100%;
4318
+ --dock-drop-zone-color: var(--accent);
4319
+ --dock-drop-zone-opacity: 0.25;
4320
+ --dock-collapsed-size: 40px;
4321
+ --dock-split-bar-size: 2px;
4322
+ position: relative;
4323
+ }
2079
4324
 
2080
- /* ── Circular host sizing ── */
2081
- :host([fill-style="circular"][circular-style="grow"]),
2082
- :host([fill-style="circular"]:not([circular-style])) {
2083
- aspect-ratio: 1;
2084
- }
2085
- `
2086
- ];
4325
+ .root {
4326
+ width: 100%;
4327
+ height: 100%;
4328
+ }
4329
+
4330
+ ixfx-split-layout.split {
4331
+ width: 100%;
4332
+ height: 100%;
4333
+ }
4334
+
4335
+ .leaf {
4336
+ display: flex;
4337
+ flex-direction: column;
4338
+ width: 100%;
4339
+ height: 100%;
4340
+ min-width: 0;
4341
+ min-height: 0;
4342
+ box-sizing: border-box;
4343
+ overflow: hidden;
4344
+ border: 1px solid var(--surface-4);
4345
+ border-radius: var(--radius-m);
4346
+ }
4347
+
4348
+ .leaf[collapsed] {
4349
+ pointer-events: none;
4350
+ }
4351
+
4352
+ .leaf[collapsed] .leaf-body,
4353
+ .leaf[collapsed] .empty {
4354
+ visibility: hidden;
4355
+ }
4356
+
4357
+ .strip {
4358
+ flex-shrink: 0;
4359
+ border: 0;
4360
+ /* The leaf background shows through selected tabs; the accent
4361
+ underline indicator carries the selection. */
4362
+ --ixfx-tab-selected-background: transparent;
4363
+ }
4364
+
4365
+ /* Uniform tab height: a panel toolbar button would otherwise inflate
4366
+ its tab item, giving strips of different stacks different heights. */
4367
+ .strip ixfx-tab-list-item {
4368
+ height: var(--dock-tab-height, 30px);
4369
+ }
4370
+
4371
+ .leaf-body {
4372
+ flex: 1;
4373
+ min-height: 0;
4374
+ min-width: 0;
4375
+ box-sizing: border-box;
4376
+ padding: var(--space-s);
4377
+ }
4378
+
4379
+ .empty {
4380
+ flex: 1;
4381
+ display: flex;
4382
+ align-items: center;
4383
+ justify-content: center;
4384
+ color: var(--surface-3-text);
4385
+ opacity: 0.6;
4386
+ font-size: var(--text-m);
4387
+ border: 1px dashed var(--border-subtle);
4388
+ border-radius: var(--radius-m);
4389
+ margin: var(--space-s);
4390
+ }
4391
+
4392
+ .group-toolbar {
4393
+ display: flex;
4394
+ gap: 2px;
4395
+ padding: 0 var(--space-xs);
4396
+ }
4397
+
4398
+ .chrome-btn {
4399
+ display: inline-flex;
4400
+ align-items: center;
4401
+ justify-content: center;
4402
+ width: 1.6em;
4403
+ height: 1.6em;
4404
+ padding: 0;
4405
+ background: none;
4406
+ border: none;
4407
+ border-radius: var(--radius-s);
4408
+ color: inherit;
4409
+ cursor: pointer;
4410
+ opacity: var(--prominence-idle-opacity);
4411
+ transition: background var(--transition), opacity var(--transition);
4412
+ }
4413
+
4414
+ .chrome-btn:hover {
4415
+ background: var(--surface-5);
4416
+ opacity: var(--prominence-tickled-opacity);
4417
+ }
4418
+
4419
+ .chrome-btn:focus-visible {
4420
+ outline: 2px solid var(--accent);
4421
+ outline-offset: 1px;
4422
+ }
4423
+
4424
+ .overlay {
4425
+ position: absolute;
4426
+ inset: 0;
4427
+ pointer-events: none;
4428
+ z-index: 100;
4429
+ }
4430
+
4431
+ .zone-rect {
4432
+ position: absolute;
4433
+ background: var(--dock-drop-zone-color);
4434
+ opacity: var(--dock-drop-zone-opacity);
4435
+ border-radius: var(--radius-s);
4436
+ }
4437
+
4438
+ .zone-caret {
4439
+ position: absolute;
4440
+ background: var(--dock-drop-zone-color);
4441
+ border-radius: 1px;
4442
+ }
4443
+
4444
+ .junctions {
4445
+ position: absolute;
4446
+ inset: 0;
4447
+ pointer-events: none;
4448
+ z-index: 99;
4449
+ }
4450
+
4451
+ .junctions[hidden] {
4452
+ display: none;
4453
+ }
4454
+
4455
+ .junction-handle {
4456
+ position: absolute;
4457
+ width: 14px;
4458
+ height: 14px;
4459
+ transform: translate(-50%, -50%);
4460
+ border-radius: 50%;
4461
+ cursor: nwse-resize;
4462
+ pointer-events: auto;
4463
+ }
4464
+
4465
+ .junction-handle::after {
4466
+ content: '';
4467
+ position: absolute;
4468
+ inset: 4px;
4469
+ border-radius: 50%;
4470
+ background: var(--dock-junction-color, var(--accent));
4471
+ opacity: 0;
4472
+ transition: opacity var(--transition);
4473
+ }
4474
+
4475
+ .junction-handle:hover::after {
4476
+ opacity: 0.9;
4477
+ }
4478
+
4479
+ slot:not([name]) {
4480
+ display: none;
4481
+ }
4482
+ `];
2087
4483
  }
2088
4484
  };
2089
- __decorate([property({ type: Number })], EditableNumber.prototype, "min", void 0);
2090
- __decorate([property({ type: Number })], EditableNumber.prototype, "max", void 0);
2091
- __decorate([property({ type: Number })], EditableNumber.prototype, "step", void 0);
2092
- __decorate([property({ type: Number })], EditableNumber.prototype, "stops", void 0);
2093
4485
  __decorate([property({
2094
- type: Number,
2095
- attribute: `drag-sensitivity`
2096
- })], EditableNumber.prototype, "dragSensitivity", void 0);
2097
- __decorate([property({ attribute: `fill-style` })], EditableNumber.prototype, "fillStyle", void 0);
2098
- __decorate([property({ attribute: `linear-style` })], EditableNumber.prototype, "linearStyle", void 0);
2099
- __decorate([property({ attribute: `circular-style` })], EditableNumber.prototype, "circularStyle", void 0);
4486
+ type: String,
4487
+ attribute: `layout`
4488
+ })], DockHostElement.prototype, "layout", void 0);
2100
4489
  __decorate([property({
2101
- type: Boolean,
2102
- attribute: `no-drag`
2103
- })], EditableNumber.prototype, "noDrag", void 0);
2104
- __decorate([property({ type: Number })], EditableNumber.prototype, "precision", void 0);
2105
- EditableNumber = __decorate([safeCustomElement(`ixfx-editable-number`)], EditableNumber);
4490
+ type: String,
4491
+ attribute: `accepts`
4492
+ })], DockHostElement.prototype, "accepts", void 0);
4493
+ __decorate([property({ attribute: false })], DockHostElement.prototype, "canAccept", void 0);
4494
+ __decorate([state()], DockHostElement.prototype, "_model", void 0);
4495
+ __decorate([state()], DockHostElement.prototype, "_pool", void 0);
4496
+ __decorate([state()], DockHostElement.prototype, "_dropZone", void 0);
4497
+ __decorate([state()], DockHostElement.prototype, "_dragActive", void 0);
4498
+ __decorate([state()], DockHostElement.prototype, "_junctions", void 0);
4499
+ DockHostElement = __decorate([safeCustomElement(`ixfx-dock-host`)], DockHostElement);
4500
+ /** `split-horizontal` = horizontal divider = panels stacked vertically. */
4501
+ function opToDirection(op) {
4502
+ return op === `split-horizontal` ? `column` : `row`;
4503
+ }
4504
+ function operationToToken(op) {
4505
+ if (op === `tabs`) return `tabs`;
4506
+ return op === `split-horizontal` ? `horizontal` : `vertical`;
4507
+ }
4508
+ /** Host-level `accepts`: empty or `none` rejects everything. */
4509
+ function tokenAllows(tokens, token) {
4510
+ const list = tokens.trim().split(/\s+/).filter((t) => t.length > 0);
4511
+ if (list.length === 0 || list.length === 1 && list[0] === `none`) return false;
4512
+ return list.includes(`all`) || list.includes(token);
4513
+ }
4514
+ /** Panel-level `accepts`: empty means unrestricted (host rules still apply). */
4515
+ function panelAllows(tokens, token) {
4516
+ const list = tokens.trim().split(/\s+/).filter((t) => t.length > 0);
4517
+ if (list.length === 0) return true;
4518
+ if (list.length === 1 && list[0] === `none`) return false;
4519
+ return list.includes(`all`) || list.includes(token);
4520
+ }
4521
+ function weightsEqual(a, b) {
4522
+ if (a.length !== b.length) return false;
4523
+ return a.every((v, i) => Math.abs(v - b[i]) < 1e-4);
4524
+ }
4525
+ function rectStyle(r) {
4526
+ return `left:${r.x}px; top:${r.y}px; width:${r.w}px; height:${r.h}px;`;
4527
+ }
4528
+ /** Strip a live `DockDropZone` down to its serializable {@link DockDropTarget}. */
4529
+ function zoneToTarget(zone) {
4530
+ return {
4531
+ operation: zone.operation,
4532
+ targetPath: [...zone.targetPath],
4533
+ side: zone.side,
4534
+ ...zone.gapIndex === void 0 ? {} : { gapIndex: zone.gapIndex }
4535
+ };
4536
+ }
4537
+ function cloneNode(node) {
4538
+ return JSON.parse(JSON.stringify(node));
4539
+ }
4540
+ function parsePath(pathStr) {
4541
+ return pathStr.length === 0 ? [] : pathStr.split(`-`).filter((s) => s.length > 0).map(Number);
4542
+ }
4543
+ /** Toolbar-slot light children of a dock panel. */
4544
+ function toolbarNodesOf(el) {
4545
+ return Array.from(el.children).filter((c) => c instanceof HTMLElement && c.getAttribute(`slot`) === `toolbar`);
4546
+ }
4547
+ /** Nearest strip gap to the pointer (reuses the tab-list midpoint rule). */
4548
+ function computeStripGapIndex(strip, clientX, _clientY) {
4549
+ const items = Array.from(strip.querySelectorAll(`ixfx-tab-list-item`));
4550
+ if (items.length === 0) return 0;
4551
+ for (let i = 0; i < items.length; i++) {
4552
+ const r = items[i].getBoundingClientRect();
4553
+ if (clientX < (r.left + r.right) / 2) return i;
4554
+ }
4555
+ return items.length;
4556
+ }
2106
4557
  //#endregion
2107
4558
  //#region src/form/form-group.ts
2108
4559
  let FormGroupElement = class FormGroupElement extends LitElement {
@@ -2766,6 +5217,7 @@ let PanelGroupElement = class PanelGroupElement extends LitElement {
2766
5217
  this.orientation = `horizontal`;
2767
5218
  this.variant = `titlebar`;
2768
5219
  this.toggleInteraction = `implicit`;
5220
+ this.collapsible = true;
2769
5221
  this.allowMultiple = false;
2770
5222
  this.gap = `var(--space-m)`;
2771
5223
  }
@@ -2782,7 +5234,7 @@ let PanelGroupElement = class PanelGroupElement extends LitElement {
2782
5234
  }
2783
5235
  updated(changed) {
2784
5236
  super.updated(changed);
2785
- if (changed.has(`variant`) || changed.has(`toggleInteraction`) || changed.has(`orientation`)) this.updateComplete.then(() => {
5237
+ if (changed.has(`variant`) || changed.has(`toggleInteraction`) || changed.has(`orientation`) || changed.has(`collapsible`)) this.updateComplete.then(() => {
2786
5238
  this._propagateAll();
2787
5239
  });
2788
5240
  }
@@ -2792,6 +5244,8 @@ let PanelGroupElement = class PanelGroupElement extends LitElement {
2792
5244
  panel.setAttribute(`variant`, this.variant);
2793
5245
  panel.setAttribute(`toggle-interaction`, this.toggleInteraction);
2794
5246
  panel.setAttribute(`orientation`, invertedOrientation);
5247
+ if (this.collapsible) panel.setAttribute(`collapsible`, ``);
5248
+ else panel.removeAttribute(`collapsible`);
2795
5249
  }
2796
5250
  }
2797
5251
  render() {
@@ -2822,132 +5276,31 @@ let PanelGroupElement = class PanelGroupElement extends LitElement {
2822
5276
  ::slotted(ixfx-panel) {
2823
5277
  flex-shrink: 1;
2824
5278
  }
2825
-
2826
- :host([orientation="horizontal"]) ::slotted(ixfx-panel) {
2827
- flex-basis: content;
2828
- min-width: 0;
2829
- }
2830
- `;
2831
- }
2832
- };
2833
- __decorate([property({
2834
- type: String,
2835
- reflect: true
2836
- })], PanelGroupElement.prototype, "orientation", void 0);
2837
- __decorate([property({ type: String })], PanelGroupElement.prototype, "variant", void 0);
2838
- __decorate([property({
2839
- type: String,
2840
- attribute: `toggle-interaction`
2841
- })], PanelGroupElement.prototype, "toggleInteraction", void 0);
2842
- __decorate([property({
2843
- type: Boolean,
2844
- attribute: `allow-multiple`
2845
- })], PanelGroupElement.prototype, "allowMultiple", void 0);
2846
- __decorate([property()], PanelGroupElement.prototype, "gap", void 0);
2847
- __decorate([queryAssignedElements({ slot: `` })], PanelGroupElement.prototype, "_panels", void 0);
2848
- PanelGroupElement = __decorate([safeCustomElement(`ixfx-panel-group`)], PanelGroupElement);
2849
- //#endregion
2850
- //#region src/util/is-numeric.ts
2851
- function isNumeric(str) {
2852
- if (typeof str === `number`) return true;
2853
- if (typeof str !== `string`) return false;
2854
- const trimmed = str.trim();
2855
- return trimmed !== `` && !Number.isNaN(Number(trimmed));
2856
- }
2857
- //#endregion
2858
- //#region src/util/tooltip.ts
2859
- var _TooltipElement;
2860
- let _baseStylesInjected = false;
2861
- function ensureBaseTooltipStyles() {
2862
- if (_baseStylesInjected) return;
2863
- _baseStylesInjected = true;
2864
- const style = document.createElement(`style`);
2865
- style.textContent = `
2866
- ixfx-tooltip {
2867
- display: inline-block;
2868
- background: var(--surface-5);
2869
- color: var(--surface-5-text);
2870
- border: var(--border);
2871
- border-radius: var(--radius-l, 6px);
2872
- box-shadow: var(--shadow-m);
2873
- padding: var(--space-s, 4px);
2874
- font-size: 10px;
2875
- font-family: var(--font-family, inherit);
2876
- }
2877
- ixfx-tooltip .tooltip-type-number {
2878
- font-family: var(--font-mono, ui-monospace, monospace);
2879
- }
2880
- `;
2881
- document.head.appendChild(style);
2882
- }
2883
- let TooltipElement = class TooltipElement extends LitElement {
2884
- static {
2885
- _TooltipElement = this;
2886
- }
2887
- constructor(..._args) {
2888
- super(..._args);
2889
- this.valueFormatTooltip = _TooltipElement.defaultFormatter;
2890
- this.orientation = `top`;
2891
- this.anchor = `--tooltip`;
2892
- }
2893
- connectedCallback() {
2894
- super.connectedCallback();
2895
- ensureBaseTooltipStyles();
2896
- }
2897
- static {
2898
- this.numberFormatter = (v) => Math.floor(v).toString();
2899
- }
2900
- static {
2901
- this.defaultFormatter = (v) => {
2902
- if (typeof v === `number`) {
2903
- if (Number.isInteger(v)) return v.toString();
2904
- return v.toFixed(2);
2905
- } else if (typeof v === `string`) return v;
2906
- else return JSON.stringify(v);
2907
- };
2908
- }
2909
- #renderIcon() {
2910
- if (!this.icon) return nothing;
2911
- if (this.icon.startsWith(`<svg`)) return html`<span class="tooltip-icon" style="display:inline-flex;align-items:center;margin-right:0.25em">${unsafeHTML(this.icon)}</span>`;
2912
- if (!getIcon(this.icon)) return nothing;
2913
- return html`<ixfx-icon class="tooltip-icon" style="margin-right:0.25em" name="${this.icon}"></ixfx-icon>`;
2914
- }
2915
- render() {
2916
- const classes = {
2917
- "tooltip": true,
2918
- "tooltip-orientation-top": this.orientation === `top`,
2919
- "tooltip-orientation-bottom": this.orientation === `bottom`,
2920
- "tooltip-orientation-right": this.orientation === `right`,
2921
- "tooltip-orientation-left": this.orientation === `left`,
2922
- "tooltip-type-text": true,
2923
- "tooltip-type-number": false,
2924
- "tooltip-style-bubble": true
2925
- };
2926
- let value = ``;
2927
- switch (typeof this.value) {
2928
- case `number`:
2929
- classes[`tooltip-type-number`] = true;
2930
- value = this.valueFormatTooltip(this.value);
2931
- break;
2932
- case `string`:
2933
- if (isNumeric(this.value)) {
2934
- classes[`tooltip-type-number`] = true;
2935
- value = this.valueFormatTooltip(Number.parseFloat(this.value));
2936
- } else value = this.value;
2937
- break;
2938
- default: value = JSON.stringify(this.value);
2939
- }
2940
- return html`<output style="--anchor-id: ${this.anchor}" class=${classMap(classes)}>${this.#renderIcon()}${value}</output>`;
2941
- }
2942
- createRenderRoot() {
2943
- return this;
5279
+
5280
+ :host([orientation="horizontal"]) ::slotted(ixfx-panel) {
5281
+ flex-basis: content;
5282
+ min-width: 0;
5283
+ }
5284
+ `;
2944
5285
  }
2945
5286
  };
2946
- __decorate([property({ type: String })], TooltipElement.prototype, "orientation", void 0);
2947
- __decorate([property({ type: String })], TooltipElement.prototype, "anchor", void 0);
2948
- __decorate([property()], TooltipElement.prototype, "value", void 0);
2949
- __decorate([property({ type: String })], TooltipElement.prototype, "icon", void 0);
2950
- TooltipElement = _TooltipElement = __decorate([safeCustomElement(`ixfx-tooltip`)], TooltipElement);
5287
+ __decorate([property({
5288
+ type: String,
5289
+ reflect: true
5290
+ })], PanelGroupElement.prototype, "orientation", void 0);
5291
+ __decorate([property({ type: String })], PanelGroupElement.prototype, "variant", void 0);
5292
+ __decorate([property({
5293
+ type: String,
5294
+ attribute: `toggle-interaction`
5295
+ })], PanelGroupElement.prototype, "toggleInteraction", void 0);
5296
+ __decorate([property({ type: Boolean })], PanelGroupElement.prototype, "collapsible", void 0);
5297
+ __decorate([property({
5298
+ type: Boolean,
5299
+ attribute: `allow-multiple`
5300
+ })], PanelGroupElement.prototype, "allowMultiple", void 0);
5301
+ __decorate([property()], PanelGroupElement.prototype, "gap", void 0);
5302
+ __decorate([queryAssignedElements({ slot: `` })], PanelGroupElement.prototype, "_panels", void 0);
5303
+ PanelGroupElement = __decorate([safeCustomElement(`ixfx-panel-group`)], PanelGroupElement);
2951
5304
  //#endregion
2952
5305
  //#region src/util/drag.ts
2953
5306
  function getCoordinates(event) {
@@ -4006,278 +6359,54 @@ let SlidingNumberElement = class SlidingNumberElement extends LitElement {
4006
6359
  opacity: 0;
4007
6360
  }
4008
6361
 
4009
- .cell.digit[data-phase='enter'],
4010
- .cell.digit[data-phase='leave'] {
4011
- width: 0;
4012
- }
4013
-
4014
- .cell.symbol[data-phase='enter'],
4015
- .cell.symbol[data-phase='leave'] {
4016
- max-width: 0;
4017
- }
4018
-
4019
- .reel {
4020
- position: absolute;
4021
- inset: 0 0 auto 0;
4022
- display: flex;
4023
- flex-direction: column;
4024
- will-change: transform;
4025
- transform: translateY(calc(-1em * var(--o, 10)));
4026
- }
4027
-
4028
- .reel > span {
4029
- display: block;
4030
- height: 1em;
4031
- line-height: 1;
4032
- text-align: center;
4033
- }
4034
-
4035
- :host([data-static]) .cell {
4036
- transition: none;
4037
- }
4038
- `;
4039
- }
4040
- };
4041
- __decorate([property({ type: Number })], SlidingNumberElement.prototype, "value", void 0);
4042
- __decorate([property({ type: String })], SlidingNumberElement.prototype, "locales", void 0);
4043
- __decorate([property({ type: Object })], SlidingNumberElement.prototype, "format", void 0);
4044
- __decorate([property({ type: String })], SlidingNumberElement.prototype, "prefix", void 0);
4045
- __decorate([property({ type: String })], SlidingNumberElement.prototype, "suffix", void 0);
4046
- __decorate([property({ type: String })], SlidingNumberElement.prototype, "trend", void 0);
4047
- __decorate([property({
4048
- type: Boolean,
4049
- attribute: `no-animate`
4050
- })], SlidingNumberElement.prototype, "noAnimate", void 0);
4051
- __decorate([property({
4052
- type: Boolean,
4053
- attribute: false
4054
- })], SlidingNumberElement.prototype, "respectMotionPreference", void 0);
4055
- __decorate([state()], SlidingNumberElement.prototype, "_cells", void 0);
4056
- SlidingNumberElement = __decorate([safeCustomElement(`ixfx-sliding-number`)], SlidingNumberElement);
4057
- //#endregion
4058
- //#region src/tabs/tab-list-item.ts
4059
- let TabListItemElement = class TabListItemElement extends LitElement {
4060
- constructor(..._args) {
4061
- super(..._args);
4062
- this.closeable = false;
4063
- this.hideLabels = false;
4064
- this.iconName = void 0;
4065
- this.description = void 0;
4066
- this.commandActive = void 0;
4067
- this.registry = void 0;
4068
- this._compact = false;
4069
- this._hasToolbarContent = false;
4070
- this._onToolbarSlotChange = () => {
4071
- this._updateToolbarContent();
4072
- };
4073
- }
4074
- connectedCallback() {
4075
- super.connectedCallback();
4076
- this._resizeObserver = new ResizeObserver(([entry]) => {
4077
- this._compact = entry.contentRect.width < 64;
4078
- });
4079
- this._resizeObserver.observe(this);
4080
- (this.shadowRoot?.querySelector(`slot[name="toolbar"]`))?.addEventListener(`slotchange`, this._onToolbarSlotChange);
4081
- }
4082
- disconnectedCallback() {
4083
- super.disconnectedCallback();
4084
- this._resizeObserver?.disconnect();
4085
- this._resizeObserver = void 0;
4086
- this._commandCleanup?.();
4087
- this._commandCleanup = void 0;
4088
- (this.shadowRoot?.querySelector(`slot[name="toolbar"]`))?.removeEventListener(`slotchange`, this._onToolbarSlotChange);
4089
- }
4090
- updated(changed) {
4091
- super.updated(changed);
4092
- this._syncTooltipAttrs();
4093
- if (changed.has(`registry`) || changed.has(`commandActive`)) this._syncCommandListener();
4094
- }
4095
- _syncTooltipAttrs() {
4096
- if (this.description) {
4097
- this.setAttribute(`data-tooltip`, this.description);
4098
- this.setAttribute(`data-tooltip-anchor`, `top`);
4099
- } else {
4100
- this.removeAttribute(`data-tooltip`);
4101
- this.removeAttribute(`data-tooltip-anchor`);
4102
- }
4103
- }
4104
- _syncCommandListener() {
4105
- this._commandCleanup?.();
4106
- this._commandCleanup = void 0;
4107
- if (this.registry && this.commandActive) this._commandCleanup = this.registry.addOnInvokeHandler((id) => {
4108
- if (id === this.commandActive) this.click();
4109
- });
4110
- }
4111
- _updateToolbarContent() {
4112
- const slot = this.shadowRoot?.querySelector(`slot[name="toolbar"]`);
4113
- this._hasToolbarContent = (slot?.assignedElements().length ?? 0) > 0;
4114
- }
4115
- firstUpdated() {
4116
- this._updateToolbarContent();
4117
- }
4118
- render() {
4119
- const showLabel = !this.hideLabels || !this.iconName;
4120
- const hasActions = this.closeable || this._hasToolbarContent;
4121
- return html`
4122
- <div>
4123
- ${this.iconName ? html`<ixfx-icon name="${this.iconName}"></ixfx-icon>` : nothing}
4124
- ${showLabel ? html`<span class="label"><slot></slot></span>` : nothing}
4125
- ${hasActions ? html`
4126
- <div class="actions">
4127
- <div class="toolbar"><slot name="toolbar"></slot></div>
4128
- ${this.closeable ? html`
4129
- <button
4130
- class="close-btn"
4131
- type="button"
4132
- aria-label="Close tab"
4133
- @click=${this._onCloseClick}
4134
- >
4135
- <ixfx-icon name="close"></ixfx-icon>
4136
- </button>
4137
- ` : nothing}
4138
- </div>
4139
- ` : nothing}
4140
- </div>
4141
- `;
4142
- }
4143
- _onCloseClick(e) {
4144
- e.stopPropagation();
4145
- this.dispatchEvent(new CustomEvent(`close`, {
4146
- bubbles: true,
4147
- composed: true
4148
- }));
4149
- this.remove();
4150
- }
4151
- static {
4152
- this.styles = [
4153
- themeFallbacks,
4154
- prominenceStyles,
4155
- css`
4156
- :host {
4157
- --selected-tab-color: var(--ixfx-selected-tab-color, var(--surface-2-text));
4158
- --selected-tab-border-width: var(--ixfx-selected-tab-border-width, 1px);
4159
- --selected-tab-weight: var(--ixfx-selected-tab-weight, bold);
4160
- --selected-background: var(--ixfx-tab-selected-background, var(--surface-5));
4161
- --tab-padding: var(--ixfx-tab-padding, var(--space-m));
4162
- display: inline-flex;
4163
- min-width: 0;
4164
- user-select: none;
4165
- box-sizing: border-box;
4166
- height:fit-content;
4167
- }
4168
-
4169
- :host > div {
4170
- display: flex;
4171
- align-items: center;
4172
- padding: var(--tab-padding) calc(2 * var(--tab-padding-horizontal, var(--tab-padding)));
4173
- min-width: 0;
4174
- width: 100%;
4175
- font-weight: 500;
4176
- border: var(--selected-tab-border-width) solid transparent;
4177
- gap: var(--space-s);
4178
-
4179
- }
4180
-
4181
- :host([selected]) > div {
4182
- color: var(--selected-tab-color);
4183
- border-top-color: var(--indicator-color-top, transparent);
4184
- border-bottom-color: var(--indicator-color-bottom, transparent);
4185
- border-left-color: var(--indicator-color-left, transparent);
4186
- border-right-color: var(--indicator-color-right, transparent);
4187
- background: var(--selected-background);
4188
- font-weight: var(--selected-tab-weight);
4189
- border-radius:var(--radius-m);
4190
- }
4191
-
4192
- .label {
4193
- overflow: var(--allow-overflow, hidden);
4194
- text-overflow: ellipsis;
4195
- white-space: nowrap;
4196
- min-width: 0;
4197
- flex: 1 1 auto;
4198
- }
4199
-
4200
- ixfx-icon {
4201
- flex-shrink: 0;
4202
- }
4203
-
4204
- .actions {
4205
- display: flex;
4206
- align-items: center;
4207
- gap: var(--space-xs);
4208
- margin-left: auto;
4209
- flex-shrink: 0;
4210
- }
4211
-
4212
- .toolbar {
4213
- /* Always laid out (display: flex) so the tab's width doesn't shift when
4214
- the toolbar appears on selection. visibility: hidden keeps the
4215
- toolbar from receiving pointer events or painting, while preserving
4216
- the space its contents take up. */
4217
- display: flex;
4218
- align-items: center;
4219
- gap: var(--space-xs);
4220
- visibility: hidden;
4221
- }
4222
-
4223
- :host([selected]) .toolbar {
4224
- visibility: visible;
4225
- }
4226
-
4227
- slot[name="toolbar"] {
4228
- display: contents;
4229
- }
4230
-
4231
- .close-btn {
4232
- display: inline-flex;
4233
- align-items: center;
4234
- justify-content: center;
4235
- background: none;
4236
- border: none;
4237
- padding: var(--space-xs);
4238
- cursor: pointer;
4239
- color: inherit;
4240
- border-radius: var(--radius-s);
4241
- opacity: var(--prominence-idle-opacity);
4242
- transition: opacity var(--transition), background var(--transition);
4243
- }
4244
-
4245
- .close-btn:hover {
4246
- background: var(--surface-5);
4247
- opacity: var(--prominence-tickled-opacity);
4248
- }
6362
+ .cell.digit[data-phase='enter'],
6363
+ .cell.digit[data-phase='leave'] {
6364
+ width: 0;
6365
+ }
4249
6366
 
4250
- .close-btn:focus-visible {
4251
- outline: 2px solid var(--accent);
4252
- outline-offset: 1px;
4253
- }
6367
+ .cell.symbol[data-phase='enter'],
6368
+ .cell.symbol[data-phase='leave'] {
6369
+ max-width: 0;
6370
+ }
4254
6371
 
4255
- :host([selected]) .close-btn,
4256
- :host(:hover) .close-btn {
4257
- opacity: var(--prominence-tickled-opacity);
4258
- }
4259
- `
4260
- ];
6372
+ .reel {
6373
+ position: absolute;
6374
+ inset: 0 0 auto 0;
6375
+ display: flex;
6376
+ flex-direction: column;
6377
+ will-change: transform;
6378
+ transform: translateY(calc(-1em * var(--o, 10)));
6379
+ }
6380
+
6381
+ .reel > span {
6382
+ display: block;
6383
+ height: 1em;
6384
+ line-height: 1;
6385
+ text-align: center;
6386
+ }
6387
+
6388
+ :host([data-static]) .cell {
6389
+ transition: none;
6390
+ }
6391
+ `;
4261
6392
  }
4262
6393
  };
4263
- __decorate([property({ type: Boolean })], TabListItemElement.prototype, "closeable", void 0);
6394
+ __decorate([property({ type: Number })], SlidingNumberElement.prototype, "value", void 0);
6395
+ __decorate([property({ type: String })], SlidingNumberElement.prototype, "locales", void 0);
6396
+ __decorate([property({ type: Object })], SlidingNumberElement.prototype, "format", void 0);
6397
+ __decorate([property({ type: String })], SlidingNumberElement.prototype, "prefix", void 0);
6398
+ __decorate([property({ type: String })], SlidingNumberElement.prototype, "suffix", void 0);
6399
+ __decorate([property({ type: String })], SlidingNumberElement.prototype, "trend", void 0);
4264
6400
  __decorate([property({
4265
6401
  type: Boolean,
4266
- attribute: `hide-labels`
4267
- })], TabListItemElement.prototype, "hideLabels", void 0);
4268
- __decorate([property({
4269
- type: String,
4270
- attribute: `icon-name`
4271
- })], TabListItemElement.prototype, "iconName", void 0);
4272
- __decorate([property({ type: String })], TabListItemElement.prototype, "description", void 0);
6402
+ attribute: `no-animate`
6403
+ })], SlidingNumberElement.prototype, "noAnimate", void 0);
4273
6404
  __decorate([property({
4274
- type: String,
4275
- attribute: `command-active`
4276
- })], TabListItemElement.prototype, "commandActive", void 0);
4277
- __decorate([property({ attribute: false })], TabListItemElement.prototype, "registry", void 0);
4278
- __decorate([state()], TabListItemElement.prototype, "_compact", void 0);
4279
- __decorate([state()], TabListItemElement.prototype, "_hasToolbarContent", void 0);
4280
- TabListItemElement = __decorate([safeCustomElement(`ixfx-tab-list-item`)], TabListItemElement);
6405
+ type: Boolean,
6406
+ attribute: false
6407
+ })], SlidingNumberElement.prototype, "respectMotionPreference", void 0);
6408
+ __decorate([state()], SlidingNumberElement.prototype, "_cells", void 0);
6409
+ SlidingNumberElement = __decorate([safeCustomElement(`ixfx-sliding-number`)], SlidingNumberElement);
4281
6410
  //#endregion
4282
6411
  //#region src/tabs/tab-panel.ts
4283
6412
  let TabPanelElement = class TabPanelElement extends LitElement {
@@ -4793,7 +6922,7 @@ let ToolbarGroupElement = class ToolbarGroupElement extends LitElement {
4793
6922
  align-items: var(--toolbar-group-align-items, center);
4794
6923
  justify-content: var(--toolbar-group-justify-content, flex-start);
4795
6924
  gap: var(--toolbar-gap, var(--space-s));
4796
- padding: var(--space-xs);
6925
+ padding: var(--toolbar-group-padding, var(--space-xs));
4797
6926
  border: 1px solid var(--border-subtle, var(--border));
4798
6927
  border-radius: var(--radius-s);
4799
6928
  background: var(--surface-3);
@@ -4849,6 +6978,7 @@ let ToolbarElement = class ToolbarElement extends LitElement {
4849
6978
  this.orientation = `horizontal`;
4850
6979
  this.layout = `one-line`;
4851
6980
  this.verticalWriting = `auto`;
6981
+ this.toolbarAlign = `near`;
4852
6982
  this.showGroupLabels = false;
4853
6983
  this.invisibleGroups = false;
4854
6984
  this._overflowCount = 0;
@@ -4868,6 +6998,7 @@ let ToolbarElement = class ToolbarElement extends LitElement {
4868
6998
  :host {
4869
6999
  --toolbar-gap: var(--space-s);
4870
7000
  --toolbar-group-gap: var(--space-m);
7001
+ --toolbar-overflow: auto;
4871
7002
  display: block;
4872
7003
  container-type: inline-size;
4873
7004
  }
@@ -4894,6 +7025,7 @@ let ToolbarElement = class ToolbarElement extends LitElement {
4894
7025
  :host([orientation="vertical-right"]) .inner { flex-direction: column; align-items: stretch; }
4895
7026
  :host([layout="wrap"]) .inner { flex-wrap: wrap; }
4896
7027
  :host([layout="one-line"]) .inner { flex-wrap: nowrap; overflow: hidden; }
7028
+ :host([toolbar-align="far"][orientation="horizontal"]) .inner { justify-content: flex-end; }
4897
7029
  :host([orientation="vertical-left"][vertical-writing="auto"]) ::slotted(*) { writing-mode: sideways-lr; }
4898
7030
  :host([orientation="vertical-right"][vertical-writing="auto"]) ::slotted(*) { writing-mode: vertical-lr; }
4899
7031
  :host([vertical-writing="up"]) ::slotted(*) { writing-mode: vertical-rl; text-orientation: upright; }
@@ -4906,14 +7038,8 @@ let ToolbarElement = class ToolbarElement extends LitElement {
4906
7038
  .overflow-btn {
4907
7039
  anchor-name: var(--anchor-name);
4908
7040
  flex-shrink: 0;
4909
- background: var(--surface-3);
4910
- border: var(--border);
4911
- border-radius: var(--radius-s);
4912
- padding: var(--space-xs) var(--space-s);
4913
- cursor: pointer;
4914
- display: none;
4915
7041
  }
4916
- .overflow-btn[visible] { display: inline-flex; }
7042
+ .overflow-btn[hidden] { display: none; }
4917
7043
  .overflow-popover {
4918
7044
  position: fixed;
4919
7045
  top: anchor(bottom);
@@ -4973,7 +7099,8 @@ let ToolbarElement = class ToolbarElement extends LitElement {
4973
7099
  const isGrow = (el) => el.hasAttribute(`grow`) || el.matches(`[grow]`);
4974
7100
  const overflowable = children.filter((el) => !isGrow(el));
4975
7101
  const isHoriz = this.orientation === `horizontal`;
4976
- if (this.layout !== `one-line`) {
7102
+ const overflowMode = getComputedStyle(this).getPropertyValue(`--toolbar-overflow`).trim();
7103
+ if (this.layout !== `one-line` || overflowMode === `disabled`) {
4977
7104
  let mutated = false;
4978
7105
  for (const el of children) {
4979
7106
  if (el.getAttribute(`slot`) === `overflow`) {
@@ -5005,8 +7132,23 @@ let ToolbarElement = class ToolbarElement extends LitElement {
5005
7132
  if (s > 0) this._sizes.set(el, s);
5006
7133
  cum += (i ? gap : 0) + (this._sizes.get(el) ?? s);
5007
7134
  });
5008
- const btnAllowance = inOverflow.length > 0 ? gap + btnSize : 0;
5009
7135
  let mutated = false;
7136
+ if (overflowMode === `collapsed`) {
7137
+ if (inMain.length > 0) {
7138
+ for (const el of inMain) el.setAttribute(`slot`, `overflow`);
7139
+ inOverflow.push(...inMain);
7140
+ inMain.length = 0;
7141
+ mutated = true;
7142
+ }
7143
+ const newCount = inOverflow.length;
7144
+ if (newCount !== this._overflowCount) {
7145
+ this._overflowCount = newCount;
7146
+ mutated = true;
7147
+ }
7148
+ if (mutated) this.requestUpdate();
7149
+ return;
7150
+ }
7151
+ const btnAllowance = inOverflow.length > 0 ? gap + btnSize : 0;
5010
7152
  while (inMain.length > 0 && cum + btnAllowance > available) {
5011
7153
  const last = inMain[inMain.length - 1];
5012
7154
  const s = this._sizes.get(last) ?? 0;
@@ -5044,11 +7186,12 @@ let ToolbarElement = class ToolbarElement extends LitElement {
5044
7186
  }
5045
7187
  }
5046
7188
  render() {
5047
- const hasOverflow = this._overflowCount > 0;
7189
+ const overflowMode = getComputedStyle(this).getPropertyValue(`--toolbar-overflow`).trim();
7190
+ const hasOverflow = this._overflowCount > 0 || overflowMode === `collapsed`;
5048
7191
  return html`
5049
7192
  <div class="inner" role="toolbar" aria-orientation=${this.orientation}>
5050
7193
  <slot></slot>
5051
- <button class="overflow-btn" ?visible=${hasOverflow} @click=${this._toggleOverflow} aria-label="More" aria-haspopup="true" aria-expanded=${this._popover?.matches(`:popover-open`) ?? false}>⋯</button>
7194
+ <ixfx-button class="overflow-btn" ?hidden=${!hasOverflow} @click=${this._toggleOverflow} aria-label="More" aria-haspopup="true" aria-expanded=${this._popover?.matches(`:popover-open`) ?? false}>⋯</ixfx-button>
5052
7195
  </div>
5053
7196
  <div class="overflow-popover popup-surface" popover="auto" @click=${(e) => e.stopPropagation()}>
5054
7197
  <slot name="overflow"></slot>
@@ -5069,6 +7212,11 @@ __decorate([property({
5069
7212
  attribute: `vertical-writing`,
5070
7213
  reflect: true
5071
7214
  })], ToolbarElement.prototype, "verticalWriting", void 0);
7215
+ __decorate([property({
7216
+ type: String,
7217
+ attribute: `toolbar-align`,
7218
+ reflect: true
7219
+ })], ToolbarElement.prototype, "toolbarAlign", void 0);
5072
7220
  __decorate([property({
5073
7221
  type: Boolean,
5074
7222
  attribute: `show-group-labels`,
@@ -5085,6 +7233,616 @@ __decorate([query(`.overflow-popover`)], ToolbarElement.prototype, "_popover", v
5085
7233
  __decorate([query(`slot`)], ToolbarElement.prototype, "_slot", void 0);
5086
7234
  ToolbarElement = _ToolbarElement = __decorate([safeCustomElement(`ixfx-toolbar`)], ToolbarElement);
5087
7235
  //#endregion
7236
+ //#region src/twosplit-layout/split-layout.ts
7237
+ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends LitElement {
7238
+ constructor(..._args) {
7239
+ super(..._args);
7240
+ this.layout = `vertical`;
7241
+ this.sizeA = ``;
7242
+ this.sizeB = ``;
7243
+ this.fixedA = false;
7244
+ this.fixedB = false;
7245
+ this.collapsible = false;
7246
+ this.collapsedA = false;
7247
+ this.collapsedB = false;
7248
+ this.minSizeA = ``;
7249
+ this.maxSizeA = ``;
7250
+ this.minSizeB = ``;
7251
+ this.maxSizeB = ``;
7252
+ this._hasCorner = false;
7253
+ this.#startSizeA = ``;
7254
+ this.#startSizeB = ``;
7255
+ this.#cornerStartX = 0;
7256
+ this.#cornerStartY = 0;
7257
+ this.#nestedSplitInitialSizeA = ``;
7258
+ this.#pendingNestedUpdate = null;
7259
+ this.#hysteresisThreshold = .1;
7260
+ this.#onResizeHandleDown = (event) => {
7261
+ event.preventDefault();
7262
+ const zoneEl = this.getZone();
7263
+ if (zoneEl) zoneEl.classList.add(`user-resizing`);
7264
+ this.#elementBounds = this.getBoundingClientRect();
7265
+ this.#startSizeA = this.sizeA;
7266
+ this.#startSizeB = this.sizeB;
7267
+ window.addEventListener(`pointerup`, this.#onResizeHandleUp);
7268
+ window.addEventListener(`pointermove`, this.#onResizeHandleMove);
7269
+ };
7270
+ this.#onResizeHandleUp = (event) => {
7271
+ event.preventDefault();
7272
+ const zoneEl = this.getZone();
7273
+ if (zoneEl) zoneEl.classList.remove(`user-resizing`);
7274
+ window.removeEventListener(`pointerup`, this.#onResizeHandleUp);
7275
+ window.removeEventListener(`pointermove`, this.#onResizeHandleMove);
7276
+ };
7277
+ this.#onResizeHandleMove = (event) => {
7278
+ const bounds = this.#elementBounds;
7279
+ if (!bounds) return;
7280
+ const threshold = 5;
7281
+ if (this.layout === `horizontal`) {
7282
+ const y = event.y - bounds.y;
7283
+ const relativeY = clamp(y / bounds.height);
7284
+ if (this.collapsible && this.collapsedA) {
7285
+ if (relativeY > (this.#collapsedAPointerAtCollapse ?? .05) + this.#hysteresisThreshold) {
7286
+ this.collapsedA = false;
7287
+ this.#dispatchCollapsedEvent(`A`, false);
7288
+ this.#collapsedAPointerAtCollapse = void 0;
7289
+ const { sizeA, sizeB } = this.#applyConstraints(`${relativeY * 100}%`, `${(1 - relativeY) * 100}%`, bounds.height);
7290
+ this.sizeA = sizeA;
7291
+ this.sizeB = sizeB;
7292
+ }
7293
+ } else if (this.collapsible && !this.collapsedA && relativeY < threshold / bounds.height) {
7294
+ this.collapsedA = true;
7295
+ this.#dispatchCollapsedEvent(`A`, true);
7296
+ this.sizeA = `0px`;
7297
+ this.sizeB = `100%`;
7298
+ this.#collapsedAPointerAtCollapse = relativeY;
7299
+ } else if (!this.collapsedA) {
7300
+ const { sizeA, sizeB } = this.#applyConstraints(`${relativeY * 100}%`, `${(1 - relativeY) * 100}%`, bounds.height);
7301
+ this.sizeA = sizeA;
7302
+ this.sizeB = sizeB;
7303
+ }
7304
+ if (this.collapsible && this.collapsedB) {
7305
+ if (relativeY < (this.#collapsedBPointerAtCollapse ?? .95) - this.#hysteresisThreshold) {
7306
+ this.collapsedB = false;
7307
+ this.#dispatchCollapsedEvent(`B`, false);
7308
+ this.#collapsedBPointerAtCollapse = void 0;
7309
+ const { sizeA, sizeB } = this.#applyConstraints(`${relativeY * 100}%`, `${(1 - relativeY) * 100}%`, bounds.height);
7310
+ this.sizeA = sizeA;
7311
+ this.sizeB = sizeB;
7312
+ }
7313
+ } else if (this.collapsible && !this.collapsedB && relativeY > 1 - threshold / bounds.height) {
7314
+ this.collapsedB = true;
7315
+ this.#dispatchCollapsedEvent(`B`, true);
7316
+ this.sizeA = `100%`;
7317
+ this.sizeB = `0px`;
7318
+ this.#collapsedBPointerAtCollapse = relativeY;
7319
+ }
7320
+ } else {
7321
+ const x = event.x - bounds.x;
7322
+ const relativeX = clamp(x / bounds.width);
7323
+ if (this.collapsible && this.collapsedA) {
7324
+ if (relativeX > (this.#collapsedAPointerAtCollapse ?? .05) + this.#hysteresisThreshold) {
7325
+ this.collapsedA = false;
7326
+ this.#dispatchCollapsedEvent(`A`, false);
7327
+ this.#collapsedAPointerAtCollapse = void 0;
7328
+ const { sizeA, sizeB } = this.#applyConstraints(`${relativeX * 100}%`, `${(1 - relativeX) * 100}%`, bounds.width);
7329
+ this.sizeA = sizeA;
7330
+ this.sizeB = sizeB;
7331
+ }
7332
+ } else if (this.collapsible && !this.collapsedA && relativeX < threshold / bounds.width) {
7333
+ this.collapsedA = true;
7334
+ this.#dispatchCollapsedEvent(`A`, true);
7335
+ this.sizeA = `0px`;
7336
+ this.sizeB = `100%`;
7337
+ this.#collapsedAPointerAtCollapse = relativeX;
7338
+ } else if (!this.collapsedA) {
7339
+ const { sizeA, sizeB } = this.#applyConstraints(`${relativeX * 100}%`, `${(1 - relativeX) * 100}%`, bounds.width);
7340
+ this.sizeA = sizeA;
7341
+ this.sizeB = sizeB;
7342
+ }
7343
+ if (this.collapsible && this.collapsedB) {
7344
+ if (relativeX < (this.#collapsedBPointerAtCollapse ?? .95) - this.#hysteresisThreshold) {
7345
+ this.collapsedB = false;
7346
+ this.#dispatchCollapsedEvent(`B`, false);
7347
+ this.#collapsedBPointerAtCollapse = void 0;
7348
+ const { sizeA, sizeB } = this.#applyConstraints(`${relativeX * 100}%`, `${(1 - relativeX) * 100}%`, bounds.width);
7349
+ this.sizeA = sizeA;
7350
+ this.sizeB = sizeB;
7351
+ }
7352
+ } else if (this.collapsible && !this.collapsedB && relativeX > 1 - threshold / bounds.width) {
7353
+ this.collapsedB = true;
7354
+ this.#dispatchCollapsedEvent(`B`, true);
7355
+ this.sizeA = `100%`;
7356
+ this.sizeB = `0px`;
7357
+ this.#collapsedBPointerAtCollapse = relativeX;
7358
+ }
7359
+ }
7360
+ event.preventDefault();
7361
+ };
7362
+ this.#onCornerResizeDown = (event) => {
7363
+ event.preventDefault();
7364
+ event.stopPropagation();
7365
+ const zoneEl = this.getZone();
7366
+ if (zoneEl) zoneEl.classList.add(`user-resizing`);
7367
+ this.#elementBounds = this.getBoundingClientRect();
7368
+ this.#cornerStartX = event.clientX;
7369
+ this.#cornerStartY = event.clientY;
7370
+ this.#startSizeA = this.sizeA;
7371
+ this.#startSizeB = this.sizeB;
7372
+ const nestedSplit = this.#getNestedSplit();
7373
+ if (nestedSplit) {
7374
+ this.#nestedSplitInitialSizeA = nestedSplit.sizeA;
7375
+ const nestedBounds = nestedSplit.getBoundingClientRect();
7376
+ const nestedHandle = nestedSplit.shadowRoot?.querySelector(`.split-bar`);
7377
+ if (nestedHandle) {
7378
+ this.#nestedSplitBounds = nestedBounds;
7379
+ this.#nestedHandleBounds = nestedHandle.getBoundingClientRect();
7380
+ }
7381
+ const nestedZoneEl = nestedSplit.getZone();
7382
+ if (nestedZoneEl) nestedZoneEl.classList.add(`user-resizing`);
7383
+ }
7384
+ window.addEventListener(`pointerup`, this.#onCornerResizeUp);
7385
+ window.addEventListener(`pointermove`, this.#onCornerResizeMove);
7386
+ };
7387
+ this.#onCornerResizeUp = (event) => {
7388
+ event.preventDefault();
7389
+ const zoneEl = this.getZone();
7390
+ if (zoneEl) zoneEl.classList.remove(`user-resizing`);
7391
+ const nestedSplit = this.#getNestedSplit();
7392
+ if (nestedSplit) {
7393
+ const nestedZoneEl = nestedSplit.getZone();
7394
+ if (nestedZoneEl) nestedZoneEl.classList.remove(`user-resizing`);
7395
+ }
7396
+ window.removeEventListener(`pointerup`, this.#onCornerResizeUp);
7397
+ window.removeEventListener(`pointermove`, this.#onCornerResizeMove);
7398
+ };
7399
+ this.#onCornerResizeMove = (event) => {
7400
+ const outerBounds = this.#elementBounds;
7401
+ const zoneEl = this.getZone();
7402
+ const nestedSplit = this.#getNestedSplit();
7403
+ if (!outerBounds || !zoneEl || !nestedSplit) return;
7404
+ const zoneBounds = zoneEl.getBoundingClientRect();
7405
+ const cursorZoneLocalX = event.clientX - zoneBounds.left;
7406
+ const cursorZoneLocalY = event.clientY - zoneBounds.top;
7407
+ if (this.layout === `vertical` && nestedSplit.layout === `horizontal`) {
7408
+ let newOuterFraction = cursorZoneLocalX / outerBounds.width;
7409
+ const newNestedFraction = cursorZoneLocalY / outerBounds.height;
7410
+ const { sizeA: constrainedA, sizeB: constrainedB } = this.#applyConstraints(`${newOuterFraction * 100}%`, `${(1 - newOuterFraction) * 100}%`, outerBounds.width);
7411
+ newOuterFraction = this.#parseSizeAsFraction(constrainedA, outerBounds.width);
7412
+ this.sizeA = constrainedA;
7413
+ this.sizeB = constrainedB;
7414
+ this.#pendingNestedUpdate = {
7415
+ sizeA: newNestedFraction,
7416
+ sizeB: 1 - newNestedFraction
7417
+ };
7418
+ requestAnimationFrame(() => this.#applyPendingNestedUpdate());
7419
+ const cornerZone = this.shadowRoot?.querySelector(`.corner-zone`);
7420
+ if (cornerZone) {
7421
+ cornerZone.style.left = `${cursorZoneLocalX - 6}px`;
7422
+ cornerZone.style.top = `${cursorZoneLocalY - 6}px`;
7423
+ }
7424
+ } else if (this.layout === `horizontal` && nestedSplit.layout === `vertical`) {
7425
+ const newNestedFraction = cursorZoneLocalX / outerBounds.width;
7426
+ let newOuterFraction = cursorZoneLocalY / outerBounds.height;
7427
+ const { sizeA: constrainedA, sizeB: constrainedB } = this.#applyConstraints(`${newOuterFraction * 100}%`, `${(1 - newOuterFraction) * 100}%`, outerBounds.height);
7428
+ newOuterFraction = this.#parseSizeAsFraction(constrainedA, outerBounds.height);
7429
+ this.sizeA = constrainedA;
7430
+ this.sizeB = constrainedB;
7431
+ this.#pendingNestedUpdate = {
7432
+ sizeA: newNestedFraction,
7433
+ sizeB: 1 - newNestedFraction
7434
+ };
7435
+ requestAnimationFrame(() => this.#applyPendingNestedUpdate());
7436
+ const cornerZone = this.shadowRoot?.querySelector(`.corner-zone`);
7437
+ if (cornerZone) {
7438
+ cornerZone.style.left = `${cursorZoneLocalX - 6}px`;
7439
+ cornerZone.style.top = `${cursorZoneLocalY - 6}px`;
7440
+ }
7441
+ }
7442
+ event.preventDefault();
7443
+ };
7444
+ }
7445
+ get showGrabHandle() {
7446
+ const size = getComputedStyle(this).getPropertyValue(`--handle-size`).trim();
7447
+ const num = Number.parseFloat(size);
7448
+ return !Number.isNaN(num) && num > 3;
7449
+ }
7450
+ #elementBounds;
7451
+ #startSizeA;
7452
+ #startSizeB;
7453
+ #cornerStartX;
7454
+ #cornerStartY;
7455
+ #nestedSplitInitialSizeA;
7456
+ #cornerZone;
7457
+ #nestedSplit;
7458
+ #nestedSplitBounds;
7459
+ #nestedHandleBounds;
7460
+ #observer;
7461
+ #pendingNestedUpdate;
7462
+ #collapsedAPointerAtCollapse;
7463
+ #collapsedBPointerAtCollapse;
7464
+ #hysteresisThreshold;
7465
+ disconnectedCallback() {
7466
+ super.disconnectedCallback();
7467
+ this.#observer?.disconnect();
7468
+ }
7469
+ firstUpdated() {
7470
+ this.#setupNestedSplitObserver();
7471
+ }
7472
+ updated(changedProperties) {
7473
+ if (changedProperties.has(`sizeA`) || changedProperties.has(`sizeB`) || changedProperties.has(`collapsedA`) || changedProperties.has(`collapsedB`)) this.#updateCornerPosition();
7474
+ if (changedProperties.has(`sizeA`) || changedProperties.has(`sizeB`)) this.#updateDataCollapsed();
7475
+ }
7476
+ #updateDataCollapsed() {
7477
+ if (this.collapsedA) this.setAttribute(`data-collapsed-a`, ``);
7478
+ else this.removeAttribute(`data-collapsed-a`);
7479
+ if (this.collapsedB) this.setAttribute(`data-collapsed-b`, ``);
7480
+ else this.removeAttribute(`data-collapsed-b`);
7481
+ }
7482
+ #dispatchCollapsedEvent(panel, collapsed) {
7483
+ this.dispatchEvent(new CustomEvent(`ixfx-split-collapsed`, {
7484
+ detail: {
7485
+ panel,
7486
+ collapsed
7487
+ },
7488
+ bubbles: true,
7489
+ composed: true
7490
+ }));
7491
+ }
7492
+ #setupNestedSplitObserver() {
7493
+ const slotA = this.shadowRoot?.querySelector(`slot[name="a"]`);
7494
+ if (!slotA) return;
7495
+ this.#observer = new MutationObserver(() => {
7496
+ this.#checkForNestedSplit();
7497
+ });
7498
+ this.#observer.observe(slotA, {
7499
+ characterData: true,
7500
+ childList: true,
7501
+ subtree: true
7502
+ });
7503
+ this.#checkForNestedSplit();
7504
+ }
7505
+ #checkForNestedSplit() {
7506
+ const slotA = this.shadowRoot?.querySelector(`slot[name="a"]`);
7507
+ if (!slotA) return;
7508
+ const assignedElements = slotA.assignedElements();
7509
+ const findNestedSplit = (elements) => {
7510
+ for (const el of elements) {
7511
+ if (el.tagName === `IXFX-SPLIT-LAYOUT`) return el;
7512
+ const nested = findNestedSplit(Array.from(el.children));
7513
+ if (nested) return nested;
7514
+ }
7515
+ };
7516
+ const nestedSplit = findNestedSplit(assignedElements);
7517
+ if (nestedSplit) {
7518
+ this.#nestedSplit = nestedSplit;
7519
+ this._hasCorner = true;
7520
+ requestAnimationFrame(() => this.#updateCornerPosition());
7521
+ } else {
7522
+ this.#nestedSplit = void 0;
7523
+ this._hasCorner = false;
7524
+ }
7525
+ }
7526
+ #getNestedSplit() {
7527
+ if (this.#nestedSplit) return this.#nestedSplit;
7528
+ const slotA = this.shadowRoot?.querySelector(`slot[name="a"]`);
7529
+ if (!slotA) return void 0;
7530
+ return slotA.assignedElements().find((el) => el.tagName === `IXFX-SPLIT-LAYOUT`);
7531
+ }
7532
+ #parseSizeAsFraction(size, total) {
7533
+ if (size.endsWith(`px`)) return Number.parseFloat(size.replace(`px`, ``)) / total;
7534
+ else if (size.endsWith(`%`)) return Number.parseFloat(size.replace(`%`, ``)) / 100;
7535
+ return .5;
7536
+ }
7537
+ #parseSizeToPx(size, total) {
7538
+ if (size.endsWith(`px`)) return Number.parseFloat(size.replace(`px`, ``));
7539
+ else if (size.endsWith(`%`)) return Number.parseFloat(size.replace(`%`, ``)) / 100 * total;
7540
+ return null;
7541
+ }
7542
+ #applyConstraints(sizeA, sizeB, total) {
7543
+ let fractionA = this.#parseSizeAsFraction(sizeA, total);
7544
+ let fractionB = this.#parseSizeAsFraction(sizeB, total);
7545
+ const minA = this.#minSizeToFraction(this.minSizeA, total);
7546
+ const minB = this.#minSizeToFraction(this.minSizeB, total);
7547
+ const maxA = this.#maxSizeToFraction(this.maxSizeA, total);
7548
+ const maxB = this.#maxSizeToFraction(this.maxSizeB, total);
7549
+ if (minA !== null || minB !== null) {
7550
+ const minSum = (minA ?? 0) + (minB ?? 0);
7551
+ if (minSum > 1) {
7552
+ const scale = 1 / minSum;
7553
+ if (minA !== null) minA !== null ? fractionA = minA * scale : fractionA = 0;
7554
+ if (minB !== null) minB !== null ? fractionB = minB * scale : fractionB = 0;
7555
+ }
7556
+ if (fractionA < (minA ?? 0)) fractionA = minA ?? 0;
7557
+ if (fractionB < (minB ?? 0)) fractionB = minB ?? 0;
7558
+ }
7559
+ if (maxA !== null) {
7560
+ if (fractionA > maxA) fractionA = maxA;
7561
+ }
7562
+ if (maxB !== null) {
7563
+ if (fractionB > maxB) fractionB = maxB;
7564
+ }
7565
+ fractionB = 1 - fractionA;
7566
+ const isPercentA = !this.sizeA.endsWith(`px`);
7567
+ this.sizeB.endsWith(`px`);
7568
+ if (isPercentA) return {
7569
+ sizeA: `${fractionA * 100}%`,
7570
+ sizeB: `${fractionB * 100}%`
7571
+ };
7572
+ else return {
7573
+ sizeA: `${fractionA * total}px`,
7574
+ sizeB: `${fractionB * total}px`
7575
+ };
7576
+ }
7577
+ #minSizeToFraction(minSize, total) {
7578
+ if (minSize.length === 0) return null;
7579
+ const px = this.#parseSizeToPx(minSize, total);
7580
+ if (px !== null) return px / total;
7581
+ if (minSize.endsWith(`%`)) return Number.parseFloat(minSize.replace(`%`, ``)) / 100;
7582
+ return null;
7583
+ }
7584
+ #maxSizeToFraction(maxSize, total) {
7585
+ if (maxSize.length === 0) return null;
7586
+ const px = this.#parseSizeToPx(maxSize, total);
7587
+ if (px !== null) return px / total;
7588
+ if (maxSize.endsWith(`%`)) return Number.parseFloat(maxSize.replace(`%`, ``)) / 100;
7589
+ return null;
7590
+ }
7591
+ #getIntersectionPoint() {
7592
+ const outerBounds = this.getBoundingClientRect();
7593
+ const zoneEl = this.getZone();
7594
+ if (!zoneEl) return null;
7595
+ const nestedSplit = this.#getNestedSplit();
7596
+ if (!nestedSplit) return null;
7597
+ zoneEl.getBoundingClientRect();
7598
+ const outerFraction = this.#parseSizeAsFraction(this.sizeA || `50%`, outerBounds.width);
7599
+ let x, y;
7600
+ if (this.layout === `vertical` && nestedSplit.layout === `horizontal`) {
7601
+ x = outerFraction * outerBounds.width;
7602
+ y = this.#parseSizeAsFraction(nestedSplit.sizeA || `50%`, outerBounds.height) * outerBounds.height;
7603
+ } else if (this.layout === `horizontal` && nestedSplit.layout === `vertical`) {
7604
+ x = this.#parseSizeAsFraction(nestedSplit.sizeA || `50%`, outerBounds.width) * outerBounds.width;
7605
+ y = outerFraction * outerBounds.height;
7606
+ } else return null;
7607
+ return {
7608
+ x,
7609
+ y
7610
+ };
7611
+ }
7612
+ #updateCornerPosition() {
7613
+ if (!this._hasCorner) return;
7614
+ const cornerZone = this.shadowRoot?.querySelector(`.corner-zone`);
7615
+ if (!cornerZone) return;
7616
+ if (!this.getZone()) return;
7617
+ const intersection = this.#getIntersectionPoint();
7618
+ if (!intersection) {
7619
+ cornerZone.style.display = `none`;
7620
+ return;
7621
+ }
7622
+ const cornerX = intersection.x - 6;
7623
+ const cornerY = intersection.y - 6;
7624
+ cornerZone.style.left = `${cornerX}px`;
7625
+ cornerZone.style.top = `${cornerY}px`;
7626
+ cornerZone.style.display = `block`;
7627
+ const nestedSplit = this.#getNestedSplit();
7628
+ if (nestedSplit) {
7629
+ if (this.layout === `vertical` && nestedSplit.layout === `horizontal`) cornerZone.style.cursor = `nwse-resize`;
7630
+ else if (this.layout === `horizontal` && nestedSplit.layout === `vertical`) cornerZone.style.cursor = `nesw-resize`;
7631
+ }
7632
+ }
7633
+ getZone() {
7634
+ if (!this.shadowRoot) return;
7635
+ const el = this.shadowRoot.querySelector(`#zone`);
7636
+ if (!el) return void 0;
7637
+ return el;
7638
+ }
7639
+ #onResizeHandleDown;
7640
+ #onResizeHandleUp;
7641
+ #onResizeHandleMove;
7642
+ #onCornerResizeDown;
7643
+ #onCornerResizeUp;
7644
+ #applyPendingNestedUpdate() {
7645
+ if (!this.#pendingNestedUpdate) return;
7646
+ const { sizeA, sizeB } = this.#pendingNestedUpdate;
7647
+ const nested = this.#getNestedSplit();
7648
+ if (nested) {
7649
+ nested.sizeA = `${sizeA * 100}%`;
7650
+ nested.sizeB = `${sizeB * 100}%`;
7651
+ }
7652
+ this.#pendingNestedUpdate = null;
7653
+ }
7654
+ #onCornerResizeMove;
7655
+ render() {
7656
+ const sizePrefix = this.layout === `vertical` ? `width` : `height`;
7657
+ let sizeA = this.sizeA;
7658
+ let sizeB = this.sizeB;
7659
+ if (this.collapsedA) sizeA = `0px`;
7660
+ else if (sizeA.length === 0 && sizeB.length === 0) sizeB = sizeA = `50%`;
7661
+ else if (sizeA.length === 0) sizeA = `auto`;
7662
+ else if (sizeB.length === 0) sizeB = `auto`;
7663
+ if (this.collapsedB) sizeB = `0px`;
7664
+ sizeA = `${sizePrefix}:${sizeA}`;
7665
+ sizeB = `${sizePrefix}:${sizeB}`;
7666
+ return html`
7667
+ <div id="zone">
7668
+ <slot style=${sizeA} name="a"></slot>
7669
+ ${this._hasCorner ? html`<div class="corner-zone" @pointerdown=${this.#onCornerResizeDown}></div>` : ``}
7670
+ <div @pointerdown=${this.#onResizeHandleDown} class="split-bar">
7671
+ ${this.showGrabHandle ? html`
7672
+ <div class="grab-handle">
7673
+ <span class="dot"></span>
7674
+ <span class="dot"></span>
7675
+ <span class="dot"></span>
7676
+ </div>
7677
+ ` : ``}
7678
+ </div>
7679
+ <slot style=${sizeB} name="b"></slot>
7680
+ </div>`;
7681
+ }
7682
+ static {
7683
+ this.styles = [themeFallbacks, css`
7684
+ :host {
7685
+ --handle-size: 2px;
7686
+ --handle-colour: transparent;
7687
+ --handle-hover-colour: var(--surface-6);
7688
+ --handle-active-colour: var(--accent);
7689
+ --handle-bg: var(--handle-colour);
7690
+ --handle-grab-colour: var(--border-subtle-color);
7691
+ --handle-grab-dot-size: 3px;
7692
+ --handle-grab-size: 16px;
7693
+ --handle-grab-thickness: 8px;
7694
+
7695
+ display: block;
7696
+ box-sizing: border-box;
7697
+ width: 100%;
7698
+ height: 100%;
7699
+ }
7700
+ * {
7701
+ box-sizing: border-box;
7702
+ }
7703
+ #zone {
7704
+ display: flex;
7705
+ height: 100%;
7706
+ position: relative;
7707
+ }
7708
+ slot {
7709
+ display: block;
7710
+ overflow: hidden;
7711
+ border: var(--border-subtle);
7712
+ border-radius: var(--radius-m);
7713
+ }
7714
+ :slotted(*) {
7715
+ width: 100%;
7716
+ height: 100%;
7717
+ }
7718
+
7719
+ .split-bar {
7720
+ position: relative;
7721
+ background-color: var(--handle-colour);
7722
+ transition: background-color 0.3s ease 0s;
7723
+ width: var(--handle-size);
7724
+ height: 100%;
7725
+ cursor: ew-resize;
7726
+ flex-shrink: 0;
7727
+ border-radius: var(--radius-m);
7728
+
7729
+ }
7730
+
7731
+ @keyframes handle-hover {
7732
+ from { background-color: var(--handle-colour); }
7733
+ to { background-color: var(--handle-hover-colour); }
7734
+ }
7735
+
7736
+ .grab-handle {
7737
+ position: absolute;
7738
+ top: 50%;
7739
+ left: 50%;
7740
+ transform: translate(-50%, -50%);
7741
+ display: flex;
7742
+ align-items: center;
7743
+ justify-content: center;
7744
+ gap: var(--handle-grab-dot-size);
7745
+ background: var(--handle-bg);
7746
+ border-radius: var(--radius-s);
7747
+ }
7748
+
7749
+ .grab-handle .dot {
7750
+ width: var(--handle-grab-dot-size);
7751
+ height: var(--handle-grab-dot-size);
7752
+ border-radius: 50%;
7753
+ background: var(--handle-grab-colour);
7754
+ flex-shrink: 0;
7755
+ }
7756
+
7757
+ :host([layout="vertical"]) .grab-handle {
7758
+ flex-direction: column;
7759
+ width: var(--handle-grab-thickness);
7760
+ height: var(--handle-grab-size);
7761
+ }
7762
+
7763
+ :host([layout="horizontal"]) .grab-handle {
7764
+ flex-direction: row;
7765
+ width: var(--handle-grab-size);
7766
+ height: var(--handle-grab-thickness);
7767
+ }
7768
+
7769
+ #zone.user-resizing .grab-handle {
7770
+ display: none;
7771
+ }
7772
+ #zone.user-resizing {
7773
+ slot {
7774
+ opacity: 0.5;
7775
+ }
7776
+ .split-bar {
7777
+ background-color: var(--handle-active-colour);
7778
+ transition: none;
7779
+ animation: none !important;
7780
+ }
7781
+ }
7782
+
7783
+ #zone:not(.user-resizing) .split-bar:hover {
7784
+ animation: handle-hover 0.001s ease-out 0.7s both;
7785
+ }
7786
+
7787
+ .corner-zone {
7788
+ position: absolute;
7789
+ width: 12px;
7790
+ height: 12px;
7791
+ z-index: 10;
7792
+ border-radius: 50%;
7793
+ background: transparent;
7794
+ }
7795
+ .corner-zone:hover {
7796
+ background: transparent;
7797
+ }
7798
+
7799
+ :host([layout='horizontal']) {
7800
+ #zone {
7801
+ flex-direction: column;
7802
+ }
7803
+ .split-bar {
7804
+ position: relative;
7805
+ background-color: var(--handle-colour);
7806
+ transition: background-color 0.3s ease 0s;
7807
+ height: var(--handle-size);
7808
+ width: 100%;
7809
+ cursor: ns-resize;
7810
+ }
7811
+ #zone.user-resizing .split-bar {
7812
+ background-color: var(--handle-active-colour);
7813
+ transition: none;
7814
+ animation: none !important;
7815
+ }
7816
+ }
7817
+
7818
+ :host([data-collapsed-a]) slot[name="a"],
7819
+ :host([data-collapsed-b]) slot[name="b"] {
7820
+ display: none;
7821
+ }
7822
+ `];
7823
+ }
7824
+ };
7825
+ __decorate([property({ type: String })], TwoSplitLayoutElement.prototype, "layout", void 0);
7826
+ __decorate([property({ type: String })], TwoSplitLayoutElement.prototype, "sizeA", void 0);
7827
+ __decorate([property({ type: String })], TwoSplitLayoutElement.prototype, "sizeB", void 0);
7828
+ __decorate([property({ type: Boolean })], TwoSplitLayoutElement.prototype, "fixedA", void 0);
7829
+ __decorate([property({ type: Boolean })], TwoSplitLayoutElement.prototype, "fixedB", void 0);
7830
+ __decorate([property({ type: Boolean })], TwoSplitLayoutElement.prototype, "collapsible", void 0);
7831
+ __decorate([property({
7832
+ type: Boolean,
7833
+ reflect: true
7834
+ })], TwoSplitLayoutElement.prototype, "collapsedA", void 0);
7835
+ __decorate([property({
7836
+ type: Boolean,
7837
+ reflect: true
7838
+ })], TwoSplitLayoutElement.prototype, "collapsedB", void 0);
7839
+ __decorate([property({ type: String })], TwoSplitLayoutElement.prototype, "minSizeA", void 0);
7840
+ __decorate([property({ type: String })], TwoSplitLayoutElement.prototype, "maxSizeA", void 0);
7841
+ __decorate([property({ type: String })], TwoSplitLayoutElement.prototype, "minSizeB", void 0);
7842
+ __decorate([property({ type: String })], TwoSplitLayoutElement.prototype, "maxSizeB", void 0);
7843
+ __decorate([state()], TwoSplitLayoutElement.prototype, "_hasCorner", void 0);
7844
+ TwoSplitLayoutElement = __decorate([safeCustomElement(`ixfx-twosplit-layout`)], TwoSplitLayoutElement);
7845
+ //#endregion
5088
7846
  //#region src/util/checkbox.ts
5089
7847
  function isCheckboxLike(el) {
5090
7848
  if (!(el instanceof HTMLElement)) return false;
@@ -5142,6 +7900,6 @@ function init() {
5142
7900
  console.log(`Init`);
5143
7901
  }
5144
7902
  //#endregion
5145
- export { AcTextElement, AcTokenElement, ButtonElement, Checkbox, ColourPicker, ColourPickerPopup, CrumbNavigationElement, CrumbPathController, CrumbPathElement, DataController, DataDisplayComponent, DataGridElement, 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, 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, SwipeElement, TabController, TabListElement, TabListItemElement, TabPanelElement, TabPanelsElement, TextShimmerElement, TimelineController, TimelineTrackTabElement, ToolbarElement, ToolbarGroupElement, TooltipElement, TransitoryLabelElement, TreeBaseElement, TreeController, TreeDataModel, TreeListElement, VerticalListElement, XyPad, applyAgeModification, applyHighlights, attachTimelineInteractions, buildHighlightTargets, clearHighlights, collectDataResult, computeColour, createAcCompletionProvider, createElementIncrSearch, createFuzzySearch, createIncrSearch, createPillRenderer, createTreeListAdapter, createVerticalListAdapter, getIcon, hasIcon, iconBus, init, monitorIndeterminate, parseColourScaleAttr, readBaseColour, registerIcon, resolveCssVars, resolveGrid, svgToDataUri, timelineStyles };
7903
+ 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 };
5146
7904
 
5147
7905
  //# sourceMappingURL=index.js.map