@mintplayer/ng-bootstrap 21.22.0 → 21.23.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 (182) hide show
  1. package/fesm2022/mintplayer-ng-bootstrap-accordion.mjs +20 -20
  2. package/fesm2022/mintplayer-ng-bootstrap-accordion.mjs.map +1 -1
  3. package/fesm2022/mintplayer-ng-bootstrap-alert.mjs +8 -8
  4. package/fesm2022/mintplayer-ng-bootstrap-alert.mjs.map +1 -1
  5. package/fesm2022/mintplayer-ng-bootstrap-badge.mjs +5 -5
  6. package/fesm2022/mintplayer-ng-bootstrap-badge.mjs.map +1 -1
  7. package/fesm2022/mintplayer-ng-bootstrap-breadcrumb.mjs +6 -6
  8. package/fesm2022/mintplayer-ng-bootstrap-breadcrumb.mjs.map +1 -1
  9. package/fesm2022/mintplayer-ng-bootstrap-button-group.mjs +3 -3
  10. package/fesm2022/mintplayer-ng-bootstrap-button-group.mjs.map +1 -1
  11. package/fesm2022/mintplayer-ng-bootstrap-button-type.mjs +4 -4
  12. package/fesm2022/mintplayer-ng-bootstrap-button-type.mjs.map +1 -1
  13. package/fesm2022/mintplayer-ng-bootstrap-calendar-month.mjs +9 -9
  14. package/fesm2022/mintplayer-ng-bootstrap-calendar-month.mjs.map +1 -1
  15. package/fesm2022/mintplayer-ng-bootstrap-calendar.mjs +10 -10
  16. package/fesm2022/mintplayer-ng-bootstrap-calendar.mjs.map +1 -1
  17. package/fesm2022/mintplayer-ng-bootstrap-card.mjs +8 -8
  18. package/fesm2022/mintplayer-ng-bootstrap-card.mjs.map +1 -1
  19. package/fesm2022/mintplayer-ng-bootstrap-carousel.mjs +25 -25
  20. package/fesm2022/mintplayer-ng-bootstrap-carousel.mjs.map +1 -1
  21. package/fesm2022/mintplayer-ng-bootstrap-close.mjs +3 -3
  22. package/fesm2022/mintplayer-ng-bootstrap-close.mjs.map +1 -1
  23. package/fesm2022/mintplayer-ng-bootstrap-code-snippet.mjs +7 -7
  24. package/fesm2022/mintplayer-ng-bootstrap-code-snippet.mjs.map +1 -1
  25. package/fesm2022/mintplayer-ng-bootstrap-color-picker.mjs +58 -58
  26. package/fesm2022/mintplayer-ng-bootstrap-color-picker.mjs.map +1 -1
  27. package/fesm2022/mintplayer-ng-bootstrap-container.mjs +3 -3
  28. package/fesm2022/mintplayer-ng-bootstrap-container.mjs.map +1 -1
  29. package/fesm2022/mintplayer-ng-bootstrap-context-menu.mjs +3 -3
  30. package/fesm2022/mintplayer-ng-bootstrap-context-menu.mjs.map +1 -1
  31. package/fesm2022/mintplayer-ng-bootstrap-copy.mjs +4 -4
  32. package/fesm2022/mintplayer-ng-bootstrap-copy.mjs.map +1 -1
  33. package/fesm2022/mintplayer-ng-bootstrap-datatable.mjs +20 -20
  34. package/fesm2022/mintplayer-ng-bootstrap-datatable.mjs.map +1 -1
  35. package/fesm2022/mintplayer-ng-bootstrap-datepicker.mjs +6 -6
  36. package/fesm2022/mintplayer-ng-bootstrap-datepicker.mjs.map +1 -1
  37. package/fesm2022/mintplayer-ng-bootstrap-dock.mjs +789 -1175
  38. package/fesm2022/mintplayer-ng-bootstrap-dock.mjs.map +1 -1
  39. package/fesm2022/mintplayer-ng-bootstrap-dropdown-divider.mjs +3 -3
  40. package/fesm2022/mintplayer-ng-bootstrap-dropdown-divider.mjs.map +1 -1
  41. package/fesm2022/mintplayer-ng-bootstrap-dropdown-menu.mjs +10 -10
  42. package/fesm2022/mintplayer-ng-bootstrap-dropdown-menu.mjs.map +1 -1
  43. package/fesm2022/mintplayer-ng-bootstrap-dropdown.mjs +15 -15
  44. package/fesm2022/mintplayer-ng-bootstrap-dropdown.mjs.map +1 -1
  45. package/fesm2022/mintplayer-ng-bootstrap-enhanced-paste.mjs +3 -3
  46. package/fesm2022/mintplayer-ng-bootstrap-enhanced-paste.mjs.map +1 -1
  47. package/fesm2022/mintplayer-ng-bootstrap-enum.mjs +3 -3
  48. package/fesm2022/mintplayer-ng-bootstrap-enum.mjs.map +1 -1
  49. package/fesm2022/mintplayer-ng-bootstrap-file-upload.mjs +16 -16
  50. package/fesm2022/mintplayer-ng-bootstrap-file-upload.mjs.map +1 -1
  51. package/fesm2022/mintplayer-ng-bootstrap-floating-labels.mjs +3 -3
  52. package/fesm2022/mintplayer-ng-bootstrap-floating-labels.mjs.map +1 -1
  53. package/fesm2022/mintplayer-ng-bootstrap-font-color.mjs +3 -3
  54. package/fesm2022/mintplayer-ng-bootstrap-font-color.mjs.map +1 -1
  55. package/fesm2022/mintplayer-ng-bootstrap-for.mjs +4 -4
  56. package/fesm2022/mintplayer-ng-bootstrap-for.mjs.map +1 -1
  57. package/fesm2022/mintplayer-ng-bootstrap-form.mjs +11 -11
  58. package/fesm2022/mintplayer-ng-bootstrap-form.mjs.map +1 -1
  59. package/fesm2022/mintplayer-ng-bootstrap-grid.mjs +26 -26
  60. package/fesm2022/mintplayer-ng-bootstrap-grid.mjs.map +1 -1
  61. package/fesm2022/mintplayer-ng-bootstrap-has-overlay.mjs +4 -4
  62. package/fesm2022/mintplayer-ng-bootstrap-has-overlay.mjs.map +1 -1
  63. package/fesm2022/mintplayer-ng-bootstrap-has-property.mjs +3 -3
  64. package/fesm2022/mintplayer-ng-bootstrap-has-property.mjs.map +1 -1
  65. package/fesm2022/mintplayer-ng-bootstrap-in-list.mjs +3 -3
  66. package/fesm2022/mintplayer-ng-bootstrap-in-list.mjs.map +1 -1
  67. package/fesm2022/mintplayer-ng-bootstrap-input-group.mjs +3 -3
  68. package/fesm2022/mintplayer-ng-bootstrap-input-group.mjs.map +1 -1
  69. package/fesm2022/mintplayer-ng-bootstrap-instance-of.mjs +14 -14
  70. package/fesm2022/mintplayer-ng-bootstrap-instance-of.mjs.map +1 -1
  71. package/fesm2022/mintplayer-ng-bootstrap-let.mjs +4 -4
  72. package/fesm2022/mintplayer-ng-bootstrap-let.mjs.map +1 -1
  73. package/fesm2022/mintplayer-ng-bootstrap-linify.mjs +3 -3
  74. package/fesm2022/mintplayer-ng-bootstrap-linify.mjs.map +1 -1
  75. package/fesm2022/mintplayer-ng-bootstrap-list-group.mjs +7 -7
  76. package/fesm2022/mintplayer-ng-bootstrap-list-group.mjs.map +1 -1
  77. package/fesm2022/mintplayer-ng-bootstrap-markdown.mjs +12 -12
  78. package/fesm2022/mintplayer-ng-bootstrap-markdown.mjs.map +1 -1
  79. package/fesm2022/mintplayer-ng-bootstrap-marquee.mjs +3 -3
  80. package/fesm2022/mintplayer-ng-bootstrap-marquee.mjs.map +1 -1
  81. package/fesm2022/mintplayer-ng-bootstrap-modal.mjs +24 -24
  82. package/fesm2022/mintplayer-ng-bootstrap-modal.mjs.map +1 -1
  83. package/fesm2022/mintplayer-ng-bootstrap-multiselect.mjs +24 -24
  84. package/fesm2022/mintplayer-ng-bootstrap-multiselect.mjs.map +1 -1
  85. package/fesm2022/mintplayer-ng-bootstrap-navbar-toggler.mjs +5 -5
  86. package/fesm2022/mintplayer-ng-bootstrap-navbar-toggler.mjs.map +1 -1
  87. package/fesm2022/mintplayer-ng-bootstrap-navbar.mjs +58 -58
  88. package/fesm2022/mintplayer-ng-bootstrap-navbar.mjs.map +1 -1
  89. package/fesm2022/mintplayer-ng-bootstrap-navigation-lock.mjs +8 -8
  90. package/fesm2022/mintplayer-ng-bootstrap-navigation-lock.mjs.map +1 -1
  91. package/fesm2022/mintplayer-ng-bootstrap-no-noscript.mjs +3 -3
  92. package/fesm2022/mintplayer-ng-bootstrap-no-noscript.mjs.map +1 -1
  93. package/fesm2022/mintplayer-ng-bootstrap-offcanvas.mjs +40 -40
  94. package/fesm2022/mintplayer-ng-bootstrap-offcanvas.mjs.map +1 -1
  95. package/fesm2022/mintplayer-ng-bootstrap-ordinal-number.mjs +3 -3
  96. package/fesm2022/mintplayer-ng-bootstrap-ordinal-number.mjs.map +1 -1
  97. package/fesm2022/mintplayer-ng-bootstrap-pagination.mjs +12 -12
  98. package/fesm2022/mintplayer-ng-bootstrap-pagination.mjs.map +1 -1
  99. package/fesm2022/mintplayer-ng-bootstrap-parallax.mjs +6 -6
  100. package/fesm2022/mintplayer-ng-bootstrap-parallax.mjs.map +1 -1
  101. package/fesm2022/mintplayer-ng-bootstrap-placeholder.mjs +7 -7
  102. package/fesm2022/mintplayer-ng-bootstrap-placeholder.mjs.map +1 -1
  103. package/fesm2022/mintplayer-ng-bootstrap-playlist-toggler.mjs +5 -5
  104. package/fesm2022/mintplayer-ng-bootstrap-playlist-toggler.mjs.map +1 -1
  105. package/fesm2022/mintplayer-ng-bootstrap-popover.mjs +20 -20
  106. package/fesm2022/mintplayer-ng-bootstrap-popover.mjs.map +1 -1
  107. package/fesm2022/mintplayer-ng-bootstrap-priority-nav.mjs +30 -30
  108. package/fesm2022/mintplayer-ng-bootstrap-priority-nav.mjs.map +1 -1
  109. package/fesm2022/mintplayer-ng-bootstrap-progress-bar.mjs +17 -17
  110. package/fesm2022/mintplayer-ng-bootstrap-progress-bar.mjs.map +1 -1
  111. package/fesm2022/mintplayer-ng-bootstrap-range.mjs +9 -9
  112. package/fesm2022/mintplayer-ng-bootstrap-range.mjs.map +1 -1
  113. package/fesm2022/mintplayer-ng-bootstrap-rating.mjs +7 -7
  114. package/fesm2022/mintplayer-ng-bootstrap-rating.mjs.map +1 -1
  115. package/fesm2022/mintplayer-ng-bootstrap-resizable.mjs +25 -25
  116. package/fesm2022/mintplayer-ng-bootstrap-resizable.mjs.map +1 -1
  117. package/fesm2022/mintplayer-ng-bootstrap-scheduler.mjs +16 -16
  118. package/fesm2022/mintplayer-ng-bootstrap-scheduler.mjs.map +1 -1
  119. package/fesm2022/mintplayer-ng-bootstrap-scrollspy.mjs +14 -14
  120. package/fesm2022/mintplayer-ng-bootstrap-scrollspy.mjs.map +1 -1
  121. package/fesm2022/mintplayer-ng-bootstrap-searchbox.mjs +24 -24
  122. package/fesm2022/mintplayer-ng-bootstrap-searchbox.mjs.map +1 -1
  123. package/fesm2022/mintplayer-ng-bootstrap-select.mjs +19 -19
  124. package/fesm2022/mintplayer-ng-bootstrap-select.mjs.map +1 -1
  125. package/fesm2022/mintplayer-ng-bootstrap-select2.mjs +20 -20
  126. package/fesm2022/mintplayer-ng-bootstrap-select2.mjs.map +1 -1
  127. package/fesm2022/mintplayer-ng-bootstrap-shell.mjs +11 -11
  128. package/fesm2022/mintplayer-ng-bootstrap-shell.mjs.map +1 -1
  129. package/fesm2022/mintplayer-ng-bootstrap-signature-pad.mjs +7 -7
  130. package/fesm2022/mintplayer-ng-bootstrap-signature-pad.mjs.map +1 -1
  131. package/fesm2022/mintplayer-ng-bootstrap-slugify.mjs +3 -3
  132. package/fesm2022/mintplayer-ng-bootstrap-slugify.mjs.map +1 -1
  133. package/fesm2022/mintplayer-ng-bootstrap-spinner.mjs +7 -7
  134. package/fesm2022/mintplayer-ng-bootstrap-spinner.mjs.map +1 -1
  135. package/fesm2022/mintplayer-ng-bootstrap-split-string.mjs +3 -3
  136. package/fesm2022/mintplayer-ng-bootstrap-split-string.mjs.map +1 -1
  137. package/fesm2022/mintplayer-ng-bootstrap-sticky-footer.mjs +6 -6
  138. package/fesm2022/mintplayer-ng-bootstrap-sticky-footer.mjs.map +1 -1
  139. package/fesm2022/mintplayer-ng-bootstrap-tab-control.mjs +57 -67
  140. package/fesm2022/mintplayer-ng-bootstrap-tab-control.mjs.map +1 -1
  141. package/fesm2022/mintplayer-ng-bootstrap-table.mjs +10 -10
  142. package/fesm2022/mintplayer-ng-bootstrap-table.mjs.map +1 -1
  143. package/fesm2022/mintplayer-ng-bootstrap-timepicker.mjs +8 -8
  144. package/fesm2022/mintplayer-ng-bootstrap-timepicker.mjs.map +1 -1
  145. package/fesm2022/mintplayer-ng-bootstrap-toast.mjs +24 -24
  146. package/fesm2022/mintplayer-ng-bootstrap-toast.mjs.map +1 -1
  147. package/fesm2022/mintplayer-ng-bootstrap-toggle-button.mjs +22 -22
  148. package/fesm2022/mintplayer-ng-bootstrap-toggle-button.mjs.map +1 -1
  149. package/fesm2022/mintplayer-ng-bootstrap-tooltip.mjs +10 -10
  150. package/fesm2022/mintplayer-ng-bootstrap-tooltip.mjs.map +1 -1
  151. package/fesm2022/mintplayer-ng-bootstrap-treeview.mjs +14 -14
  152. package/fesm2022/mintplayer-ng-bootstrap-treeview.mjs.map +1 -1
  153. package/fesm2022/mintplayer-ng-bootstrap-trust-html.mjs +3 -3
  154. package/fesm2022/mintplayer-ng-bootstrap-trust-html.mjs.map +1 -1
  155. package/fesm2022/mintplayer-ng-bootstrap-typeahead.mjs +10 -10
  156. package/fesm2022/mintplayer-ng-bootstrap-typeahead.mjs.map +1 -1
  157. package/fesm2022/mintplayer-ng-bootstrap-uc-first.mjs +3 -3
  158. package/fesm2022/mintplayer-ng-bootstrap-uc-first.mjs.map +1 -1
  159. package/fesm2022/mintplayer-ng-bootstrap-user-agent.mjs +3 -3
  160. package/fesm2022/mintplayer-ng-bootstrap-user-agent.mjs.map +1 -1
  161. package/fesm2022/mintplayer-ng-bootstrap-viewport.mjs +3 -3
  162. package/fesm2022/mintplayer-ng-bootstrap-viewport.mjs.map +1 -1
  163. package/fesm2022/mintplayer-ng-bootstrap-virtual-datatable.mjs +10 -10
  164. package/fesm2022/mintplayer-ng-bootstrap-virtual-datatable.mjs.map +1 -1
  165. package/fesm2022/mintplayer-ng-bootstrap-web-components-scheduler-core.mjs +1356 -0
  166. package/fesm2022/mintplayer-ng-bootstrap-web-components-scheduler-core.mjs.map +1 -0
  167. package/fesm2022/mintplayer-ng-bootstrap-web-components-scheduler.mjs +3819 -0
  168. package/fesm2022/mintplayer-ng-bootstrap-web-components-scheduler.mjs.map +1 -0
  169. package/fesm2022/mintplayer-ng-bootstrap-web-components-splitter.mjs +731 -0
  170. package/fesm2022/mintplayer-ng-bootstrap-web-components-splitter.mjs.map +1 -0
  171. package/fesm2022/mintplayer-ng-bootstrap-web-components-tab-control.mjs +549 -0
  172. package/fesm2022/mintplayer-ng-bootstrap-web-components-tab-control.mjs.map +1 -0
  173. package/fesm2022/mintplayer-ng-bootstrap-word-count.mjs +3 -3
  174. package/fesm2022/mintplayer-ng-bootstrap-word-count.mjs.map +1 -1
  175. package/package.json +20 -6
  176. package/types/mintplayer-ng-bootstrap-dock.d.ts +55 -19
  177. package/types/mintplayer-ng-bootstrap-scheduler.d.ts +2 -2
  178. package/types/mintplayer-ng-bootstrap-tab-control.d.ts +7 -11
  179. package/types/mintplayer-ng-bootstrap-web-components-scheduler-core.d.ts +890 -0
  180. package/types/mintplayer-ng-bootstrap-web-components-scheduler.d.ts +354 -0
  181. package/types/mintplayer-ng-bootstrap-web-components-splitter.d.ts +165 -0
  182. package/types/mintplayer-ng-bootstrap-web-components-tab-control.d.ts +95 -0
@@ -2,11 +2,13 @@ import * as i0 from '@angular/core';
2
2
  import { input, viewChild, TemplateRef, ChangeDetectionStrategy, Component, output, signal, contentChildren, inject, effect, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
3
3
  import { DOCUMENT, NgTemplateOutlet } from '@angular/common';
4
4
  import { html, unsafeCSS, LitElement } from 'lit';
5
+ import '@mintplayer/ng-bootstrap/web-components/tab-control';
6
+ import '@mintplayer/ng-bootstrap/web-components/splitter';
5
7
 
6
8
  class BsDockPaneComponent {
7
9
  constructor() {
8
- this.name = input.required(...(ngDevMode ? [{ debugName: "name" }] : []));
9
- this.title = input(undefined, ...(ngDevMode ? [{ debugName: "title" }] : []));
10
+ this.name = input.required(...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
11
+ this.title = input(undefined, ...(ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []));
10
12
  this.template = viewChild.required(TemplateRef);
11
13
  }
12
14
  ngAfterContentInit() {
@@ -14,10 +16,10 @@ class BsDockPaneComponent {
14
16
  throw new Error('bs-dock-pane requires a unique "name" input.');
15
17
  }
16
18
  }
17
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: BsDockPaneComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
18
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.6", type: BsDockPaneComponent, isStandalone: true, selector: "bs-dock-pane", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "template", first: true, predicate: TemplateRef, descendants: true, isSignal: true }], ngImport: i0, template: `<ng-template><ng-content></ng-content></ng-template>`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
19
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: BsDockPaneComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
20
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.2.11", type: BsDockPaneComponent, isStandalone: true, selector: "bs-dock-pane", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "template", first: true, predicate: TemplateRef, descendants: true, isSignal: true }], ngImport: i0, template: `<ng-template><ng-content></ng-content></ng-template>`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
19
21
  }
20
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: BsDockPaneComponent, decorators: [{
22
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: BsDockPaneComponent, decorators: [{
21
23
  type: Component,
22
24
  args: [{
23
25
  selector: 'bs-dock-pane',
@@ -96,9 +98,7 @@ const styles = unsafeCSS(`:host {
96
98
  .dock-root,
97
99
  .dock-docked,
98
100
  .dock-split,
99
- .dock-split__child,
100
101
  .dock-stack,
101
- .dock-stack__content,
102
102
  .dock-stack__pane {
103
103
  box-sizing: border-box;
104
104
  min-width: 0;
@@ -137,23 +137,27 @@ const styles = unsafeCSS(`:host {
137
137
  display: flex;
138
138
  flex-direction: column;
139
139
  pointer-events: auto;
140
- border: 1px solid rgba(0, 0, 0, 0.3);
140
+ border: 1px solid var(--bs-border-color);
141
141
  border-radius: 0.5rem;
142
- background: rgba(255, 255, 255, 0.92);
143
- box-shadow: 0 16px 32px rgba(15, 23, 42, 0.25);
142
+ background: var(--bs-body-bg);
143
+ box-shadow: var(--bs-box-shadow-lg);
144
144
  overflow: hidden;
145
145
  min-width: 12rem;
146
146
  min-height: 8rem;
147
147
  }
148
148
 
149
+ .dock-floating[data-dragging=true] {
150
+ pointer-events: none;
151
+ }
152
+
149
153
  .dock-floating__chrome {
150
154
  display: flex;
151
155
  align-items: center;
152
156
  gap: 0.5rem;
153
157
  padding: 0.35rem 0.75rem;
154
158
  cursor: move;
155
- background: linear-gradient(to bottom, rgba(148, 163, 184, 0.6), rgba(148, 163, 184, 0.25));
156
- border-bottom: 1px solid rgba(148, 163, 184, 0.5);
159
+ background: linear-gradient(to bottom, rgba(var(--bs-primary-rgb), 0.15), rgba(var(--bs-primary-rgb), 0.05));
160
+ border-bottom: 1px solid var(--bs-primary-border-subtle);
157
161
  user-select: none;
158
162
  -webkit-user-select: none;
159
163
  }
@@ -162,7 +166,7 @@ const styles = unsafeCSS(`:host {
162
166
  flex: 1 1 auto;
163
167
  font-size: 0.875rem;
164
168
  font-weight: 500;
165
- color: rgba(30, 41, 59, 0.95);
169
+ color: var(--bs-body-color);
166
170
  overflow: hidden;
167
171
  text-overflow: ellipsis;
168
172
  white-space: nowrap;
@@ -178,13 +182,13 @@ const styles = unsafeCSS(`:host {
178
182
  position: absolute;
179
183
  pointer-events: auto;
180
184
  z-index: 2;
181
- background: rgba(148, 163, 184, 0.25);
185
+ background: rgba(var(--bs-primary-rgb), 0.1);
182
186
  transition: background 120ms ease;
183
187
  }
184
188
 
185
189
  .dock-floating__resizer:hover,
186
190
  .dock-floating__resizer[data-resizing=true] {
187
- background: rgba(148, 163, 184, 0.4);
191
+ background: rgba(var(--bs-primary-rgb), 0.3);
188
192
  }
189
193
 
190
194
  .dock-floating__resizer--top,
@@ -254,75 +258,10 @@ const styles = unsafeCSS(`:host {
254
258
  }
255
259
 
256
260
  .dock-split {
257
- display: flex;
258
- flex: 1 1 0;
259
- gap: var(--dock-split-gap);
260
- position: relative;
261
- }
262
-
263
- .dock-split[data-direction=vertical] {
264
- flex-direction: column;
265
- }
266
-
267
- .dock-split[data-direction=horizontal] {
268
- flex-direction: row;
269
- }
270
-
271
- .dock-split__child {
272
- display: flex;
273
261
  flex: 1 1 0;
274
262
  position: relative;
275
263
  }
276
264
 
277
- .dock-split__divider {
278
- position: relative;
279
- flex: 0 0 auto;
280
- background: rgba(0, 0, 0, 0.08);
281
- transition: background 120ms ease;
282
- }
283
-
284
- .dock-split[data-direction=horizontal] > .dock-split__divider {
285
- width: 0.5rem;
286
- cursor: col-resize;
287
- /* Extend through perpendicular gaps for visual continuity */
288
- margin-top: calc(var(--dock-split-gap) * -1);
289
- margin-bottom: calc(var(--dock-split-gap) * -1);
290
- }
291
-
292
- .dock-split[data-direction=vertical] > .dock-split__divider {
293
- height: 0.5rem;
294
- cursor: row-resize;
295
- /* Extend through perpendicular gaps for visual continuity */
296
- margin-left: calc(var(--dock-split-gap) * -1);
297
- margin-right: calc(var(--dock-split-gap) * -1);
298
- }
299
-
300
- .dock-split__divider::after {
301
- content: "";
302
- position: absolute;
303
- top: 50%;
304
- left: 50%;
305
- transform: translate(-50%, -50%);
306
- border-radius: 999px;
307
- background: rgba(0, 0, 0, 0.25);
308
- }
309
-
310
- .dock-split[data-direction=horizontal] > .dock-split__divider::after {
311
- width: 0.125rem;
312
- height: 60%;
313
- }
314
-
315
- .dock-split[data-direction=vertical] > .dock-split__divider::after {
316
- width: 60%;
317
- height: 0.125rem;
318
- }
319
-
320
- .dock-split__divider:hover,
321
- .dock-split__divider:focus-visible,
322
- .dock-split__divider[data-resizing=true] {
323
- background: rgba(59, 130, 246, 0.35);
324
- }
325
-
326
265
  .dock-intersection-handle {
327
266
  position: absolute;
328
267
  width: 1rem;
@@ -330,12 +269,12 @@ const styles = unsafeCSS(`:host {
330
269
  margin-left: -0.5rem;
331
270
  margin-top: -0.5rem;
332
271
  border-radius: 0.375rem;
333
- background: rgba(59, 130, 246, 0.2);
334
- border: 1px solid rgba(59, 130, 246, 0.6);
335
- box-shadow: 0 2px 6px rgba(15, 23, 42, 0.2);
272
+ background: var(--bs-primary-bg-subtle);
273
+ border: 1px solid var(--bs-primary-border-subtle);
274
+ box-shadow: var(--bs-box-shadow-sm);
336
275
  cursor: all-scroll;
337
276
  pointer-events: auto;
338
- opacity: 0;
277
+ opacity: 0.45;
339
278
  transition: background 120ms ease, border-color 120ms ease, opacity 120ms ease;
340
279
  }
341
280
 
@@ -343,8 +282,8 @@ const styles = unsafeCSS(`:host {
343
282
  .dock-intersection-handle:focus-visible,
344
283
  .dock-intersection-handle[data-visible=true],
345
284
  .dock-intersection-handle[data-resizing=true] {
346
- background: rgba(59, 130, 246, 0.35);
347
- border-color: rgba(59, 130, 246, 0.9);
285
+ background: rgba(var(--bs-primary-rgb), 0.35);
286
+ border-color: var(--bs-primary);
348
287
  opacity: 1;
349
288
  outline: none;
350
289
  }
@@ -356,84 +295,44 @@ const styles = unsafeCSS(`:host {
356
295
  margin-left: -3px;
357
296
  margin-top: -3px;
358
297
  border-radius: 50%;
359
- background: rgba(59, 130, 246, 0.7);
360
- box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15);
298
+ background: var(--bs-primary);
299
+ box-shadow: 0 0 0 2px var(--bs-primary-bg-subtle);
361
300
  pointer-events: none;
362
301
  z-index: 130;
363
302
  }
364
303
 
365
304
  .dock-stack {
366
- display: flex;
367
- flex-direction: column;
368
305
  flex: 1 1 0;
369
- border: 1px solid rgba(0, 0, 0, 0.2);
306
+ border: 1px solid var(--bs-border-color);
370
307
  border-radius: 0.25rem;
371
- background: rgba(255, 255, 255, 0.75);
308
+ background: var(--bs-body-bg);
372
309
  backdrop-filter: blur(4px);
373
310
  }
374
311
 
375
- .dock-stack__header {
376
- display: flex;
377
- flex-wrap: wrap;
378
- gap: 0.25rem;
379
- padding: 0.25rem;
380
- background: rgba(0, 0, 0, 0.05);
381
- border-bottom: 1px solid rgba(0, 0, 0, 0.15);
382
- }
383
-
384
312
  .dock-tab {
385
- appearance: none;
386
- border: none;
387
- padding: 0.25rem 0.5rem;
388
- border-radius: 0.25rem;
389
- background: transparent;
390
- color: inherit;
391
- font: inherit;
392
313
  cursor: grab;
393
- transition: background 160ms ease;
314
+ display: block;
315
+ padding: 0.5rem 1rem;
316
+ margin: -0.5rem -1rem;
394
317
  }
395
318
 
396
319
  .dock-tab:active {
397
320
  cursor: grabbing;
398
321
  }
399
322
 
400
- .dock-tab:hover {
401
- background: rgba(0, 0, 0, 0.05);
402
- }
403
-
404
- .dock-tab:focus-visible {
405
- outline: 2px solid rgba(59, 130, 246, 0.8);
406
- outline-offset: 1px;
407
- }
408
-
409
- .dock-tab--active {
410
- background: rgba(59, 130, 246, 0.15);
411
- }
412
-
413
- .dock-stack__content {
414
- position: relative;
415
- flex: 1 1 auto;
416
- display: flex;
417
- overflow: hidden;
418
- }
419
-
420
323
  .dock-stack__pane {
421
324
  position: relative;
422
- flex: 1 1 100%;
423
325
  display: flex;
424
326
  flex-direction: column;
425
327
  overflow: hidden;
426
- }
427
-
428
- .dock-stack__pane[hidden] {
429
- display: none !important;
328
+ height: 100%;
430
329
  }
431
330
 
432
331
  .dock-drop-indicator {
433
332
  position: absolute;
434
333
  pointer-events: none;
435
- border: 2px solid rgba(59, 130, 246, 0.9);
436
- background: rgba(59, 130, 246, 0.2);
334
+ border: 2px solid var(--bs-primary);
335
+ background: rgba(var(--bs-primary-rgb), 0.2);
437
336
  border-radius: 0.25rem;
438
337
  opacity: 0;
439
338
  transition: opacity 120ms ease;
@@ -452,8 +351,8 @@ const styles = unsafeCSS(`:host {
452
351
  gap: 0.125rem;
453
352
  padding: 0.125rem;
454
353
  border-radius: 999px;
455
- background: rgba(15, 23, 42, 0.15);
456
- box-shadow: 0 4px 12px rgba(15, 23, 42, 0.25);
354
+ background: var(--bs-tertiary-bg);
355
+ box-shadow: var(--bs-box-shadow);
457
356
  pointer-events: none;
458
357
  transform: translate(-50%, -50%);
459
358
  z-index: 110;
@@ -472,9 +371,9 @@ const styles = unsafeCSS(`:host {
472
371
  align-items: center;
473
372
  justify-content: center;
474
373
  border-radius: 0.375rem;
475
- border: 1px solid rgba(59, 130, 246, 0.4);
476
- background: rgba(255, 255, 255, 0.9);
477
- color: rgba(30, 64, 175, 0.9);
374
+ border: 1px solid var(--bs-primary-border-subtle);
375
+ background: var(--bs-body-bg);
376
+ color: var(--bs-primary);
478
377
  font-size: 0.75rem;
479
378
  line-height: 1;
480
379
  font-weight: 600;
@@ -486,13 +385,13 @@ const styles = unsafeCSS(`:host {
486
385
  .dock-drop-joystick__button[data-active=true],
487
386
  .dock-drop-joystick__button:hover,
488
387
  .dock-drop-joystick__button:focus-visible {
489
- background: rgba(59, 130, 246, 0.25);
490
- border-color: rgba(59, 130, 246, 0.8);
491
- color: rgb(30, 64, 175);
388
+ background: var(--bs-primary-bg-subtle);
389
+ border-color: var(--bs-primary);
390
+ color: var(--bs-primary);
492
391
  }
493
392
 
494
393
  .dock-drop-joystick__button:focus-visible {
495
- outline: 2px solid rgba(59, 130, 246, 0.9);
394
+ outline: 2px solid var(--bs-primary);
496
395
  outline-offset: 1px;
497
396
  }
498
397
 
@@ -519,39 +418,6 @@ class MintDockManagerElement extends LitElement {
519
418
  return [...(super.observedAttributes ?? []), 'layout'];
520
419
  }
521
420
  static { this.instanceCounter = 0; }
522
- renderSnapMarkersForDivider() {
523
- if (!this.showSnapMarkers)
524
- return;
525
- const layer = this.shadowRoot?.querySelector('.dock-intersections-layer, .dock-intersection-layer');
526
- if (!layer)
527
- return;
528
- // Clear previous
529
- Array.from(layer.querySelectorAll('.dock-snap-marker')).forEach((el) => el.remove());
530
- if (!this.resizeState || !this.activeSnapAxis || this.activeSnapTargets.length === 0)
531
- return;
532
- const rootRect = this.rootEl.getBoundingClientRect();
533
- const dRect = this.resizeState.divider.getBoundingClientRect();
534
- if (this.activeSnapAxis === 'x') {
535
- const y = dRect.top + dRect.height / 2 - rootRect.top;
536
- this.activeSnapTargets.forEach((sx) => {
537
- const dot = this.documentRef.createElement('div');
538
- dot.className = 'dock-snap-marker';
539
- dot.style.left = `${rootRect.left + sx - rootRect.left}px`;
540
- dot.style.top = `${y}px`;
541
- layer.appendChild(dot);
542
- });
543
- }
544
- else if (this.activeSnapAxis === 'y') {
545
- const x = dRect.left + dRect.width / 2 - rootRect.left;
546
- this.activeSnapTargets.forEach((sy) => {
547
- const dot = this.documentRef.createElement('div');
548
- dot.className = 'dock-snap-marker';
549
- dot.style.left = `${x}px`;
550
- dot.style.top = `${rootRect.top + sy - rootRect.top}px`;
551
- layer.appendChild(dot);
552
- });
553
- }
554
- }
555
421
  renderSnapMarkersForCorner() {
556
422
  if (!this.showSnapMarkers)
557
423
  return;
@@ -562,17 +428,23 @@ class MintDockManagerElement extends LitElement {
562
428
  if (!this.cornerResizeState)
563
429
  return;
564
430
  const rootRect = this.rootEl.getBoundingClientRect();
565
- // Compute representative center lines from first entries
431
+ // Compute representative center lines from the dividers being resized.
432
+ // st.{hs,vs}[i].container is the <mp-splitter>; the divider lives in its
433
+ // shadow at getSplitterDividers(splitter)[index].
566
434
  let centerX = null;
567
435
  let centerY = null;
568
436
  const st = this.cornerResizeState;
569
437
  if (st.vs.length > 0) {
570
- const vRect = st.vs[0].container.querySelector(':scope > .dock-split__divider')?.getBoundingClientRect();
438
+ const v0 = st.vs[0];
439
+ const vDiv = this.getSplitterDividers(v0.container)[v0.index];
440
+ const vRect = vDiv?.getBoundingClientRect();
571
441
  if (vRect)
572
442
  centerX = vRect.left + vRect.width / 2 - rootRect.left;
573
443
  }
574
444
  if (st.hs.length > 0) {
575
- const hRect = st.hs[0].container.querySelector(':scope > .dock-split__divider')?.getBoundingClientRect();
445
+ const h0 = st.hs[0];
446
+ const hDiv = this.getSplitterDividers(h0.container)[h0.index];
447
+ const hRect = hDiv?.getBoundingClientRect();
576
448
  if (hRect)
577
449
  centerY = hRect.top + hRect.height / 2 - rootRect.top;
578
450
  }
@@ -610,19 +482,16 @@ class MintDockManagerElement extends LitElement {
610
482
  this.floatingLayouts = [];
611
483
  this.titles = {};
612
484
  this.pendingTabDragMetrics = null;
613
- this.resizeState = null;
614
485
  this.dragState = null;
615
486
  this.floatingDragState = null;
616
487
  this.floatingResizeState = null;
617
488
  this.intersectionRaf = null;
618
- this.intersectionHandles = new Map();
489
+ this.rootResizeObserver = null;
490
+ this.dockedMutationObserver = null;
619
491
  this.cornerResizeState = null;
620
492
  this.pointerTrackingActive = false;
621
493
  this.dragPointerTrackingActive = false;
622
494
  this.lastDragPointerPosition = null;
623
- // Localized snapping while dragging a divider
624
- this.activeSnapAxis = null;
625
- this.activeSnapTargets = [];
626
495
  // Localized snapping while dragging an intersection handle
627
496
  this.cornerSnapXTargets = [];
628
497
  this.cornerSnapYTargets = [];
@@ -631,19 +500,19 @@ class MintDockManagerElement extends LitElement {
631
500
  this.pendingDragEndTimeout = null;
632
501
  this.previousSplitSizes = new Map();
633
502
  this.instanceId = `mint-dock-${++MintDockManagerElement.instanceCounter}`;
503
+ // Set windowRef eagerly so connectedCallback's window-level drag listeners
504
+ // (added before firstUpdated runs) can actually attach. Without this,
505
+ // win?.addEventListener was a silent no-op on first connect and HTML5
506
+ // drag-to-detach gestures never reached the dock — the floating wrapper
507
+ // was created but stayed at its conversion-time coordinates because the
508
+ // 'drag' listener that updates its position was never attached.
509
+ this.windowRef = typeof window !== 'undefined' ? window : null;
634
510
  this.onPointerMove = this.onPointerMove.bind(this);
635
511
  this.onPointerUp = this.onPointerUp.bind(this);
636
- this.onDragOver = this.onDragOver.bind(this);
637
- this.onGlobalDragOver = this.onGlobalDragOver.bind(this);
638
- this.onGlobalDragEnd = this.onGlobalDragEnd.bind(this);
639
- this.onDrop = this.onDrop.bind(this);
640
- this.onDragLeave = this.onDragLeave.bind(this);
641
- this.onDrag = this.onDrag.bind(this);
642
- this.onDragMouseMove = this.onDragMouseMove.bind(this);
643
- this.onDragTouchMove = this.onDragTouchMove.bind(this);
644
- this.onDragMouseUp = this.onDragMouseUp.bind(this);
645
- this.onDragTouchEnd = this.onDragTouchEnd.bind(this);
646
- this.onWindowResize = this.onWindowResize.bind(this);
512
+ this.onDragPointerMove = this.onDragPointerMove.bind(this);
513
+ this.onDragPointerUp = this.onDragPointerUp.bind(this);
514
+ this.onDragPointerCancel = this.onDragPointerCancel.bind(this);
515
+ this.onSplitterResize = this.onSplitterResize.bind(this);
647
516
  }
648
517
  render() {
649
518
  return template;
@@ -688,56 +557,47 @@ class MintDockManagerElement extends LitElement {
688
557
  // Tag the docked surface with a root path so it can act as
689
558
  // a drop target when the main layout is empty.
690
559
  this.dockedEl.dataset['path'] = this.formatPath({ type: 'docked', segments: [] });
691
- // Now safe to attach shadow-DOM-targeted event listeners.
692
- this.rootEl.addEventListener('dragover', this.onDragOver);
693
- this.rootEl.addEventListener('drop', this.onDrop);
694
- this.rootEl.addEventListener('dragleave', this.onDragLeave);
695
- this.dropJoystick.addEventListener('dragover', this.onDragOver);
696
- this.dropJoystick.addEventListener('drop', this.onDrop);
697
- this.dropJoystick.addEventListener('dragleave', this.onDragLeave);
698
- this.dropJoystickButtons.forEach((btn) => {
699
- const handler = (e) => {
700
- if (!this.dragState)
701
- return;
702
- const z = btn.dataset['zone'];
703
- if (this.isDropZone(z)) {
704
- this.updateDropJoystickActiveZone(z);
705
- e.preventDefault();
706
- }
707
- };
708
- btn.addEventListener('dragenter', handler);
709
- btn.addEventListener('dragover', handler);
710
- });
560
+ // Drop targeting (drop indicator + joystick zone selection) runs entirely
561
+ // off pointer-based hit-testing in updatePaneDragDropTargetFromPoint and
562
+ // findDropZoneByPoint — no HTML5 dragover/drop/dragleave listeners needed.
711
563
  // Render any layout that was set before the shadow DOM existed.
712
564
  this.renderLayout();
565
+ // Reactive triggers for intersection-handle re-rendering. Each observer
566
+ // wakes scheduleRenderIntersectionHandles() (rAF-coalesced), which means
567
+ // multiple notifications in the same frame collapse to one render and
568
+ // the rAF tick gives <mp-splitter> elements time to populate their
569
+ // shadow roots before we query their dividers.
570
+ this.rootResizeObserver = new ResizeObserver(() => this.scheduleRenderIntersectionHandles());
571
+ this.rootResizeObserver.observe(this.rootEl);
572
+ this.dockedMutationObserver = new MutationObserver(() => this.scheduleRenderIntersectionHandles());
573
+ this.dockedMutationObserver.observe(this.dockedEl, { childList: true, subtree: true });
574
+ // mp-splitter dispatches bubbling 'resizing' / 'resize-end' on user drag;
575
+ // delegating on dockedEl catches every nested splitter without per-instance wiring.
576
+ this.dockedEl.addEventListener('resizing', this.onSplitterResize);
577
+ this.dockedEl.addEventListener('resize-end', this.onSplitterResize);
713
578
  }
714
579
  connectedCallback() {
715
580
  super.connectedCallback();
716
581
  if (!this.hasAttribute('role')) {
717
582
  this.setAttribute('role', 'application');
718
583
  }
719
- const win = this.windowRef;
720
- win?.addEventListener('dragover', this.onGlobalDragOver);
721
- win?.addEventListener('drag', this.onDrag);
722
- win?.addEventListener('dragend', this.onGlobalDragEnd, true);
723
- win?.addEventListener('resize', this.onWindowResize);
724
584
  }
725
585
  disconnectedCallback() {
726
- this.rootEl?.removeEventListener('dragover', this.onDragOver);
727
- this.rootEl?.removeEventListener('drop', this.onDrop);
728
- this.rootEl?.removeEventListener('dragleave', this.onDragLeave);
729
- this.dropJoystick?.removeEventListener('dragover', this.onDragOver);
730
- this.dropJoystick?.removeEventListener('drop', this.onDrop);
731
- this.dropJoystick?.removeEventListener('dragleave', this.onDragLeave);
732
586
  const win = this.windowRef;
733
- win?.removeEventListener('dragover', this.onGlobalDragOver);
734
- win?.removeEventListener('drag', this.onDrag);
735
- win?.removeEventListener('dragend', this.onGlobalDragEnd, true);
736
587
  this.stopDragPointerTracking();
737
588
  win?.removeEventListener('pointermove', this.onPointerMove);
738
589
  win?.removeEventListener('pointerup', this.onPointerUp);
739
590
  this.pointerTrackingActive = false;
740
- win?.removeEventListener('resize', this.onWindowResize);
591
+ this.rootResizeObserver?.disconnect();
592
+ this.rootResizeObserver = null;
593
+ this.dockedMutationObserver?.disconnect();
594
+ this.dockedMutationObserver = null;
595
+ this.dockedEl?.removeEventListener('resizing', this.onSplitterResize);
596
+ this.dockedEl?.removeEventListener('resize-end', this.onSplitterResize);
597
+ if (this.intersectionRaf !== null) {
598
+ this.windowRef?.clearTimeout(this.intersectionRaf);
599
+ this.intersectionRaf = null;
600
+ }
741
601
  super.disconnectedCallback();
742
602
  }
743
603
  attributeChangedCallback(name, _oldValue, newValue) {
@@ -761,11 +621,52 @@ class MintDockManagerElement extends LitElement {
761
621
  }
762
622
  set layout(value) {
763
623
  const snapshot = this.ensureSnapshot(value);
624
+ // While a drag/resize is in flight, the dock manager is the source of
625
+ // truth for layout state — its mid-drag mutations (e.g. floating bounds
626
+ // updated every mousemove, or a stack split during a pane-drag-to-floating
627
+ // conversion) race the host's two-way binding round-trip. The host re-
628
+ // feeds the layout we *just* dispatched via `dock-layout-changed`, but by
629
+ // the time the round-trip arrives the user has moved the cursor again, so
630
+ // the structural-equality guard below would let it through and clobber the
631
+ // in-progress state (e.g. snap a freshly-detached floating window back to
632
+ // the converted-at coordinates instead of letting it follow the cursor).
633
+ // Reject any external layout write during interaction; the host will sync
634
+ // back to the dock's final state when interaction ends and the dock fires
635
+ // a fresh dock-layout-changed event.
636
+ if (this.isInteracting())
637
+ return;
638
+ // Skip renderLayout when the incoming layout is structurally identical
639
+ // to the current state. After a divider drag the dock dispatches
640
+ // dock-layout-changed; an Angular host doing two-way binding will feed
641
+ // that snapshot right back through `[layout]` (and through the
642
+ // `[attr.layout]` round-trip). Without this guard, every drag-end
643
+ // tears down and rebuilds the whole splitter tree, giving a one-frame
644
+ // flash of `flex: 1 1 0` equal-share before the pin restores sizes.
645
+ const currentJson = JSON.stringify({
646
+ root: this.rootLayout,
647
+ floating: this.floatingLayouts,
648
+ titles: this.titles,
649
+ });
650
+ const newJson = JSON.stringify(snapshot);
651
+ if (currentJson === newJson)
652
+ return;
764
653
  this.rootLayout = this.cloneLayoutNode(snapshot.root);
765
654
  this.floatingLayouts = this.cloneFloatingArray(snapshot.floating);
766
655
  this.titles = snapshot.titles ? { ...snapshot.titles } : {};
767
656
  this.renderLayout();
768
657
  }
658
+ /**
659
+ * True while the user is actively interacting with the dock — pane drag,
660
+ * floating window drag, floating window resize, or intersection corner
661
+ * resize. The `set layout` setter consults this to refuse external
662
+ * round-trips that would overwrite in-progress drag state.
663
+ */
664
+ isInteracting() {
665
+ return !!(this.dragState ||
666
+ this.floatingDragState ||
667
+ this.floatingResizeState ||
668
+ this.cornerResizeState);
669
+ }
769
670
  get snapshot() {
770
671
  return this.layout;
771
672
  }
@@ -831,7 +732,10 @@ class MintDockManagerElement extends LitElement {
831
732
  this.dockedEl.appendChild(fragment);
832
733
  }
833
734
  this.renderFloatingPanes();
834
- this.scheduleRenderIntersectionHandles();
735
+ // Note: intersection handles are repositioned reactively via observers
736
+ // wired up in firstUpdated (rootResizeObserver, dockedMutationObserver,
737
+ // and delegated 'resizing' / 'resize-end' events). The MutationObserver
738
+ // on dockedEl fires when the renderNode subtree above is appended.
835
739
  }
836
740
  renderNode(node, path, floatingIndex) {
837
741
  if (node.kind === 'split') {
@@ -926,104 +830,88 @@ class MintDockManagerElement extends LitElement {
926
830
  this.floatingLayerEl.appendChild(wrapper);
927
831
  });
928
832
  }
929
- onWindowResize() {
930
- // Recompute intersection handles on window resize
833
+ onSplitterResize() {
834
+ // mp-splitter dispatches 'resizing' continuously during a divider drag
835
+ // and 'resize-end' when the user releases. Both keep the handle glued
836
+ // to the new intersection coordinate.
931
837
  this.scheduleRenderIntersectionHandles();
932
838
  }
933
839
  scheduleRenderIntersectionHandles() {
934
- this.intersectionRaf = null;
935
- this.renderIntersectionHandles();
840
+ if (this.intersectionRaf !== null)
841
+ return;
842
+ const win = this.windowRef;
843
+ if (!win)
844
+ return;
845
+ // Defer with setTimeout(5) instead of rAF so we run AFTER any
846
+ // flex-redistribution settles. Sequence we have to wait through:
847
+ // (1) DOM mutation (e.g. panel removed by drop)
848
+ // (2) microtasks: <mp-splitter>'s slotchange + size-pinning rAF
849
+ // (3) layout flush
850
+ // A bare rAF can fire before (2) resolves, so getBoundingClientRect on
851
+ // the dividers reads a transient flex-distributed position and the
852
+ // glyph lands ~tens of pixels off. 5ms is past the microtask queue and
853
+ // past splitter's pinning rAF in practice, so the divider rects we
854
+ // read are the settled, post-pin values.
855
+ this.intersectionRaf = win.setTimeout(() => {
856
+ this.intersectionRaf = null;
857
+ this.renderIntersectionHandles();
858
+ }, 5);
936
859
  }
937
860
  renderIntersectionHandles() {
938
861
  const layer = this.shadowRoot?.querySelector('.dock-intersections-layer, .dock-intersection-layer');
939
862
  if (!layer)
940
863
  return;
941
- // Keep existing handles; we will diff and update positions
942
- // 1) Clean up legacy handles (created before keying) that lack a data-key
943
- Array.from(layer.querySelectorAll('.dock-intersection-handle'))
944
- .filter((el) => !el.dataset['key'])
945
- .forEach((el) => el.remove());
946
- // 2) Rebuild the internal map from DOM to avoid drifting state and dedupe duplicates
947
- const domByKey = new Map();
948
- Array.from(layer.querySelectorAll('.dock-intersection-handle[data-key]')).forEach((el) => {
949
- const key = el.dataset['key'] ?? '';
950
- if (!key)
951
- return;
952
- if (domByKey.has(key)) {
953
- // Remove duplicates with the same key, keep the first one
954
- el.remove();
955
- return;
956
- }
957
- domByKey.set(key, el);
958
- // Ensure listener is attached only once
959
- if (!el.dataset['listener']) {
960
- el.dataset['listener'] = '1';
961
- // Listener will be (re)assigned later when we know the current h/v pair
962
- }
963
- });
964
- // Sync internal map with DOM
965
- this.intersectionHandles = domByKey;
966
864
  const rootRect = this.rootEl.getBoundingClientRect();
967
- // If a corner resize is active, only update that handle's position and avoid creating new ones
865
+ // Active corner-resize: keep st.handle alive (it owns pointer capture and
866
+ // the cornerResizeState references it). Update its position from current
867
+ // divider rects, drop every other handle by reference.
968
868
  if (this.cornerResizeState) {
969
869
  const st = this.cornerResizeState;
970
870
  const h0 = st.hs[0];
971
871
  const v0 = st.vs[0];
972
- const hPathStr = this.formatPath(h0.path);
973
- const vPathStr = this.formatPath(v0.path);
974
- const key = `${hPathStr}:${h0.index}|${vPathStr}:${v0.index}`;
975
- // Find divider elements corresponding to active paths
976
- const hDiv = this.shadowRoot?.querySelector(`.dock-split__divider[data-path="${hPathStr}"][data-index="${h0.index}"]`);
977
- const vDiv = this.shadowRoot?.querySelector(`.dock-split__divider[data-path="${vPathStr}"][data-index="${v0.index}"]`);
872
+ const hSplitter = this.findSplitterByPath(h0.path.segments);
873
+ const vSplitter = this.findSplitterByPath(v0.path.segments);
874
+ const hDiv = hSplitter ? this.getSplitterDividers(hSplitter)[h0.index] : null;
875
+ const vDiv = vSplitter ? this.getSplitterDividers(vSplitter)[v0.index] : null;
978
876
  if (hDiv && vDiv) {
979
877
  const hr = hDiv.getBoundingClientRect();
980
878
  const vr = vDiv.getBoundingClientRect();
981
879
  const x = vr.left + vr.width / 2 - rootRect.left;
982
880
  const y = hr.top + hr.height / 2 - rootRect.top;
983
- const handle = st.handle;
984
- if (!handle.dataset['key']) {
985
- handle.dataset['key'] = key;
986
- }
987
- this.intersectionHandles.set(key, handle);
988
- handle.style.left = `${x}px`;
989
- handle.style.top = `${y}px`;
990
- // Remove any other handles that don't match the active key
881
+ st.handle.style.left = `${x}px`;
882
+ st.handle.style.top = `${y}px`;
991
883
  Array.from(layer.querySelectorAll('.dock-intersection-handle')).forEach((el) => {
992
- if ((el.dataset['key'] ?? '') !== key) {
884
+ if (el !== st.handle)
993
885
  el.remove();
994
- }
995
886
  });
996
- // Normalize internal map as well
997
- this.intersectionHandles = new Map([[key, handle]]);
998
887
  }
999
888
  return;
1000
889
  }
1001
- const allDividers = Array.from(this.shadowRoot?.querySelectorAll('.dock-split__divider') ?? []);
890
+ // Idle path: full clear + rebuild. Cheaper to reason about than incremental
891
+ // diffing, and handles' positions are always derived from current divider
892
+ // rects so a layout change (drop, splitter restructure, flex redistribution)
893
+ // can never leave a stale glyph behind.
894
+ layer.replaceChildren();
1002
895
  const hDividers = [];
1003
896
  const vDividers = [];
1004
- allDividers.forEach((el) => {
1005
- const orientation = el.dataset['orientation'] ?? undefined;
1006
- const rect = el.getBoundingClientRect();
1007
- const container = el.closest('.dock-split');
1008
- const path = this.parsePath(el.dataset['path']);
1009
- const pathStr = el.dataset['path'] ?? '';
1010
- const index = Number.parseInt(el.dataset['index'] ?? '', 10);
1011
- if (!container || !Number.isFinite(index))
1012
- return;
1013
- const info = { el, rect, path, pathStr, index, container };
1014
- // Note: node.direction === 'horizontal' means the split lays out children left-to-right,
1015
- // which yields a VERTICAL divider bar. So mapping is inverted here.
1016
- if (orientation === 'horizontal') {
1017
- vDividers.push(info);
1018
- }
1019
- else if (orientation === 'vertical') {
1020
- hDividers.push(info);
1021
- }
897
+ const allSplitters = Array.from(this.shadowRoot?.querySelectorAll('.dock-split') ?? []);
898
+ allSplitters.forEach((splitter) => {
899
+ const direction = splitter.dataset['direction'];
900
+ const pathStr = splitter.dataset['path'] ?? '';
901
+ this.getSplitterDividers(splitter).forEach((el, index) => {
902
+ const info = { rect: el.getBoundingClientRect(), pathStr, index };
903
+ // direction='horizontal' means children flow left-to-right, so the
904
+ // divider bars between them are VERTICAL (and vice-versa).
905
+ if (direction === 'horizontal')
906
+ vDividers.push(info);
907
+ else if (direction === 'vertical')
908
+ hDividers.push(info);
909
+ });
1022
910
  });
1023
- const desiredKeys = new Set();
1024
- const tol = 24; // px tolerance to account for gaps and subpixel layout
1025
- const groupMap = new Map();
1026
- const groupPairs = new Map();
911
+ // Group intersections that round to the same on-screen pixel, so two
912
+ // sibling splitters whose dividers happen to overlap share one handle.
913
+ const tol = 24;
914
+ const groups = new Map();
1027
915
  hDividers.forEach((h) => {
1028
916
  const hCenterY = h.rect.top + h.rect.height / 2;
1029
917
  vDividers.forEach((v) => {
@@ -1034,53 +922,33 @@ class MintDockManagerElement extends LitElement {
1034
922
  return;
1035
923
  const x = vCenterX - rootRect.left;
1036
924
  const y = hCenterY - rootRect.top;
1037
- const key = `${h.pathStr}:${h.index}|${v.pathStr}:${v.index}`;
1038
925
  const gk = `${Math.round(x)}:${Math.round(y)}`;
1039
- let handle = groupMap.get(gk);
1040
- if (!handle) {
1041
- // Try reuse via existing pair mapping
1042
- handle = this.intersectionHandles.get(key) ?? null;
1043
- if (!handle) {
1044
- handle = this.documentRef.createElement('div');
1045
- handle.classList.add('dock-intersection-handle', 'glyph');
1046
- handle.setAttribute('role', 'separator');
1047
- handle.setAttribute('aria-label', 'Resize split intersection');
1048
- handle.dataset['key'] = key;
1049
- handle.dataset['listener'] = '1';
1050
- handle.addEventListener('pointerdown', (ev) => this.beginCornerResize(ev, h, v, handle));
1051
- handle.addEventListener('dblclick', (ev) => this.onIntersectionDoubleClick(ev, handle));
1052
- layer.appendChild(handle);
1053
- }
1054
- groupMap.set(gk, handle);
1055
- }
1056
- // Track pairs for this group and map all pair keys to the same handle
1057
- const arr = groupPairs.get(gk) ?? [];
1058
- arr.push({ h: { pathStr: h.pathStr ?? '', index: h.index }, v: { pathStr: v.pathStr ?? '', index: v.index } });
1059
- groupPairs.set(gk, arr);
1060
- this.intersectionHandles.set(key, handle);
1061
- // Update position for the grouped handle
1062
- handle.style.left = `${x}px`;
1063
- handle.style.top = `${y}px`;
926
+ const group = groups.get(gk) ?? { x, y, pairs: [] };
927
+ group.pairs.push({ h: { pathStr: h.pathStr, index: h.index }, v: { pathStr: v.pathStr, index: v.index } });
928
+ groups.set(gk, group);
1064
929
  });
1065
930
  });
1066
- // Attach grouped pairs data to each handle and prune stale ones
1067
- const keep = new Set(groupMap.values());
1068
- groupMap.forEach((handle, gk) => {
1069
- const pairs = groupPairs.get(gk) ?? [];
1070
- handle.dataset['pairs'] = JSON.stringify(pairs);
931
+ groups.forEach((group, gk) => {
932
+ const handle = this.documentRef.createElement('div');
933
+ handle.classList.add('dock-intersection-handle', 'glyph');
934
+ handle.setAttribute('role', 'separator');
935
+ handle.setAttribute('aria-label', 'Resize split intersection');
936
+ const firstPair = group.pairs[0];
937
+ const key = `${firstPair.h.pathStr}:${firstPair.h.index}|${firstPair.v.pathStr}:${firstPair.v.index}`;
938
+ handle.dataset['key'] = key;
939
+ handle.dataset['pairs'] = JSON.stringify(group.pairs);
940
+ handle.style.left = `${group.x}px`;
941
+ handle.style.top = `${group.y}px`;
942
+ // beginCornerResize/onIntersectionDoubleClick read data-pairs to
943
+ // reconstruct the (h, v) pair list, so the (h, v) args we pass here
944
+ // are only used as a fallback when data-pairs is empty — safe to use
945
+ // the first pair's structure as the seed.
946
+ const seedH = { path: this.parsePath(firstPair.h.pathStr), index: firstPair.h.index, container: this.findSplitterByPath(this.parsePath(firstPair.h.pathStr)?.segments ?? []) ?? this.rootEl, rect: new DOMRect() };
947
+ const seedV = { path: this.parsePath(firstPair.v.pathStr), index: firstPair.v.index, container: this.findSplitterByPath(this.parsePath(firstPair.v.pathStr)?.segments ?? []) ?? this.rootEl, rect: new DOMRect() };
948
+ handle.addEventListener('pointerdown', (ev) => this.beginCornerResize(ev, seedH, seedV, handle));
949
+ handle.addEventListener('dblclick', (ev) => this.onIntersectionDoubleClick(ev, handle));
950
+ layer.appendChild(handle);
1071
951
  });
1072
- Array.from(layer.querySelectorAll('.dock-intersection-handle')).forEach((el) => {
1073
- if (!keep.has(el)) {
1074
- el.remove();
1075
- }
1076
- });
1077
- // Reset intersectionHandles to only currently mapped keys
1078
- const newMap = new Map();
1079
- groupPairs.forEach((pairs, gk) => {
1080
- const handle = groupMap.get(gk);
1081
- pairs.forEach((p) => newMap.set(`${p.h.pathStr}:${p.h.index}|${p.v.pathStr}:${p.v.index}`, handle));
1082
- });
1083
- this.intersectionHandles = newMap;
1084
952
  }
1085
953
  beginCornerResize(event, h, v, handle) {
1086
954
  event.preventDefault();
@@ -1093,20 +961,22 @@ class MintDockManagerElement extends LitElement {
1093
961
  const path = this.parsePath(pathStr);
1094
962
  if (!path)
1095
963
  return;
1096
- const div = this.shadowRoot?.querySelector(`.dock-split__divider[data-path="${pathStr}"][data-index="${index}"]`) ?? null;
1097
- const container = div?.closest('.dock-split');
1098
- if (!container)
964
+ const splitter = this.findSplitterByPath(path.segments);
965
+ if (!splitter)
1099
966
  return;
1100
- if (axis === 'h') {
1101
- const children = Array.from(container.querySelectorAll(':scope > .dock-split__child'));
1102
- const initial = children.map((c) => c.getBoundingClientRect().height);
1103
- hs.push({ path, index, container, initialSizes: initial, before: initial[index], after: initial[index + 1] });
1104
- }
1105
- else {
1106
- const children = Array.from(container.querySelectorAll(':scope > .dock-split__child'));
1107
- const initial = children.map((c) => c.getBoundingClientRect().width);
1108
- vs.push({ path, index, container, initialSizes: initial, before: initial[index], after: initial[index + 1] });
1109
- }
967
+ // Initial pixel sizes come from each panel-wrapper inside the splitter's
968
+ // shadow root. We capture them once on pointerdown and feed deltas to
969
+ // setPanelSizes() during the drag.
970
+ const panels = this.getSplitterPanels(splitter);
971
+ if (panels.length === 0)
972
+ return;
973
+ const dim = axis === 'h' ? 'height' : 'width';
974
+ const initial = panels.map((p) => p.getBoundingClientRect()[dim]);
975
+ const entry = { path, index, container: splitter, initialSizes: initial, before: initial[index], after: initial[index + 1] };
976
+ if (axis === 'h')
977
+ hs.push(entry);
978
+ else
979
+ vs.push(entry);
1110
980
  };
1111
981
  if (parsed.length > 0) {
1112
982
  parsed.forEach((p) => { ensureHV(p.h.pathStr, p.h.index, 'h'); ensureHV(p.v.pathStr, p.v.index, 'v'); });
@@ -1138,44 +1008,47 @@ class MintDockManagerElement extends LitElement {
1138
1008
  // Compute localized snap targets for this intersection
1139
1009
  try {
1140
1010
  const rootRect = this.rootEl.getBoundingClientRect();
1141
- // Use first pair to define the crossing lines
1011
+ // Use first pair to define the crossing lines. Resolve dividers via
1012
+ // each splitter's shadow root.
1142
1013
  let centerX = null;
1143
1014
  let centerY = null;
1144
- // Resolve one vertical bar (from vs) and one horizontal bar (from hs)
1145
1015
  if (vs.length > 0) {
1146
1016
  const vPair = vs[0];
1147
- const vPathStr = this.formatPath(vPair.path);
1148
- const vDiv = this.shadowRoot?.querySelector(`.dock-split__divider[data-path="${vPathStr}"][data-index="${vPair.index}"]`) ?? null;
1017
+ const vDiv = this.getSplitterDividers(vPair.container)[vPair.index];
1149
1018
  const vr = vDiv?.getBoundingClientRect();
1150
1019
  if (vr)
1151
1020
  centerX = vr.left + vr.width / 2;
1152
1021
  }
1153
1022
  if (hs.length > 0) {
1154
1023
  const hPair = hs[0];
1155
- const hPathStr = this.formatPath(hPair.path);
1156
- const hDiv = this.shadowRoot?.querySelector(`.dock-split__divider[data-path="${hPathStr}"][data-index="${hPair.index}"]`) ?? null;
1024
+ const hDiv = this.getSplitterDividers(hPair.container)[hPair.index];
1157
1025
  const hr = hDiv?.getBoundingClientRect();
1158
1026
  if (hr)
1159
1027
  centerY = hr.top + hr.height / 2;
1160
1028
  }
1161
1029
  const xTargets = [];
1162
1030
  const yTargets = [];
1163
- const allDividers = Array.from(this.shadowRoot?.querySelectorAll('.dock-split__divider') ?? []);
1164
- allDividers.forEach((el) => {
1165
- const o = el.dataset['orientation'] ?? undefined;
1166
- const r = el.getBoundingClientRect();
1167
- if (o === 'horizontal' && centerY != null) {
1168
- // vertical bar contributes X if it crosses centerY
1169
- if (centerY >= r.top && centerY <= r.bottom) {
1170
- xTargets.push(r.left + r.width / 2 - rootRect.left);
1031
+ // Iterate every splitter, then flat-map its shadow dividers — a
1032
+ // splitter's data-direction tells us whether its bars are vertical
1033
+ // (horizontal split) or horizontal (vertical split).
1034
+ const allSplitters = Array.from(this.shadowRoot?.querySelectorAll('.dock-split') ?? []);
1035
+ allSplitters.forEach((splitter) => {
1036
+ const direction = splitter.dataset['direction'] ?? undefined;
1037
+ this.getSplitterDividers(splitter).forEach((el) => {
1038
+ const r = el.getBoundingClientRect();
1039
+ if (direction === 'horizontal' && centerY != null) {
1040
+ // vertical bar → contributes X if it crosses centerY
1041
+ if (centerY >= r.top && centerY <= r.bottom) {
1042
+ xTargets.push(r.left + r.width / 2 - rootRect.left);
1043
+ }
1171
1044
  }
1172
- }
1173
- else if (o === 'vertical' && centerX != null) {
1174
- // horizontal bar contributes Y if it crosses centerX
1175
- if (centerX >= r.left && centerX <= r.right) {
1176
- yTargets.push(r.top + r.height / 2 - rootRect.top);
1045
+ else if (direction === 'vertical' && centerX != null) {
1046
+ // horizontal bar contributes Y if it crosses centerX
1047
+ if (centerX >= r.left && centerX <= r.right) {
1048
+ yTargets.push(r.top + r.height / 2 - rootRect.top);
1049
+ }
1177
1050
  }
1178
- }
1051
+ });
1179
1052
  });
1180
1053
  this.cornerSnapXTargets = xTargets;
1181
1054
  this.cornerSnapYTargets = yTargets;
@@ -1236,49 +1109,37 @@ class MintDockManagerElement extends LitElement {
1236
1109
  if (bestDist <= tol)
1237
1110
  clientY = best;
1238
1111
  }
1239
- // Update all horizontal bars (vertical splits) with Y delta
1240
- state.hs.forEach((h) => {
1241
- const node = this.resolveSplitNode(h.path);
1112
+ // Apply the new pair sizes to one splitter's panel-wrappers via
1113
+ // mp-splitter's setPanelSizes(pixels) API. We persist the normalized
1114
+ // ratios on the layout node so renderSplit's initial sizing stays in sync.
1115
+ const applyPairSize = (entry, delta) => {
1116
+ const node = this.resolveSplitNode(entry.path);
1242
1117
  if (!node)
1243
1118
  return;
1244
- const deltaY = clientY - h.startY;
1245
1119
  const minSize = 48;
1246
- const pairTotal = h.beforeSize + h.afterSize;
1247
- let newBefore = Math.min(Math.max(h.beforeSize + deltaY, minSize), pairTotal - minSize);
1120
+ const pairTotal = entry.beforeSize + entry.afterSize;
1121
+ let newBefore = Math.min(Math.max(entry.beforeSize + delta, minSize), pairTotal - minSize);
1248
1122
  newBefore = snapValue(newBefore, pairTotal, event.shiftKey);
1249
1123
  const newAfter = pairTotal - newBefore;
1250
- const sizesPx = [...h.initialSizes];
1251
- sizesPx[h.index] = newBefore;
1252
- sizesPx[h.index + 1] = newAfter;
1124
+ const sizesPx = [...entry.initialSizes];
1125
+ sizesPx[entry.index] = newBefore;
1126
+ sizesPx[entry.index + 1] = newAfter;
1253
1127
  const total = sizesPx.reduce((a, s) => a + s, 0);
1254
- const normalized = total > 0 ? sizesPx.map((s) => s / total) : [];
1255
- node.sizes = normalized;
1256
- const children = Array.from(h.container.querySelectorAll(':scope > .dock-split__child'));
1257
- normalized.forEach((size, idx) => { if (children[idx])
1258
- children[idx].style.flex = `${Math.max(size, 0)} 1 0`; });
1259
- });
1260
- // Update all vertical bars (horizontal splits) with X delta
1261
- state.vs.forEach((v) => {
1262
- const node = this.resolveSplitNode(v.path);
1263
- if (!node)
1264
- return;
1265
- const deltaX = clientX - v.startX;
1266
- const minSize = 48;
1267
- const pairTotal = v.beforeSize + v.afterSize;
1268
- let newBefore = Math.min(Math.max(v.beforeSize + deltaX, minSize), pairTotal - minSize);
1269
- newBefore = snapValue(newBefore, pairTotal, event.shiftKey);
1270
- const newAfter = pairTotal - newBefore;
1271
- const sizesPx = [...v.initialSizes];
1272
- sizesPx[v.index] = newBefore;
1273
- sizesPx[v.index + 1] = newAfter;
1274
- const total = sizesPx.reduce((a, s) => a + s, 0);
1275
- const normalized = total > 0 ? sizesPx.map((s) => s / total) : [];
1276
- node.sizes = normalized;
1277
- const children = Array.from(v.container.querySelectorAll(':scope > .dock-split__child'));
1278
- normalized.forEach((size, idx) => { if (children[idx])
1279
- children[idx].style.flex = `${Math.max(size, 0)} 1 0`; });
1280
- });
1128
+ node.sizes = total > 0 ? sizesPx.map((s) => s / total) : [];
1129
+ entry.container
1130
+ .setPanelSizes?.(sizesPx);
1131
+ };
1132
+ // Update all horizontal bars (vertical splits) with Y delta, then all
1133
+ // vertical bars (horizontal splits) with X delta.
1134
+ state.hs.forEach((h) => applyPairSize(h, clientY - h.startY));
1135
+ state.vs.forEach((v) => applyPairSize(v, clientX - v.startX));
1281
1136
  this.dispatchLayoutChanged();
1137
+ // setPanelSizes() is programmatic and doesn't fire 'resizing' events, so
1138
+ // the delegated listener on dockedEl doesn't wake during a corner drag.
1139
+ // Schedule the handle repositioning ourselves; renderIntersectionHandles
1140
+ // has a fast-path for the active cornerResizeState that just updates
1141
+ // left/top from the new divider rects.
1142
+ this.scheduleRenderIntersectionHandles();
1282
1143
  }
1283
1144
  endCornerResize(pointerId) {
1284
1145
  const state = this.cornerResizeState;
@@ -1322,7 +1183,28 @@ class MintDockManagerElement extends LitElement {
1322
1183
  let hasStored = false;
1323
1184
  splitKeys.forEach((k) => { if (this.previousSplitSizes.has(k))
1324
1185
  hasStored = true; });
1325
- const applySizes = (pathStr, mutate) => {
1186
+ // Persist `node.sizes` (normalized) and push pixel sizes into the
1187
+ // matching <mp-splitter> via setPanelSizes(). The splitter's panel
1188
+ // wrappers live in its shadow DOM, so direct flex mutation is no
1189
+ // longer an option.
1190
+ const pushSizesToSplitter = (path, normalized) => {
1191
+ const splitter = this.findSplitterByPath(path.segments);
1192
+ if (!splitter)
1193
+ return;
1194
+ const direction = splitter.dataset['direction'] ?? 'horizontal';
1195
+ const containerSize = direction === 'horizontal'
1196
+ ? splitter.getBoundingClientRect().width
1197
+ : splitter.getBoundingClientRect().height;
1198
+ if (!Number.isFinite(containerSize) || containerSize <= 0)
1199
+ return;
1200
+ const totalWeight = normalized.reduce((s, w) => s + Math.max(w, 0), 0);
1201
+ if (totalWeight <= 0)
1202
+ return;
1203
+ const px = normalized.map((w) => (Math.max(w, 0) / totalWeight) * containerSize);
1204
+ splitter
1205
+ .setPanelSizes?.(px);
1206
+ };
1207
+ const applySizes = (pathStr, dividerIndex, mutate) => {
1326
1208
  const path = this.parsePath(pathStr);
1327
1209
  if (!path)
1328
1210
  return;
@@ -1330,35 +1212,20 @@ class MintDockManagerElement extends LitElement {
1330
1212
  if (!node)
1331
1213
  return;
1332
1214
  const sizes = this.normalizeSizesArray(node.sizes ?? [], node.children.length);
1333
- // Find divider index from any divider belonging to this path
1334
- const divEl = this.shadowRoot?.querySelector(`.dock-split__divider[data-path="${pathStr}"]`);
1335
- const index = divEl ? Number.parseInt(divEl.dataset['index'] ?? '0', 10) : 0;
1336
- const newSizes = mutate([...sizes], index);
1215
+ const newSizes = mutate([...sizes], dividerIndex);
1337
1216
  node.sizes = newSizes;
1338
- const segments = path.segments.join('/');
1339
- const container = this.shadowRoot?.querySelector(`.dock-split[data-path="${segments}"]`);
1340
- if (container) {
1341
- const children = Array.from(container.querySelectorAll(':scope > .dock-split__child'));
1342
- newSizes.forEach((s, i) => { if (children[i])
1343
- children[i].style.flex = `${Math.max(s, 0)} 1 0`; });
1344
- }
1217
+ pushSizesToSplitter(path, newSizes);
1345
1218
  };
1346
1219
  if (hasStored) {
1347
1220
  // Restore stored sizes
1348
1221
  this.previousSplitSizes.forEach((sizes, pathStr) => {
1349
1222
  const path = this.parsePath(pathStr);
1350
1223
  const node = path ? this.resolveSplitNode(path) : null;
1351
- if (!node)
1224
+ if (!node || !path)
1352
1225
  return;
1353
1226
  const norm = this.normalizeSizesArray(sizes, node.children.length);
1354
1227
  node.sizes = norm;
1355
- const segments = path.segments.join('/');
1356
- const container = this.shadowRoot?.querySelector(`.dock-split[data-path="${segments}"]`);
1357
- if (container) {
1358
- const children = Array.from(container.querySelectorAll(':scope > .dock-split__child'));
1359
- norm.forEach((s, i) => { if (children[i])
1360
- children[i].style.flex = `${Math.max(s, 0)} 1 0`; });
1361
- }
1228
+ pushSizesToSplitter(path, norm);
1362
1229
  });
1363
1230
  this.previousSplitSizes.clear();
1364
1231
  }
@@ -1376,7 +1243,7 @@ class MintDockManagerElement extends LitElement {
1376
1243
  }
1377
1244
  touched.add(key);
1378
1245
  });
1379
- applySizes(p.h.pathStr, (sizes, idx) => {
1246
+ const equalize = (sizes, idx) => {
1380
1247
  const total = (sizes[idx] ?? 0) + (sizes[idx + 1] ?? 0);
1381
1248
  if (total <= 0)
1382
1249
  return sizes;
@@ -1384,16 +1251,9 @@ class MintDockManagerElement extends LitElement {
1384
1251
  sizes[idx + 1] = total / 2;
1385
1252
  const sum = sizes.reduce((a, s) => a + s, 0);
1386
1253
  return sum > 0 ? sizes.map((s) => s / sum) : sizes;
1387
- });
1388
- applySizes(p.v.pathStr, (sizes, idx) => {
1389
- const total = (sizes[idx] ?? 0) + (sizes[idx + 1] ?? 0);
1390
- if (total <= 0)
1391
- return sizes;
1392
- sizes[idx] = total / 2;
1393
- sizes[idx + 1] = total / 2;
1394
- const sum = sizes.reduce((a, s) => a + s, 0);
1395
- return sum > 0 ? sizes.map((s) => s / sum) : sizes;
1396
- });
1254
+ };
1255
+ applySizes(p.h.pathStr, p.h.index, equalize);
1256
+ applySizes(p.v.pathStr, p.v.index, equalize);
1397
1257
  });
1398
1258
  }
1399
1259
  this.dispatchLayoutChanged();
@@ -1481,11 +1341,13 @@ class MintDockManagerElement extends LitElement {
1481
1341
  }
1482
1342
  try {
1483
1343
  state.handle.releasePointerCapture(state.pointerId);
1484
- delete state.handle.dataset['resizing'];
1485
1344
  }
1486
1345
  catch (err) {
1487
1346
  /* no-op */
1488
1347
  }
1348
+ // Clear outside the try so a thrown releasePointerCapture (capture
1349
+ // already lost) doesn't strand the handle in its visual drag state.
1350
+ delete state.handle.dataset['resizing'];
1489
1351
  const dropHandled = state.dropTarget
1490
1352
  ? this.handleFloatingStackDrop(state.index, state.dropTarget.path, state.dropTarget.zone)
1491
1353
  : false;
@@ -1581,6 +1443,12 @@ class MintDockManagerElement extends LitElement {
1581
1443
  catch (err) {
1582
1444
  /* no-op */
1583
1445
  }
1446
+ // Clear `data-resizing` outside the try — releasePointerCapture can
1447
+ // throw if the capture was already lost (e.g., the pointer left the
1448
+ // window), and we still need to drop the resizing attribute or the
1449
+ // CSS rule `.dock-floating__resizer[data-resizing='true']` keeps the
1450
+ // border dark-blue forever.
1451
+ delete state.handle.dataset['resizing'];
1584
1452
  this.floatingResizeState = null;
1585
1453
  this.dispatchLayoutChanged();
1586
1454
  }
@@ -1629,7 +1497,6 @@ class MintDockManagerElement extends LitElement {
1629
1497
  }
1630
1498
  stopPointerTrackingIfIdle() {
1631
1499
  if (this.pointerTrackingActive &&
1632
- !this.resizeState &&
1633
1500
  !this.floatingDragState &&
1634
1501
  !this.floatingResizeState &&
1635
1502
  !this.cornerResizeState) {
@@ -1671,63 +1538,102 @@ class MintDockManagerElement extends LitElement {
1671
1538
  titleEl.textContent = this.getFloatingWindowTitle(floating);
1672
1539
  }
1673
1540
  renderSplit(node, path, floatingIndex) {
1674
- const container = this.documentRef.createElement('div');
1675
- container.classList.add('dock-split');
1676
- container.dataset['direction'] = node.direction;
1677
- container.dataset['path'] = path.join('/');
1678
- const sizes = Array.isArray(node.sizes) ? node.sizes : [];
1541
+ // Each DockSplitNode renders as <mp-splitter>. The dock keeps its `.dock-split`
1542
+ // class on the host so existing `closest('.dock-split')` queries continue to
1543
+ // resolve, and stamps `data-direction` / `data-path` for the tree-driven
1544
+ // intersection-handle math.
1545
+ const splitter = this.documentRef.createElement('mp-splitter');
1546
+ splitter.classList.add('dock-split');
1547
+ splitter.dataset['direction'] = node.direction;
1548
+ splitter.dataset['path'] = path.join('/');
1549
+ // mp-splitter uses 'horizontal' (left-right) and 'vertical' (top-bottom).
1550
+ // The dock's DockSplitNode.direction matches that vocabulary 1:1.
1551
+ splitter.setAttribute('orientation', node.direction);
1552
+ const splitPath = typeof floatingIndex === 'number'
1553
+ ? { type: 'floating', index: floatingIndex, segments: [...path] }
1554
+ : { type: 'docked', segments: [...path] };
1679
1555
  node.children.forEach((child, index) => {
1680
- const childWrapper = this.documentRef.createElement('div');
1681
- childWrapper.classList.add('dock-split__child');
1682
- childWrapper.dataset['index'] = String(index);
1683
- const size = sizes[index];
1684
- if (typeof size === 'number' && Number.isFinite(size)) {
1685
- childWrapper.style.flex = `${Math.max(size, 0)} 1 0`;
1686
- }
1687
- else {
1688
- childWrapper.style.flex = '1 1 0';
1689
- }
1690
- childWrapper.appendChild(this.renderNode(child, [...path, index], floatingIndex));
1691
- container.appendChild(childWrapper);
1692
- if (index < node.children.length - 1) {
1693
- const divider = this.documentRef.createElement('div');
1694
- divider.classList.add('dock-split__divider');
1695
- divider.setAttribute('role', 'separator');
1696
- divider.tabIndex = 0;
1697
- // Tag divider with metadata for intersection detection
1698
- const dividerPath = typeof floatingIndex === 'number'
1699
- ? { type: 'floating', index: floatingIndex, segments: [...path] }
1700
- : { type: 'docked', segments: [...path] };
1701
- divider.dataset['path'] = this.formatPath(dividerPath);
1702
- divider.dataset['index'] = String(index);
1703
- divider.dataset['orientation'] = node.direction;
1704
- divider.addEventListener('pointerdown', (event) => this.beginResize(event, container, floatingIndex !== undefined
1705
- ? { type: 'floating', index: floatingIndex, segments: [...path] }
1706
- : { type: 'docked', segments: [...path] }, index));
1707
- container.appendChild(divider);
1556
+ // mp-splitter accepts direct children — it wraps each in a panel-wrapper
1557
+ // inside its shadow DOM and projects via a named slot per index.
1558
+ splitter.appendChild(this.renderNode(child, [...path, index], floatingIndex));
1559
+ });
1560
+ // Apply persisted sizes from the layout tree once mp-splitter has built
1561
+ // its panel wrappers. mp-splitter's setPanelSizes interprets values as
1562
+ // pixel widths/heights; the dock's saved sizes are flex weights, so
1563
+ // convert using the splitter's measured cross-axis container size.
1564
+ const sizes = Array.isArray(node.sizes) ? node.sizes : [];
1565
+ if (sizes.length > 0) {
1566
+ requestAnimationFrame(() => {
1567
+ const totalWeight = sizes.reduce((s, w) => s + Math.max(w, 0), 0);
1568
+ if (totalWeight <= 0)
1569
+ return;
1570
+ const containerSize = node.direction === 'horizontal'
1571
+ ? splitter.getBoundingClientRect().width
1572
+ : splitter.getBoundingClientRect().height;
1573
+ if (!Number.isFinite(containerSize) || containerSize <= 0)
1574
+ return;
1575
+ const px = sizes.map((w) => (Math.max(w, 0) / totalWeight) * containerSize);
1576
+ splitter
1577
+ .setPanelSizes?.(px);
1578
+ });
1579
+ }
1580
+ // mp-splitter fires resize-end with pixel sizes after a divider drag.
1581
+ // Convert back to flex weights (sum to a stable total — keep current sum
1582
+ // so future renders interpret consistently) and persist to the layout tree.
1583
+ splitter.addEventListener('resize-end', (event) => {
1584
+ // resize-end bubbles, so a nested mp-splitter's drag end would also
1585
+ // reach this listener. Only react to events from THIS splitter, not
1586
+ // from a descendant — otherwise we'd apply the inner's sizes to the
1587
+ // outer's splitNode and mangle the outer's weights.
1588
+ if (event.target !== splitter)
1589
+ return;
1590
+ const detail = event.detail;
1591
+ if (!Array.isArray(detail?.sizes) || detail.sizes.length === 0)
1592
+ return;
1593
+ const splitNode = this.resolveSplitNode(splitPath);
1594
+ if (!splitNode)
1595
+ return;
1596
+ const previousTotal = (splitNode.sizes ?? []).reduce((s, w) => s + Math.max(w, 0), 0);
1597
+ const total = detail.sizes.reduce((s, v) => s + Math.max(v, 0), 0);
1598
+ const targetTotal = previousTotal > 0 ? previousTotal : detail.sizes.length;
1599
+ if (total > 0) {
1600
+ splitNode.sizes = detail.sizes.map((px) => (Math.max(px, 0) / total) * targetTotal);
1601
+ this.dispatchLayoutChanged();
1708
1602
  }
1709
1603
  });
1710
- return container;
1604
+ return splitter;
1711
1605
  }
1712
1606
  renderStack(node, path, floatingIndex) {
1713
- const stack = this.documentRef.createElement('div');
1607
+ // Dock stacks are rendered as <mp-tab-control>. The dock keeps `.dock-stack`
1608
+ // as a class on the host so existing `closest('.dock-stack')` queries
1609
+ // continue to resolve. The tab strip + body slot projection are owned by
1610
+ // mp-tab-control; the dock just provides the slotted header/content
1611
+ // elements and listens for tab-activate to drive layout-tree updates.
1612
+ const stack = this.documentRef.createElement('mp-tab-control');
1714
1613
  stack.classList.add('dock-stack');
1614
+ // Dock controls activation; tell mp-tab-control not to auto-pick.
1615
+ stack.setAttribute('select-first-tab', 'false');
1616
+ // `border="top"` gives us the strip-cutout line under the tabs (so the
1617
+ // active tab visually punches through into the body) without adding the
1618
+ // full Bootstrap frame, which would double up with the dock's own outer
1619
+ // chrome border on `.dock-stack` (and on `.dock-floating` for floating
1620
+ // panels).
1621
+ stack.setAttribute('border', 'top');
1715
1622
  const location = typeof floatingIndex === 'number'
1716
1623
  ? { type: 'floating', index: floatingIndex, segments: [...path] }
1717
1624
  : { type: 'docked', segments: [...path] };
1718
1625
  stack.dataset['path'] = this.formatPath(location);
1719
- const header = this.documentRef.createElement('div');
1720
- header.classList.add('dock-stack__header');
1721
- header.setAttribute('role', 'tablist');
1722
- const content = this.documentRef.createElement('div');
1723
- content.classList.add('dock-stack__content');
1724
1626
  const panes = Array.from(new Set(node.panes));
1725
1627
  if (panes.length === 0) {
1628
+ const emptyHeader = this.documentRef.createElement('span');
1629
+ emptyHeader.setAttribute('slot', '__empty__-header');
1630
+ emptyHeader.textContent = '(empty)';
1726
1631
  const empty = this.documentRef.createElement('div');
1632
+ empty.setAttribute('slot', '__empty__-content');
1727
1633
  empty.classList.add('dock-stack__pane');
1728
1634
  empty.textContent = 'No panes configured';
1729
- content.appendChild(empty);
1730
- stack.append(header, content);
1635
+ stack.append(emptyHeader, empty);
1636
+ stack.setAttribute('active-tab', '__empty__');
1731
1637
  return stack;
1732
1638
  }
1733
1639
  const activePane = panes.includes(node.activePane ?? '')
@@ -1740,228 +1646,114 @@ class MintDockManagerElement extends LitElement {
1740
1646
  const paneSlug = paneSlugRaw.length > 0 ? paneSlugRaw : 'pane';
1741
1647
  const tabId = `${this.instanceId}-tab-${pathSlug}-${paneSlug}`;
1742
1648
  const panelId = `${this.instanceId}-panel-${pathSlug}-${paneSlug}`;
1743
- const button = this.documentRef.createElement('button');
1744
- button.type = 'button';
1745
- button.classList.add('dock-tab');
1746
- button.dataset['pane'] = paneName;
1747
- button.id = tabId;
1748
- button.textContent = this.titles[paneName] ?? paneName;
1749
- button.setAttribute('role', 'tab');
1750
- button.setAttribute('aria-controls', panelId);
1751
- if (paneName === activePane) {
1752
- button.classList.add('dock-tab--active');
1753
- }
1754
- button.setAttribute('aria-selected', String(paneName === activePane));
1755
- button.draggable = true;
1756
- button.addEventListener('pointerdown', (event) => {
1757
- const stackEl = button.closest('.dock-stack');
1758
- this.captureTabDragMetrics(event, stackEl ?? null);
1649
+ // Header span — projected via mp-tab-control's `${tabId}-header` slot
1650
+ // into the strip's button content. Carries the dock's drag handlers.
1651
+ const headerSpan = this.documentRef.createElement('span');
1652
+ headerSpan.setAttribute('slot', `${tabId}-header`);
1653
+ headerSpan.classList.add('dock-tab');
1654
+ headerSpan.dataset['pane'] = paneName;
1655
+ headerSpan.dataset['tabId'] = tabId;
1656
+ headerSpan.textContent = this.titles[paneName] ?? paneName;
1657
+ // Pointer-only drag (no HTML5 dnd). pointerdown captures metrics + arms
1658
+ // a threshold gesture; once the pointer moves >threshold pixels we
1659
+ // promote it to a real pane drag via beginPaneDrag. Using pointer
1660
+ // events sidesteps the entire class of HTML5 dnd quirks (cancellation
1661
+ // when source DOM is removed mid-drag, suppressed mousemove, bogus 0/0
1662
+ // coordinates in Firefox, browser-specific drag-image behavior).
1663
+ headerSpan.addEventListener('pointerdown', (event) => {
1664
+ this.captureTabDragMetrics(event, stack);
1665
+ this.armPaneDragGesture(event, this.clonePath(location), paneName, stack);
1759
1666
  event.stopPropagation();
1760
1667
  });
1761
- button.addEventListener('pointerup', () => this.clearPendingTabDragMetrics());
1762
- button.addEventListener('pointercancel', () => this.clearPendingTabDragMetrics());
1763
- button.addEventListener('dragstart', (event) => {
1764
- const stackEl = button.closest('.dock-stack');
1765
- this.beginPaneDrag(event, this.clonePath(location), paneName, stackEl ?? null);
1766
- });
1767
- button.addEventListener('dragend', () => {
1768
- this.endPaneDrag();
1769
- this.clearPendingTabDragMetrics();
1770
- });
1771
- button.addEventListener('click', () => {
1772
- this.activatePane(stack, paneName, this.clonePath(location));
1773
- this.dispatchEvent(new CustomEvent('dock-pane-activated', {
1774
- detail: { pane: paneName },
1775
- bubbles: true,
1776
- composed: true,
1777
- }));
1778
- });
1779
- header.appendChild(button);
1668
+ // Content wrapper — projected via mp-tab-control's `${tabId}-content`
1669
+ // slot only when this tab is active. Holds the dock manager's per-pane
1670
+ // <slot> for the consumer's content.
1780
1671
  const paneHost = this.documentRef.createElement('div');
1672
+ paneHost.setAttribute('slot', `${tabId}-content`);
1781
1673
  paneHost.classList.add('dock-stack__pane');
1782
1674
  paneHost.dataset['pane'] = paneName;
1675
+ paneHost.dataset['tabId'] = tabId;
1783
1676
  paneHost.id = panelId;
1784
- paneHost.setAttribute('role', 'tabpanel');
1785
- paneHost.setAttribute('aria-labelledby', tabId);
1786
- if (paneName !== activePane) {
1787
- paneHost.setAttribute('hidden', '');
1788
- }
1789
1677
  const slotEl = this.documentRef.createElement('slot');
1790
1678
  slotEl.name = paneName;
1791
1679
  paneHost.appendChild(slotEl);
1792
- content.appendChild(paneHost);
1680
+ stack.append(headerSpan, paneHost);
1681
+ if (paneName === activePane) {
1682
+ stack.setAttribute('active-tab', tabId);
1683
+ }
1793
1684
  });
1794
1685
  stack.dataset['activePane'] = activePane;
1795
- stack.append(header, content);
1686
+ // Drive activatePane from mp-tab-control's tab-activate event. We map the
1687
+ // tabId back to the original paneName via the header span's data-pane.
1688
+ stack.addEventListener('tab-activate', (event) => {
1689
+ const detail = event.detail;
1690
+ const headerSpan = stack.querySelector(`:scope > [data-tab-id="${detail.tabId}"]`);
1691
+ const paneName = headerSpan?.dataset['pane'];
1692
+ if (paneName) {
1693
+ this.activatePane(stack, paneName, this.clonePath(location));
1694
+ this.dispatchEvent(new CustomEvent('dock-pane-activated', {
1695
+ detail: { pane: paneName },
1696
+ bubbles: true,
1697
+ composed: true,
1698
+ }));
1699
+ }
1700
+ });
1796
1701
  return stack;
1797
1702
  }
1798
- beginResize(event, container, path, index) {
1799
- event.preventDefault();
1800
- const divider = event.currentTarget;
1801
- if (!divider) {
1802
- return;
1803
- }
1804
- const orientation = container.dataset['direction'] ?? 'horizontal';
1805
- const children = Array.from(container.querySelectorAll(':scope > .dock-split__child'));
1806
- const initialSizes = children.map((child) => {
1807
- const rect = child.getBoundingClientRect();
1808
- return orientation === 'horizontal' ? rect.width : rect.height;
1809
- });
1810
- const beforeSize = initialSizes[index];
1811
- const afterSize = initialSizes[index + 1];
1812
- const startPos = orientation === 'horizontal' ? event.clientX : event.clientY;
1813
- divider.setPointerCapture(event.pointerId);
1814
- divider.dataset['resizing'] = 'true';
1815
- this.resizeState = {
1816
- path: this.clonePath(path),
1817
- index,
1818
- pointerId: event.pointerId,
1819
- orientation,
1820
- container,
1821
- divider,
1822
- startPos,
1823
- initialSizes,
1824
- beforeSize,
1825
- afterSize,
1826
- };
1827
- this.startPointerTracking();
1828
- // Compute localized snap targets: intersections with perpendicular dividers near this divider
1829
- try {
1830
- const rootRect = this.rootEl.getBoundingClientRect();
1831
- const dividerRect = divider.getBoundingClientRect();
1832
- const allDividers = Array.from(this.shadowRoot?.querySelectorAll('.dock-split__divider') ?? []);
1833
- const targets = [];
1834
- if (orientation === 'horizontal') {
1835
- // Current bar is vertical → snap X to centers of other vertical bars (no crossing check needed)
1836
- allDividers.forEach((el) => {
1837
- if (el === divider)
1838
- return;
1839
- const o = el.dataset['orientation'] ?? undefined;
1840
- if (o !== 'horizontal')
1841
- return; // vertical divider bars (split direction horizontal)
1842
- const r = el.getBoundingClientRect();
1843
- const xCenter = r.left + r.width / 2 - rootRect.left;
1844
- targets.push(xCenter);
1845
- });
1846
- this.activeSnapAxis = 'x';
1847
- this.activeSnapTargets = targets;
1848
- this.renderSnapMarkersForDivider();
1849
- }
1850
- else {
1851
- // Current bar is horizontal → snap Y to centers of other horizontal bars (no crossing check needed)
1852
- allDividers.forEach((el) => {
1853
- if (el === divider)
1854
- return;
1855
- const o = el.dataset['orientation'] ?? undefined;
1856
- if (o !== 'vertical')
1857
- return; // horizontal divider bars (split direction vertical)
1858
- const r = el.getBoundingClientRect();
1859
- const yCenter = r.top + r.height / 2 - rootRect.top;
1860
- targets.push(yCenter);
1861
- });
1862
- this.activeSnapAxis = 'y';
1863
- this.activeSnapTargets = targets;
1864
- this.renderSnapMarkersForDivider();
1865
- }
1866
- }
1867
- catch {
1868
- this.activeSnapAxis = null;
1869
- this.activeSnapTargets = [];
1870
- this.clearSnapMarkers();
1871
- }
1703
+ /**
1704
+ * Returns the strip (`.tsc`) element inside an `<mp-tab-control>`'s shadow
1705
+ * DOM. Used by drag/drop logic that needs the strip's geometry instead of
1706
+ * the host element's bounds.
1707
+ */
1708
+ getStackStripEl(stack) {
1709
+ if (stack.tagName !== 'MP-TAB-CONTROL')
1710
+ return null;
1711
+ return stack.shadowRoot?.querySelector('.tsc') ?? null;
1712
+ }
1713
+ /**
1714
+ * Returns the rendered tab buttons inside an `<mp-tab-control>`'s shadow
1715
+ * strip the light-DOM `.dock-tab` spans the dock owns are projected into
1716
+ * these buttons via `<slot>`. Use these for geometry / position queries
1717
+ * (insert-index computation, drop-indicator placement). Use the light-DOM
1718
+ * `.dock-tab` spans for data queries (paneName, drag listeners).
1719
+ */
1720
+ getStackTabButtons(stack) {
1721
+ if (stack.tagName !== 'MP-TAB-CONTROL')
1722
+ return [];
1723
+ return Array.from(stack.shadowRoot?.querySelectorAll('button.nav-link') ?? []);
1724
+ }
1725
+ /**
1726
+ * Returns the dividers inside an `<mp-splitter>`'s shadow DOM, in DOM order.
1727
+ * mp-splitter renders one `.divider` between each pair of adjacent panels,
1728
+ * so for an N-child split, length N-1.
1729
+ */
1730
+ getSplitterDividers(splitter) {
1731
+ if (splitter.tagName !== 'MP-SPLITTER')
1732
+ return [];
1733
+ return Array.from(splitter.shadowRoot?.querySelectorAll('.divider') ?? []);
1734
+ }
1735
+ /**
1736
+ * Returns the panel wrappers inside an `<mp-splitter>`'s shadow DOM, in
1737
+ * DOM order. These are the elements mp-splitter sizes (via setPanelSizes)
1738
+ * during a divider drag — the dock reads their geometry for intersection
1739
+ * handle math and snap markers.
1740
+ */
1741
+ getSplitterPanels(splitter) {
1742
+ if (splitter.tagName !== 'MP-SPLITTER')
1743
+ return [];
1744
+ return Array.from(splitter.shadowRoot?.querySelectorAll('.panel-wrapper') ?? []);
1745
+ }
1746
+ /**
1747
+ * Locate the rendered `<mp-splitter>` element for a given DockPath
1748
+ * `segments` value (the split-tree path). Searches the dock's shadow.
1749
+ */
1750
+ findSplitterByPath(segments) {
1751
+ return (this.shadowRoot?.querySelector(`.dock-split[data-path="${segments.join('/')}"]`) ?? null);
1872
1752
  }
1873
1753
  onPointerMove(event) {
1874
1754
  if (this.cornerResizeState && event.pointerId === this.cornerResizeState.pointerId) {
1875
1755
  this.handleCornerResizeMove(event);
1876
1756
  }
1877
- if (this.resizeState && event.pointerId === this.resizeState.pointerId) {
1878
- const state = this.resizeState;
1879
- const splitNode = this.resolveSplitNode(state.path);
1880
- if (!splitNode) {
1881
- return;
1882
- }
1883
- let currentPos = state.orientation === 'horizontal' ? event.clientX : event.clientY;
1884
- // Localized axis snap near neighboring intersections
1885
- const tol = 10;
1886
- const rootRect = this.rootEl.getBoundingClientRect();
1887
- if (this.activeSnapTargets.length) {
1888
- if (state.orientation === 'horizontal' && this.activeSnapAxis === 'x') {
1889
- // Vertical divider snapping along X
1890
- let closest = Number.POSITIVE_INFINITY;
1891
- let best = currentPos;
1892
- const pointerX = event.clientX;
1893
- this.activeSnapTargets.forEach((sx) => {
1894
- const px = rootRect.left + sx;
1895
- const d = Math.abs(pointerX - px);
1896
- if (d < closest) {
1897
- closest = d;
1898
- best = px;
1899
- }
1900
- });
1901
- if (closest <= tol)
1902
- currentPos = best;
1903
- this.renderSnapMarkersForDivider();
1904
- }
1905
- else if (state.orientation === 'vertical' && this.activeSnapAxis === 'y') {
1906
- // Horizontal divider snapping along Y
1907
- let closest = Number.POSITIVE_INFINITY;
1908
- let best = currentPos;
1909
- const pointerY = event.clientY;
1910
- this.activeSnapTargets.forEach((sy) => {
1911
- const py = rootRect.top + sy;
1912
- const d = Math.abs(pointerY - py);
1913
- if (d < closest) {
1914
- closest = d;
1915
- best = py;
1916
- }
1917
- });
1918
- if (closest <= tol)
1919
- currentPos = best;
1920
- this.renderSnapMarkersForDivider();
1921
- }
1922
- }
1923
- const delta = currentPos - state.startPos;
1924
- const minSize = 48;
1925
- const pairTotal = state.beforeSize + state.afterSize;
1926
- let newBefore = state.beforeSize + delta;
1927
- // Optional snap with Shift
1928
- if (event.shiftKey && pairTotal > 0) {
1929
- const ratios = [1 / 3, 1 / 2, 2 / 3];
1930
- const target = newBefore / pairTotal;
1931
- let best = ratios[0];
1932
- let bestDist = Math.abs(target - best);
1933
- for (let i = 1; i < ratios.length; i++) {
1934
- const d = Math.abs(target - ratios[i]);
1935
- if (d < bestDist) {
1936
- best = ratios[i];
1937
- bestDist = d;
1938
- }
1939
- }
1940
- newBefore = best * pairTotal;
1941
- }
1942
- newBefore = Math.min(Math.max(newBefore, minSize), pairTotal - minSize);
1943
- let newAfter = pairTotal - newBefore;
1944
- if (!Number.isFinite(newBefore) || !Number.isFinite(newAfter)) {
1945
- return;
1946
- }
1947
- if (newAfter < minSize) {
1948
- newAfter = minSize;
1949
- newBefore = pairTotal - minSize;
1950
- }
1951
- const newSizesPixels = [...state.initialSizes];
1952
- newSizesPixels[state.index] = newBefore;
1953
- newSizesPixels[state.index + 1] = newAfter;
1954
- const total = newSizesPixels.reduce((acc, size) => acc + size, 0);
1955
- const normalized = total > 0 ? newSizesPixels.map((size) => size / total) : [];
1956
- splitNode.sizes = normalized;
1957
- const children = Array.from(state.container.querySelectorAll(':scope > .dock-split__child'));
1958
- normalized.forEach((size, idx) => {
1959
- if (children[idx]) {
1960
- children[idx].style.flex = `${Math.max(size, 0)} 1 0`;
1961
- }
1962
- });
1963
- this.dispatchLayoutChanged();
1964
- }
1965
1757
  if (this.floatingResizeState && event.pointerId === this.floatingResizeState.pointerId) {
1966
1758
  this.handleFloatingResizeMove(event);
1967
1759
  }
@@ -1973,15 +1765,6 @@ class MintDockManagerElement extends LitElement {
1973
1765
  if (this.cornerResizeState && event.pointerId === this.cornerResizeState.pointerId) {
1974
1766
  this.endCornerResize(event.pointerId);
1975
1767
  }
1976
- if (this.resizeState && event.pointerId === this.resizeState.pointerId) {
1977
- const divider = this.resizeState.divider;
1978
- divider.dataset['resizing'] = 'false';
1979
- divider.releasePointerCapture(this.resizeState.pointerId);
1980
- this.resizeState = null;
1981
- this.scheduleRenderIntersectionHandles();
1982
- this.activeSnapAxis = null;
1983
- this.activeSnapTargets = [];
1984
- }
1985
1768
  if (this.floatingDragState && event.pointerId === this.floatingDragState.pointerId) {
1986
1769
  this.endFloatingDrag(event.pointerId);
1987
1770
  }
@@ -2021,30 +1804,55 @@ class MintDockManagerElement extends LitElement {
2021
1804
  clearPendingTabDragMetrics() {
2022
1805
  this.pendingTabDragMetrics = null;
2023
1806
  }
1807
+ /**
1808
+ * Pointerdown handler arms a "may become a drag" gesture. Once the pointer
1809
+ * moves past `threshold` pixels we promote it to an actual pane drag via
1810
+ * {@link beginPaneDrag}; if the user releases first we just clear the
1811
+ * pending tab metrics. All listeners self-clean on resolve so the gesture
1812
+ * stays scoped to a single pointerdown.
1813
+ */
1814
+ armPaneDragGesture(startEvent, path, pane, stackEl) {
1815
+ if (startEvent.pointerType === 'mouse' && startEvent.button !== 0)
1816
+ return;
1817
+ const win = this.windowRef;
1818
+ if (!win)
1819
+ return;
1820
+ const startX = startEvent.clientX;
1821
+ const startY = startEvent.clientY;
1822
+ const pointerId = startEvent.pointerId;
1823
+ const threshold = 5;
1824
+ let resolved = false;
1825
+ const cleanup = () => {
1826
+ resolved = true;
1827
+ win.removeEventListener('pointermove', onMove, true);
1828
+ win.removeEventListener('pointerup', onRelease, true);
1829
+ win.removeEventListener('pointercancel', onRelease, true);
1830
+ };
1831
+ const onMove = (event) => {
1832
+ if (resolved || event.pointerId !== pointerId)
1833
+ return;
1834
+ const dx = event.clientX - startX;
1835
+ const dy = event.clientY - startY;
1836
+ if (Math.hypot(dx, dy) < threshold)
1837
+ return;
1838
+ cleanup();
1839
+ this.beginPaneDrag(event, path, pane, stackEl);
1840
+ };
1841
+ const onRelease = (event) => {
1842
+ if (resolved || event.pointerId !== pointerId)
1843
+ return;
1844
+ cleanup();
1845
+ this.clearPendingTabDragMetrics();
1846
+ };
1847
+ win.addEventListener('pointermove', onMove, true);
1848
+ win.addEventListener('pointerup', onRelease, true);
1849
+ win.addEventListener('pointercancel', onRelease, true);
1850
+ }
2024
1851
  beginPaneDrag(event, path, pane, stackEl) {
2025
- if (!event.dataTransfer) {
2026
- return;
2027
- }
2028
- // Create a ghost element for the drag image. This prevents the browser from cancelling
2029
- // the drag operation when the original element is removed from the DOM during re-render.
2030
- const ghost = event.currentTarget.cloneNode(true);
2031
- ghost.style.position = 'absolute';
2032
- ghost.style.left = '-9999px';
2033
- ghost.style.top = '-9999px';
2034
- ghost.style.width = `${event.currentTarget.offsetWidth}px`;
2035
- ghost.style.height = `${event.currentTarget.offsetHeight}px`;
2036
- this.shadowRoot?.appendChild(ghost);
2037
- // Use the ghost element as the drag image.
2038
- // The offset is set to where the user's cursor is on the original element.
2039
- const dragImgOffsetX = Number.isFinite(event.offsetX) ? event.offsetX : 0;
2040
- const dragImgOffsetY = Number.isFinite(event.offsetY) ? event.offsetY : 0;
2041
- event.dataTransfer.setDragImage(ghost, dragImgOffsetX, dragImgOffsetY);
2042
- // The ghost element is no longer needed after the drag image is set.
2043
- // We defer its removal to ensure the browser has captured it.
2044
- setTimeout(() => ghost.remove(), 0);
2045
1852
  const { path: sourcePath, floatingIndex, pointerOffsetX, pointerOffsetY, } = this.preparePaneDragSource(path, pane, stackEl, event);
2046
- // Capture header bounds for detecting when to convert to floating
2047
- const headerEl = stackEl?.querySelector('.dock-stack__header') ?? null;
1853
+ // Capture header bounds for detecting when to convert to floating.
1854
+ // The strip lives inside the mp-tab-control's shadow as `.tsc`.
1855
+ const headerEl = stackEl ? this.getStackStripEl(stackEl) : null;
2048
1856
  const headerRect = headerEl ? headerEl.getBoundingClientRect() : null;
2049
1857
  const headerBounds = headerRect
2050
1858
  ? { left: headerRect.left, top: headerRect.top, right: headerRect.right, bottom: headerRect.bottom }
@@ -2061,36 +1869,26 @@ class MintDockManagerElement extends LitElement {
2061
1869
  sourceHeaderBounds: headerBounds,
2062
1870
  startClientX: metrics && Number.isFinite(metrics.startClientX)
2063
1871
  ? metrics.startClientX
2064
- : Number.isFinite(event.clientX)
2065
- ? event.clientX
2066
- : undefined,
1872
+ : event.clientX,
2067
1873
  startClientY: metrics && Number.isFinite(metrics.startClientY)
2068
1874
  ? metrics.startClientY
2069
- : Number.isFinite(event.clientY)
2070
- ? event.clientY
2071
- : undefined,
1875
+ : event.clientY,
2072
1876
  };
2073
- // Seed last known pointer position from pointerdown metrics to avoid (0,0) glitches in Firefox
2074
- if (this.dragState.startClientX !== undefined &&
2075
- this.dragState.startClientY !== undefined &&
2076
- Number.isFinite(this.dragState.startClientX) &&
2077
- Number.isFinite(this.dragState.startClientY)) {
2078
- this.lastDragPointerPosition = {
2079
- x: this.dragState.startClientX,
2080
- y: this.dragState.startClientY,
2081
- };
2082
- }
2083
- // Prefer the pointer offset relative to the dragged tab to avoid jumps on conversion
2084
- if (Number.isFinite(event.offsetX)) {
2085
- this.dragState.pointerOffsetX = event.offsetX;
2086
- }
2087
- if (Number.isFinite(event.offsetY)) {
2088
- this.dragState.pointerOffsetY = event.offsetY;
2089
- }
2090
- this.updateDraggedFloatingPosition(event);
1877
+ this.lastDragPointerPosition = {
1878
+ x: this.dragState.startClientX,
1879
+ y: this.dragState.startClientY,
1880
+ };
1881
+ // pointerOffsetX/Y from preparePaneDragSource is the offset within the
1882
+ // source stack rect captured at pointerdown by captureTabDragMetrics.
1883
+ // Don't overwrite with event.offsetX/Y here — the threshold-trigger
1884
+ // pointermove fired on window, so its offset is in window-local coords
1885
+ // (≈ clientX/Y) which would crash the conversion math to ~(0,0).
1886
+ this.updateDraggedFloatingPositionFromPoint(event.clientX, event.clientY);
2091
1887
  this.startDragPointerTracking();
2092
- event.dataTransfer.effectAllowed = 'move';
2093
- event.dataTransfer.setData('text/plain', pane);
1888
+ // Mark the source floating wrapper (if any) so its CSS rule kicks in and
1889
+ // pointer-events:none lets findStackAtPoint see through to the docked
1890
+ // stack underneath, enabling drop zones over the dock during the drag.
1891
+ this.markDraggedFloatingWrapper();
2094
1892
  // Preferred UX: if the dragged tab is the only one in its stack,
2095
1893
  // immediately convert to a floating window unless it is already the
2096
1894
  // only pane in a floating window (this case is handled by reuse logic).
@@ -2098,18 +1896,16 @@ class MintDockManagerElement extends LitElement {
2098
1896
  const loc = this.resolveStackLocation(this.dragState.sourcePath);
2099
1897
  if (loc && Array.isArray(loc.node.panes) && loc.node.panes.length === 1) {
2100
1898
  let shouldConvert = false;
2101
- if (loc.context === "docked") {
1899
+ if (loc.context === 'docked') {
2102
1900
  shouldConvert = true;
2103
1901
  }
2104
- else if (loc.context === "floating") {
1902
+ else if (loc.context === 'floating') {
2105
1903
  const floating = this.floatingLayouts[loc.index];
2106
1904
  const totalPanes = floating && floating.root ? this.countPanesInTree(floating.root) : 0;
2107
- shouldConvert = totalPanes > 1; // not the only pane in this floating window
1905
+ shouldConvert = totalPanes > 1;
2108
1906
  }
2109
1907
  if (shouldConvert) {
2110
- const startX = Number.isFinite(event.clientX) ? event.clientX : (this.dragState.startClientX ?? 0);
2111
- const startY = Number.isFinite(event.clientY) ? event.clientY : (this.dragState.startClientY ?? 0);
2112
- this.convertPendingTabDragToFloating(startX, startY);
1908
+ this.convertPendingTabDragToFloating(event.clientX, event.clientY);
2113
1909
  }
2114
1910
  }
2115
1911
  }
@@ -2191,134 +1987,27 @@ class MintDockManagerElement extends LitElement {
2191
1987
  }
2192
1988
  endPaneDrag() {
2193
1989
  this.clearPendingDragEndTimeout();
1990
+ // Restore the dragged tab's `data-hidden` and remove the placeholder span
1991
+ // BEFORE we null out dragState — clearHeaderDragPlaceholder reads
1992
+ // `dragState.placeholderEl`, `dragState.placeholderHeader`, and
1993
+ // `dragState.pane` to know what to restore. If dragState is nulled first,
1994
+ // this becomes a silent no-op and the dragged pane stays hidden in its
1995
+ // source stack while the placeholder span lingers in the strip — which
1996
+ // is exactly the "Panel disappears, only a small tab-thumb remains"
1997
+ // regression the multi-pane drag-out path can otherwise trigger when
1998
+ // no renderLayout() runs between conversion and end (e.g. user releases
1999
+ // outside any drop zone, or HTML5 dragend fires without a drop).
2000
+ this.clearHeaderDragPlaceholder();
2001
+ this.clearDraggedFloatingWrapperMarkers();
2194
2002
  const state = this.dragState;
2195
2003
  this.dragState = null;
2196
2004
  this.hideDropIndicator();
2197
- this.clearHeaderDragPlaceholder();
2198
2005
  this.stopDragPointerTracking();
2199
2006
  this.lastDragPointerPosition = null;
2200
2007
  if (state && state.floatingIndex !== null && !state.dropHandled) {
2201
2008
  this.dispatchLayoutChanged();
2202
2009
  }
2203
2010
  }
2204
- onDragOver(event) {
2205
- if (!this.dragState) {
2206
- return;
2207
- }
2208
- event.preventDefault();
2209
- // Keep internal pointer tracking up-to-date.
2210
- this.updateDraggedFloatingPosition(event);
2211
- if (event.dataTransfer) {
2212
- event.dataTransfer.dropEffect = 'move';
2213
- }
2214
- // Some browsers intermittently report (0,0) for dragover coordinates.
2215
- // Mirror the robust logic used in onDrop: prefer actual event coordinates
2216
- // when valid, otherwise fall back to the last tracked pointer position.
2217
- const pointFromEvent = Number.isFinite(event.clientX) && Number.isFinite(event.clientY)
2218
- ? { clientX: event.clientX, clientY: event.clientY }
2219
- : null;
2220
- const point = pointFromEvent ??
2221
- (this.lastDragPointerPosition
2222
- ? { clientX: this.lastDragPointerPosition.x, clientY: this.lastDragPointerPosition.y }
2223
- : null);
2224
- const stack = this.findStackElement(event) ??
2225
- (point ? this.findStackAtPoint(point.clientX, point.clientY) : null);
2226
- if (!stack) {
2227
- if (this.dropJoystick.dataset['visible'] !== 'true') {
2228
- this.hideDropIndicator();
2229
- }
2230
- return;
2231
- }
2232
- const path = this.parsePath(stack.dataset['path']);
2233
- // While reordering within the same header, suppress the joystick/indicator entirely
2234
- if (this.dragState &&
2235
- this.dragState.floatingIndex !== null &&
2236
- this.dragState.floatingIndex < 0 &&
2237
- path &&
2238
- this.pathsEqual(path, this.dragState.sourcePath)) {
2239
- const px = (point ? point.clientX : event.clientX);
2240
- const py = (point ? point.clientY : event.clientY);
2241
- if (Number.isFinite(px) && Number.isFinite(py) && this.isPointerOverSourceHeader(px, py)) {
2242
- // Drive live reorder using the unified path so we update instantly.
2243
- this.updatePaneDragDropTargetFromPoint(px, py);
2244
- this.hideDropIndicator();
2245
- return;
2246
- }
2247
- }
2248
- // If the hovered stack changed, clear any sticky zone from the previous
2249
- // target before computing the new zone.
2250
- if (this.dropJoystickTarget && this.dropJoystickTarget !== stack) {
2251
- delete this.dropJoystick.dataset['zone'];
2252
- this.updateDropJoystickActiveZone(null);
2253
- }
2254
- const eventZoneHint = this.extractDropZoneFromEvent(event);
2255
- const pointZoneHint = point ? this.findDropZoneByPoint(point.clientX, point.clientY) : null;
2256
- const zone = this.computeDropZone(stack, point ?? event, pointZoneHint ?? eventZoneHint);
2257
- this.showDropIndicator(stack, zone);
2258
- }
2259
- updateDraggedFloatingPosition(event) {
2260
- if (!this.dragState) {
2261
- return;
2262
- }
2263
- const { clientX, clientY } = event;
2264
- const hasValidCoordinates = Number.isFinite(clientX) &&
2265
- Number.isFinite(clientY) &&
2266
- !(clientX === 0 && clientY === 0);
2267
- if (hasValidCoordinates) {
2268
- this.lastDragPointerPosition = { x: clientX, y: clientY };
2269
- this.updateDraggedFloatingPositionFromPoint(clientX, clientY);
2270
- return;
2271
- }
2272
- if (this.lastDragPointerPosition) {
2273
- const { x, y } = this.lastDragPointerPosition;
2274
- this.updateDraggedFloatingPositionFromPoint(x, y);
2275
- }
2276
- }
2277
- onGlobalDragOver(event) {
2278
- if (!this.dragState) {
2279
- return;
2280
- }
2281
- this.updateDraggedFloatingPosition(event);
2282
- }
2283
- onDrag(event) {
2284
- if (!this.dragState) {
2285
- return;
2286
- }
2287
- this.updateDraggedFloatingPosition(event);
2288
- }
2289
- onGlobalDragEnd() {
2290
- // Attempt to finalize a drop even if the drop event doesn't reach us (Firefox/edge cases)
2291
- const state = this.dragState;
2292
- const pos = this.lastDragPointerPosition;
2293
- if (state && pos) {
2294
- const stack = this.findStackAtPoint(pos.x, pos.y);
2295
- const joystickVisible = this.dropJoystick.dataset['visible'] === 'true';
2296
- const joystickPath = this.parsePath(this.dropJoystick.dataset['path']);
2297
- const joystickTarget = this.dropJoystickTarget;
2298
- const joystickTargetPath = joystickTarget ? this.parsePath(joystickTarget.dataset['path']) : null;
2299
- const path = stack ? this.parsePath(stack.dataset['path']) : (joystickPath ?? joystickTargetPath);
2300
- const joystickZone = this.dropJoystick.dataset['zone'];
2301
- const zone = this.isDropZone(joystickZone)
2302
- ? joystickZone
2303
- : (stack ? this.computeDropZone(stack, { clientX: pos.x, clientY: pos.y }, null) : null);
2304
- if (path && this.isDropZone(zone)) {
2305
- this.handleDrop(path, zone);
2306
- this.hideDropIndicator();
2307
- if (this.dragState) {
2308
- this.dragState.dropHandled = true;
2309
- }
2310
- }
2311
- }
2312
- else {
2313
- this.hideDropIndicator();
2314
- }
2315
- if (!this.dragState) {
2316
- this.clearPendingTabDragMetrics();
2317
- return;
2318
- }
2319
- this.endPaneDrag();
2320
- this.clearPendingTabDragMetrics();
2321
- }
2322
2011
  updateDraggedFloatingPositionFromPoint(clientX, clientY) {
2323
2012
  if (!this.dragState) {
2324
2013
  return;
@@ -2326,10 +2015,6 @@ class MintDockManagerElement extends LitElement {
2326
2015
  if (!Number.isFinite(clientX) || !Number.isFinite(clientY)) {
2327
2016
  return;
2328
2017
  }
2329
- // Ignore obviously bogus coordinates sometimes seen during HTML5 drag
2330
- if (clientX === 0 && clientY === 0) {
2331
- return;
2332
- }
2333
2018
  // If still dragging a tab inside its header, only convert to floating once we leave the header.
2334
2019
  if (this.dragState.floatingIndex !== null && this.dragState.floatingIndex < 0) {
2335
2020
  const b = this.dragState.sourceHeaderBounds;
@@ -2400,16 +2085,15 @@ class MintDockManagerElement extends LitElement {
2400
2085
  const inHeaderByBounds = !!this.dragState.sourceHeaderBounds && this.isPointWithinBounds(this.dragState.sourceHeaderBounds, clientX, clientY);
2401
2086
  const inHeaderByHitTest = this.isPointerOverSourceHeader(clientX, clientY);
2402
2087
  if (inHeaderByBounds || inHeaderByHitTest) {
2403
- const header = stack.querySelector('.dock-stack__header');
2404
- if (header) {
2405
- // Ensure placeholder exists and move it as the pointer moves
2406
- this.ensureHeaderDragPlaceholder(header, this.dragState.pane);
2407
- const idx = this.computeHeaderInsertIndex(header, clientX);
2408
- if (this.dragState.liveReorderIndex !== idx) {
2409
- this.updateHeaderDragPlaceholderPosition(header, idx);
2410
- // Keep model reordering until drop; only move the placeholder now
2411
- this.dragState.liveReorderIndex = idx;
2412
- }
2088
+ // Ensure placeholder exists and move it as the pointer moves.
2089
+ // Placeholder management mutates the slotted children of the
2090
+ // mp-tab-control stack; the WC re-renders the strip on slotchange.
2091
+ this.ensureHeaderDragPlaceholder(stack, this.dragState.pane);
2092
+ const idx = this.computeHeaderInsertIndex(stack, clientX);
2093
+ if (this.dragState.liveReorderIndex !== idx) {
2094
+ this.updateHeaderDragPlaceholderPosition(stack, idx);
2095
+ // Keep model reordering until drop; only move the placeholder now
2096
+ this.dragState.liveReorderIndex = idx;
2413
2097
  }
2414
2098
  this.hideDropIndicator();
2415
2099
  return;
@@ -2421,81 +2105,125 @@ class MintDockManagerElement extends LitElement {
2421
2105
  const zone = this.computeDropZone(stack, { clientX, clientY }, zoneHint);
2422
2106
  this.showDropIndicator(stack, zone);
2423
2107
  }
2424
- // Returns true when the pointer is currently over the source stack's header (tab strip)
2108
+ // Returns true when the pointer is currently over the source stack's header (tab strip).
2109
+ // The strip lives inside the mp-tab-control's shadow as `.tsc`, so we test
2110
+ // bounds directly rather than using elementsFromPoint(/contains) which won't
2111
+ // pierce the shadow boundary cleanly.
2425
2112
  isPointerOverSourceHeader(clientX, clientY) {
2426
2113
  const state = this.dragState;
2427
2114
  if (!state) {
2428
2115
  return false;
2429
2116
  }
2430
2117
  const stackEl = state.sourceStackEl ?? null;
2431
- const header = stackEl?.querySelector('.dock-stack__header');
2432
- if (!header) {
2433
- // Be conservative: if we cannot resolve the header, treat as inside
2118
+ const strip = stackEl ? this.getStackStripEl(stackEl) : null;
2119
+ if (!strip) {
2120
+ // Be conservative: if we cannot resolve the strip, treat as inside
2434
2121
  return true;
2435
2122
  }
2436
- const sr = this.shadowRoot;
2437
- const elements = sr ? sr.elementsFromPoint(clientX, clientY) : [];
2438
- for (const el of elements) {
2439
- if (el instanceof HTMLElement && header.contains(el)) {
2440
- return true;
2441
- }
2442
- }
2443
- return false;
2123
+ const r = strip.getBoundingClientRect();
2124
+ return clientX >= r.left && clientX <= r.right && clientY >= r.top && clientY <= r.bottom;
2444
2125
  }
2445
2126
  isPointWithinBounds(bounds, x, y) {
2446
2127
  return x >= bounds.left && x <= bounds.right && y >= bounds.top && y <= bounds.bottom;
2447
2128
  }
2448
- // Ensure a placeholder tab exists during in-header drag and hide the real dragged tab visually
2449
- ensureHeaderDragPlaceholder(header, pane) {
2450
- if (this.dragState?.placeholderHeader === header && this.dragState.placeholderEl) {
2451
- return;
2452
- }
2453
- const dragged = Array.from(header.querySelectorAll('.dock-tab')).find((t) => t.dataset['pane'] === pane) ?? null;
2454
- if (!dragged) {
2455
- return;
2456
- }
2457
- // Create placeholder
2458
- const placeholder = this.documentRef.createElement('button');
2459
- placeholder.type = 'button';
2460
- placeholder.classList.add('dock-tab');
2461
- placeholder.dataset['placeholder'] = 'true';
2462
- // Keep the placeholder visually empty but reserving the same width
2463
- placeholder.textContent = '';
2464
- placeholder.setAttribute('aria-hidden', 'true');
2465
- placeholder.style.width = `${dragged.offsetWidth}px`;
2466
- // Hide the original dragged tab so it doesn't duplicate visually and free up its slot
2467
- dragged.style.display = 'none';
2468
- // Insert placeholder in the original position of the dragged tab
2469
- header.insertBefore(placeholder, dragged);
2129
+ // Ensure a placeholder tab exists during in-header drag and hide the real dragged tab visually.
2130
+ // Operates on the mp-tab-control stack: the dragged content div gets `data-hidden`
2131
+ // (mp-tab-control then skips its tab in the strip), and a placeholder header+content
2132
+ // pair is appended as light-DOM children of the stack. mp-tab-control's mutation
2133
+ // observer picks up the change and renders the placeholder as a tab.
2134
+ ensureHeaderDragPlaceholder(stack, pane) {
2135
+ if (stack.tagName !== 'MP-TAB-CONTROL')
2136
+ return;
2137
+ if (this.dragState?.placeholderHeader === stack && this.dragState.placeholderEl) {
2138
+ return;
2139
+ }
2140
+ const draggedHeader = stack.querySelector(`:scope > .dock-tab[data-pane="${CSS.escape(pane)}"]`);
2141
+ const draggedContent = stack.querySelector(`:scope > .dock-stack__pane[data-pane="${CSS.escape(pane)}"]`);
2142
+ if (!draggedHeader || !draggedContent)
2143
+ return;
2144
+ // Measure the dragged tab's text-only width BEFORE hiding it. The
2145
+ // `.dock-tab` rule applies padding (matching the strip button's padding so
2146
+ // the span fills the button as a drag handle), so `offsetWidth` is
2147
+ // text + padding we subtract the span's own padding to get just the
2148
+ // text width. That's the natural slot content width we want the
2149
+ // placeholder to reserve; the placeholder span will re-apply the same
2150
+ // padding on top, mirroring the original tab's geometry exactly.
2151
+ const draggedCS = this.windowRef
2152
+ ? this.windowRef.getComputedStyle(draggedHeader)
2153
+ : globalThis.getComputedStyle(draggedHeader);
2154
+ const draggedHorizontalPadding = parseFloat(draggedCS.paddingLeft) + parseFloat(draggedCS.paddingRight);
2155
+ const slotContentWidth = Math.max(0, draggedHeader.offsetWidth - draggedHorizontalPadding);
2156
+ // Hide the dragged tab from mp-tab-control's strip (frees up the slot).
2157
+ draggedContent.setAttribute('data-hidden', '');
2158
+ // Build placeholder header + content. The placeholder uses a unique tabId
2159
+ // (`__dock-placeholder__`) so its slot names don't collide with real panes.
2160
+ // We mirror the dragged tab's text into the placeholder (dimmed via opacity)
2161
+ // so the strip reads as "this tab is being dragged" rather than "empty slot".
2162
+ const placeholderTabId = '__dock-placeholder__';
2163
+ const phHeader = this.documentRef.createElement('span');
2164
+ phHeader.setAttribute('slot', `${placeholderTabId}-header`);
2165
+ phHeader.classList.add('dock-tab');
2166
+ phHeader.dataset['placeholder'] = 'true';
2167
+ phHeader.dataset['tabId'] = placeholderTabId;
2168
+ phHeader.setAttribute('aria-hidden', 'true');
2169
+ phHeader.textContent = draggedHeader.textContent;
2170
+ // `display: inline-block` is required for `min-width` to take effect on the
2171
+ // span. Without it, an inline element ignores min-width and the placeholder
2172
+ // collapses to its content width (or 0 if textContent is also empty),
2173
+ // leaving a "mini-thumb" in the strip.
2174
+ phHeader.style.display = 'inline-block';
2175
+ phHeader.style.minWidth = `${slotContentWidth}px`;
2176
+ phHeader.style.opacity = '0.5';
2177
+ const phContent = this.documentRef.createElement('div');
2178
+ phContent.setAttribute('slot', `${placeholderTabId}-content`);
2179
+ phContent.classList.add('dock-stack__pane');
2180
+ phContent.dataset['placeholder'] = 'true';
2181
+ // Insert before the dragged header span so the placeholder appears in
2182
+ // the dragged tab's original strip position. The mutation observer in
2183
+ // mp-tab-control will refresh the tab list automatically.
2184
+ stack.insertBefore(phHeader, draggedHeader);
2185
+ stack.insertBefore(phContent, draggedContent);
2470
2186
  if (this.dragState) {
2471
- this.dragState.placeholderHeader = header;
2472
- this.dragState.placeholderEl = placeholder;
2187
+ this.dragState.placeholderHeader = stack;
2188
+ this.dragState.placeholderEl = phHeader;
2473
2189
  }
2474
2190
  }
2475
- // Move the placeholder to the computed target index within the header
2476
- updateHeaderDragPlaceholderPosition(header, targetIndex) {
2477
- const placeholder = this.dragState?.placeholderEl ?? null;
2478
- if (!placeholder) {
2191
+ // Move the placeholder to the computed target index within the strip.
2192
+ // We reorder light-DOM children (header span + matching content div); the
2193
+ // mp-tab-control then re-renders the strip in the new order on slotchange.
2194
+ updateHeaderDragPlaceholderPosition(stack, targetIndex) {
2195
+ if (stack.tagName !== 'MP-TAB-CONTROL')
2196
+ return;
2197
+ const phHeader = this.dragState?.placeholderEl ?? null;
2198
+ if (!phHeader)
2479
2199
  return;
2480
- }
2481
2200
  const draggedPane = this.dragState?.pane ?? null;
2482
- const tabs = Array.from(header.querySelectorAll('.dock-tab'))
2483
- .filter((t) => t !== placeholder && (!draggedPane || t.dataset['pane'] !== draggedPane));
2484
- const clampedTarget = Math.max(0, Math.min(targetIndex, tabs.length));
2485
- const ref = tabs[clampedTarget] ?? null;
2486
- header.insertBefore(placeholder, ref);
2487
- }
2488
- // Remove placeholder and restore original tab visibility
2201
+ // Find all real header spans (excluding the placeholder + the hidden dragged one).
2202
+ const realHeaders = Array.from(stack.querySelectorAll(':scope > .dock-tab')).filter((h) => h !== phHeader &&
2203
+ (!draggedPane || h.dataset['pane'] !== draggedPane));
2204
+ const clampedTarget = Math.max(0, Math.min(targetIndex, realHeaders.length));
2205
+ const ref = realHeaders[clampedTarget] ?? null;
2206
+ stack.insertBefore(phHeader, ref);
2207
+ // Keep the placeholder content adjacent to its header so child-order
2208
+ // remains predictable for slotchange-driven re-renders.
2209
+ const phContent = stack.querySelector(`:scope > .dock-stack__pane[data-placeholder="true"]`);
2210
+ if (phContent && phHeader.nextElementSibling !== phContent) {
2211
+ stack.insertBefore(phContent, phHeader.nextElementSibling);
2212
+ }
2213
+ }
2214
+ // Remove placeholder and restore the dragged tab's visibility.
2489
2215
  clearHeaderDragPlaceholder() {
2490
2216
  const ph = this.dragState?.placeholderEl ?? null;
2491
- const header = this.dragState?.placeholderHeader ?? null;
2492
- if (header) {
2493
- const dragged = this.dragState?.pane
2494
- ? (Array.from(header.querySelectorAll('.dock-tab')).find((t) => t.dataset['pane'] === this.dragState?.pane) ?? null)
2495
- : null;
2496
- if (dragged) {
2497
- dragged.style.display = '';
2217
+ const stack = this.dragState?.placeholderHeader ?? null;
2218
+ if (stack) {
2219
+ // Restore the dragged content div's visibility so its strip tab returns.
2220
+ if (this.dragState?.pane) {
2221
+ const draggedContent = stack.querySelector(`:scope > .dock-stack__pane[data-pane="${CSS.escape(this.dragState.pane)}"]`);
2222
+ draggedContent?.removeAttribute('data-hidden');
2498
2223
  }
2224
+ // Remove the placeholder content div sibling.
2225
+ const phContent = stack.querySelector(`:scope > .dock-stack__pane[data-placeholder="true"]`);
2226
+ phContent?.remove();
2499
2227
  }
2500
2228
  if (ph && ph.parentElement) {
2501
2229
  ph.parentElement.removeChild(ph);
@@ -2511,11 +2239,9 @@ class MintDockManagerElement extends LitElement {
2511
2239
  }
2512
2240
  this.lastDragPointerPosition = null;
2513
2241
  const win = this.windowRef;
2514
- win?.addEventListener('mousemove', this.onDragMouseMove, true);
2515
- win?.addEventListener('touchmove', this.onDragTouchMove, { passive: false });
2516
- win?.addEventListener('mouseup', this.onDragMouseUp, true);
2517
- win?.addEventListener('touchend', this.onDragTouchEnd, true);
2518
- win?.addEventListener('touchcancel', this.onDragTouchEnd, true);
2242
+ win?.addEventListener('pointermove', this.onDragPointerMove, true);
2243
+ win?.addEventListener('pointerup', this.onDragPointerUp, true);
2244
+ win?.addEventListener('pointercancel', this.onDragPointerCancel, true);
2519
2245
  this.dragPointerTrackingActive = true;
2520
2246
  }
2521
2247
  stopDragPointerTracking() {
@@ -2523,52 +2249,38 @@ class MintDockManagerElement extends LitElement {
2523
2249
  return;
2524
2250
  }
2525
2251
  const win = this.windowRef;
2526
- win?.removeEventListener('mousemove', this.onDragMouseMove, true);
2527
- win?.removeEventListener('touchmove', this.onDragTouchMove);
2528
- win?.removeEventListener('mouseup', this.onDragMouseUp, true);
2529
- win?.removeEventListener('touchend', this.onDragTouchEnd, true);
2530
- win?.removeEventListener('touchcancel', this.onDragTouchEnd, true);
2252
+ win?.removeEventListener('pointermove', this.onDragPointerMove, true);
2253
+ win?.removeEventListener('pointerup', this.onDragPointerUp, true);
2254
+ win?.removeEventListener('pointercancel', this.onDragPointerCancel, true);
2531
2255
  this.dragPointerTrackingActive = false;
2532
2256
  this.lastDragPointerPosition = null;
2533
2257
  this.clearPendingDragEndTimeout();
2534
2258
  }
2535
- onDragMouseMove(event) {
2259
+ onDragPointerMove(event) {
2536
2260
  if (!this.dragState) {
2537
2261
  this.stopDragPointerTracking();
2538
2262
  return;
2539
2263
  }
2540
- if (event.buttons === 0) {
2541
- this.scheduleDeferredDragEnd();
2542
- return;
2543
- }
2544
2264
  this.lastDragPointerPosition = { x: event.clientX, y: event.clientY };
2545
2265
  this.updateDraggedFloatingPositionFromPoint(event.clientX, event.clientY);
2546
2266
  }
2547
- onDragTouchMove(event) {
2548
- if (!this.dragState) {
2549
- this.stopDragPointerTracking();
2550
- return;
2551
- }
2552
- const touch = event.touches[0];
2553
- if (!touch) {
2554
- this.scheduleDeferredDragEnd();
2555
- return;
2556
- }
2557
- event.preventDefault();
2558
- event.stopPropagation();
2559
- this.lastDragPointerPosition = { x: touch.clientX, y: touch.clientY };
2560
- this.updateDraggedFloatingPositionFromPoint(touch.clientX, touch.clientY);
2561
- }
2562
- onDragMouseUp() {
2563
- // Prefer committing a drop from pointer-up since some browsers suppress drop events
2267
+ onDragPointerUp(event) {
2268
+ // Commit the drop from pointer release; the pointer-up coordinates are
2269
+ // authoritative for which stack/zone the user dropped into.
2564
2270
  if (this.dragState) {
2565
- const pos = this.lastDragPointerPosition;
2566
- if (pos) {
2567
- this.finalizeDropFromPoint(pos.x, pos.y);
2271
+ const x = Number.isFinite(event.clientX) ? event.clientX : this.lastDragPointerPosition?.x;
2272
+ const y = Number.isFinite(event.clientY) ? event.clientY : this.lastDragPointerPosition?.y;
2273
+ if (x !== undefined && y !== undefined) {
2274
+ this.finalizeDropFromPoint(x, y);
2568
2275
  }
2569
2276
  }
2570
2277
  this.handleDragPointerUpCommon();
2571
2278
  }
2279
+ onDragPointerCancel() {
2280
+ // OS-level cancel (e.g. pointer capture lost): end the drag without
2281
+ // committing a drop.
2282
+ this.handleDragPointerUpCommon();
2283
+ }
2572
2284
  // Convert a currently in-header tab drag into a floating window
2573
2285
  convertPendingTabDragToFloating(clientX, clientY) {
2574
2286
  if (!this.dragState) {
@@ -2601,22 +2313,34 @@ class MintDockManagerElement extends LitElement {
2601
2313
  : stackRect && Number.isFinite(stackRect.height)
2602
2314
  ? stackRect.height
2603
2315
  : fallbackHeight;
2604
- const pointerOffsetX = Number.isFinite(this.dragState?.pointerOffsetX)
2605
- ? this.dragState.pointerOffsetX
2606
- : metrics && Number.isFinite(metrics.pointerOffsetX)
2607
- ? metrics.pointerOffsetX
2608
- : width / 2;
2609
- const pointerOffsetY = Number.isFinite(this.dragState?.pointerOffsetY)
2610
- ? this.dragState.pointerOffsetY
2611
- : metrics && Number.isFinite(metrics.pointerOffsetY)
2612
- ? metrics.pointerOffsetY
2613
- : height / 2;
2614
- const pointerLeft = Number.isFinite(clientX)
2615
- ? clientX - hostRect.left - pointerOffsetX
2616
- : 0;
2617
- const pointerTop = Number.isFinite(clientY)
2618
- ? clientY - hostRect.top - pointerOffsetY
2619
- : 0;
2316
+ // Place the floating wrapper exactly where the docked stack was, so the
2317
+ // pane appears in-place at the moment of detach instead of jumping under
2318
+ // the cursor. metrics.left/top are host-relative (captured at pointerdown
2319
+ // in captureTabDragMetrics). Compensate for the floating wrapper's 1px
2320
+ // border so the visible content edge lines up with the original stack's
2321
+ // visible content edge (the docked .dock-stack also has a 1px border, so
2322
+ // the inner content rectangles match after this offset).
2323
+ const FLOATING_BORDER = 1;
2324
+ const initialLeft = metrics && Number.isFinite(metrics.left)
2325
+ ? metrics.left - FLOATING_BORDER
2326
+ : Number.isFinite(clientX)
2327
+ ? clientX - hostRect.left - width / 2
2328
+ : 0;
2329
+ const initialTop = metrics && Number.isFinite(metrics.top)
2330
+ ? metrics.top - FLOATING_BORDER
2331
+ : Number.isFinite(clientY)
2332
+ ? clientY - hostRect.top - height / 2
2333
+ : 0;
2334
+ // Derive pointerOffset from the cursor's actual position relative to the
2335
+ // freshly-placed wrapper (not from pointerdown metrics) so the very next
2336
+ // pointermove translates into a wrapper move of exactly the cursor delta
2337
+ // — no jump, no drift.
2338
+ const pointerOffsetX = Number.isFinite(clientX)
2339
+ ? clientX - hostRect.left - initialLeft
2340
+ : width / 2;
2341
+ const pointerOffsetY = Number.isFinite(clientY)
2342
+ ? clientY - hostRect.top - initialTop
2343
+ : height / 2;
2620
2344
  // Remove pane from its current stack and create a new floating entry
2621
2345
  this.removePaneFromLocation(location, pane);
2622
2346
  const floatingStack = {
@@ -2626,8 +2350,8 @@ class MintDockManagerElement extends LitElement {
2626
2350
  };
2627
2351
  const floatingLayout = {
2628
2352
  bounds: {
2629
- left: pointerLeft,
2630
- top: pointerTop,
2353
+ left: initialLeft,
2354
+ top: initialTop,
2631
2355
  width,
2632
2356
  height,
2633
2357
  },
@@ -2646,32 +2370,54 @@ class MintDockManagerElement extends LitElement {
2646
2370
  state.floatingIndex = newIndex;
2647
2371
  state.pointerOffsetX = pointerOffsetX;
2648
2372
  state.pointerOffsetY = pointerOffsetY;
2373
+ // Now that the wrapper exists, mark it so pointer-events:none kicks in
2374
+ // and findStackAtPoint can see through to docked stacks underneath.
2375
+ this.markDraggedFloatingWrapper();
2649
2376
  this.dispatchLayoutChanged();
2650
2377
  }
2651
- // Compute the intended tab insert index within a header based on pointer X
2652
- // Adds a slight rightward bias and uses the placeholder rect (if present)
2653
- // to ensure offsets are correct even when the dragged tab is display:none.
2654
- computeHeaderInsertIndex(header, clientX) {
2655
- const allTabs = Array.from(header.querySelectorAll('.dock-tab'));
2656
- if (allTabs.length === 0) {
2378
+ // Toggle data-dragging on the floating wrapper currently associated with
2379
+ // the active pane drag (dragState.floatingIndex), if any. Used to make the
2380
+ // wrapper transparent to elementsFromPoint so drop zones can be shown on
2381
+ // stacks underneath. clearDraggedFloatingWrapper() is the inverse.
2382
+ markDraggedFloatingWrapper() {
2383
+ const fi = this.dragState?.floatingIndex;
2384
+ if (fi === null || fi === undefined || fi < 0)
2385
+ return;
2386
+ const wrapper = this.getFloatingWrapper(fi);
2387
+ if (wrapper)
2388
+ wrapper.dataset['dragging'] = 'true';
2389
+ }
2390
+ clearDraggedFloatingWrapperMarkers() {
2391
+ const layer = this.floatingLayerEl;
2392
+ if (!layer)
2393
+ return;
2394
+ layer.querySelectorAll('.dock-floating[data-dragging="true"]').forEach((el) => {
2395
+ delete el.dataset['dragging'];
2396
+ });
2397
+ }
2398
+ // Compute the intended tab insert index within a stack's strip based on pointer X.
2399
+ // Uses the rendered tab buttons inside mp-tab-control's shadow strip for geometry;
2400
+ // the dragged tab is hidden during drag (its content has data-hidden), and the
2401
+ // placeholder button (if present) gives us the dragged-position reference.
2402
+ computeHeaderInsertIndex(stack, clientX) {
2403
+ if (stack.tagName !== 'MP-TAB-CONTROL')
2404
+ return 0;
2405
+ const allTabButtons = this.getStackTabButtons(stack);
2406
+ if (allTabButtons.length === 0) {
2657
2407
  return 0;
2658
2408
  }
2659
- const draggedPane = this.dragState?.pane ?? null;
2660
- const draggedEl = draggedPane
2661
- ? (allTabs.find((t) => t.dataset['pane'] === draggedPane) ?? null)
2409
+ const placeholderHeader = stack.querySelector(':scope > .dock-tab[data-placeholder="true"]');
2410
+ const placeholderTabId = placeholderHeader?.dataset['tabId'];
2411
+ const placeholderButton = placeholderTabId
2412
+ ? allTabButtons.find((b) => b.id === `${placeholderTabId}-header-button`) ?? null
2662
2413
  : null;
2663
- const placeholderEl = header.querySelector('.dock-tab[data-placeholder="true"]');
2664
- const targets = allTabs.filter((t) => t !== draggedEl && t !== placeholderEl);
2414
+ const targets = allTabButtons.filter((b) => b !== placeholderButton);
2665
2415
  if (targets.length === 0) {
2666
2416
  return 0;
2667
2417
  }
2668
2418
  const rightBias = 12;
2669
2419
  const leftBias = 0;
2670
- const baseRect = placeholderEl
2671
- ? placeholderEl.getBoundingClientRect()
2672
- : draggedEl
2673
- ? draggedEl.getBoundingClientRect()
2674
- : null;
2420
+ const baseRect = placeholderButton ? placeholderButton.getBoundingClientRect() : null;
2675
2421
  const rectValid = !!baseRect && Number.isFinite(baseRect.width) && baseRect.width > 0;
2676
2422
  const draggedCenter = rectValid && baseRect ? baseRect.left + baseRect.width / 2 : null;
2677
2423
  for (let i = 0; i < targets.length; i += 1) {
@@ -2705,9 +2451,6 @@ class MintDockManagerElement extends LitElement {
2705
2451
  }
2706
2452
  }
2707
2453
  }
2708
- onDragTouchEnd() {
2709
- this.handleDragPointerUpCommon();
2710
- }
2711
2454
  // Commit a drop using current pointer coordinates and joystick state
2712
2455
  finalizeDropFromPoint(clientX, clientY) {
2713
2456
  if (!this.dragState) {
@@ -2733,17 +2476,14 @@ class MintDockManagerElement extends LitElement {
2733
2476
  stackPath &&
2734
2477
  this.pathsEqual(stackPath, this.dragState.sourcePath) &&
2735
2478
  (!zone || zone === 'center')) {
2736
- const header = stack.querySelector('.dock-stack__header');
2737
- if (header) {
2738
- const location = this.resolveStackLocation(path);
2739
- if (location) {
2740
- const idx = this.computeHeaderInsertIndex(header, clientX);
2741
- this.reorderPaneInLocationAtIndex(location, this.dragState.pane, idx);
2742
- this.renderLayout();
2743
- this.dispatchLayoutChanged();
2744
- this.dragState.dropHandled = true;
2745
- return;
2746
- }
2479
+ const location = this.resolveStackLocation(path);
2480
+ if (location) {
2481
+ const idx = this.computeHeaderInsertIndex(stack, clientX);
2482
+ this.reorderPaneInLocationAtIndex(location, this.dragState.pane, idx);
2483
+ this.renderLayout();
2484
+ this.dispatchLayoutChanged();
2485
+ this.dragState.dropHandled = true;
2486
+ return;
2747
2487
  }
2748
2488
  }
2749
2489
  if (path && this.isDropZone(zone)) {
@@ -2782,112 +2522,6 @@ class MintDockManagerElement extends LitElement {
2782
2522
  ? win.setTimeout(completeDrag, 0)
2783
2523
  : setTimeout(completeDrag, 0);
2784
2524
  }
2785
- onDrop(event) {
2786
- if (!this.dragState) {
2787
- return;
2788
- }
2789
- event.preventDefault();
2790
- const pointFromEvent = Number.isFinite(event.clientX) && Number.isFinite(event.clientY)
2791
- ? { clientX: event.clientX, clientY: event.clientY }
2792
- : null;
2793
- const point = pointFromEvent ??
2794
- (this.lastDragPointerPosition
2795
- ? {
2796
- clientX: this.lastDragPointerPosition.x,
2797
- clientY: this.lastDragPointerPosition.y,
2798
- }
2799
- : null);
2800
- const stack = this.findStackElement(event) ??
2801
- (point ? this.findStackAtPoint(point.clientX, point.clientY) : null);
2802
- // Prefer joystick's stored target path when the joystick is visible (drop over buttons)
2803
- const joystickVisible = this.dropJoystick.dataset['visible'] === 'true';
2804
- const joystickPath = this.parsePath(this.dropJoystick.dataset['path']);
2805
- const joystickTarget = this.dropJoystickTarget;
2806
- const joystickTargetPath = joystickTarget ? this.parsePath(joystickTarget.dataset['path']) : null;
2807
- let path = stack
2808
- ? this.parsePath(stack.dataset['path'])
2809
- : (joystickPath ?? joystickTargetPath);
2810
- if (!path && joystickVisible) {
2811
- // As a last resort, target the main dock surface only when empty
2812
- const dockPath = this.parsePath(this.dockedEl.dataset['path']);
2813
- path = (!this.rootLayout ? dockPath : null);
2814
- }
2815
- // Defer same-header reorder decision until after zone resolution below
2816
- // Prefer joystick's active zone if available, else infer from event/point
2817
- const joystickZone = this.dropJoystick.dataset['zone'];
2818
- const eventZoneHint = this.extractDropZoneFromEvent(event);
2819
- const pointZoneHint = point ? this.findDropZoneByPoint(point.clientX, point.clientY) : null;
2820
- const zone = this.isDropZone(joystickZone)
2821
- ? joystickZone
2822
- : stack
2823
- ? this.computeDropZone(stack, point ?? event, pointZoneHint ?? eventZoneHint)
2824
- : (this.isDropZone(pointZoneHint ?? eventZoneHint) ? (pointZoneHint ?? eventZoneHint) : null);
2825
- // If still in same header and no side zone chosen, treat as in-header reorder
2826
- if (this.dragState &&
2827
- this.dragState.floatingIndex !== null &&
2828
- this.dragState.floatingIndex < 0 &&
2829
- stack &&
2830
- path &&
2831
- this.pathsEqual(path, this.dragState.sourcePath) &&
2832
- (!zone || zone === 'center')) {
2833
- const header = stack.querySelector('.dock-stack__header');
2834
- if (header) {
2835
- const x = (point ? point.clientX : event.clientX);
2836
- if (Number.isFinite(x)) {
2837
- const location = this.resolveStackLocation(path);
2838
- if (location) {
2839
- const idx = this.computeHeaderInsertIndex(header, x);
2840
- this.reorderPaneInLocationAtIndex(location, this.dragState.pane, idx);
2841
- this.renderLayout();
2842
- this.dispatchLayoutChanged();
2843
- this.dragState.dropHandled = true;
2844
- this.endPaneDrag();
2845
- return;
2846
- }
2847
- }
2848
- }
2849
- }
2850
- // If joystick is visible and both path and zone are resolved, force using joystick as authoritative
2851
- if (joystickVisible && path && this.isDropZone(joystickZone)) {
2852
- this.handleDrop(path, joystickZone);
2853
- this.endPaneDrag();
2854
- return;
2855
- }
2856
- if (!zone) {
2857
- this.hideDropIndicator();
2858
- this.endPaneDrag();
2859
- return;
2860
- }
2861
- this.handleDrop(path, zone);
2862
- this.endPaneDrag();
2863
- }
2864
- onDragLeave(event) {
2865
- const related = event.relatedTarget;
2866
- // During active drags, browsers can emit spurious dragleave with null
2867
- // relatedTarget while the pointer is still over the joystick/buttons.
2868
- // Be conservative: if we can resolve a stack/joystick at the last known
2869
- // pointer position, don’t hide (prevents flicker of active state).
2870
- if (this.dragState) {
2871
- const pos = (Number.isFinite(event.clientX) && Number.isFinite(event.clientY))
2872
- ? { x: event.clientX, y: event.clientY }
2873
- : this.lastDragPointerPosition;
2874
- if (pos) {
2875
- const stackAtPoint = this.findStackAtPoint(pos.x, pos.y);
2876
- if (stackAtPoint) {
2877
- return; // still inside our drop area; ignore this dragleave
2878
- }
2879
- }
2880
- }
2881
- if (!related) {
2882
- this.hideDropIndicator();
2883
- return;
2884
- }
2885
- const rootContains = this.rootEl.contains(related);
2886
- const joystickContains = this.dropJoystick.contains(related);
2887
- if (!rootContains && !joystickContains) {
2888
- this.hideDropIndicator();
2889
- }
2890
- }
2891
2525
  handleDrop(targetPath, zone) {
2892
2526
  if (!this.dragState || !targetPath) {
2893
2527
  return;
@@ -3354,24 +2988,6 @@ class MintDockManagerElement extends LitElement {
3354
2988
  }
3355
2989
  return null;
3356
2990
  }
3357
- findStackElement(event) {
3358
- const path = event.composedPath();
3359
- const stack = this.findStackInTargets(path);
3360
- if (stack) {
3361
- return stack;
3362
- }
3363
- // If the root dock area is empty, treat the docked surface as a valid
3364
- // target when it appears in the composed path.
3365
- if (!this.rootLayout) {
3366
- for (const target of path) {
3367
- if (target instanceof HTMLElement &&
3368
- (target === this.dockedEl || target.classList.contains('dock-docked'))) {
3369
- return this.dockedEl;
3370
- }
3371
- }
3372
- }
3373
- return null;
3374
- }
3375
2991
  findStackInTargets(targets) {
3376
2992
  for (const element of targets) {
3377
2993
  if (!(element instanceof HTMLElement)) {
@@ -3391,21 +3007,16 @@ class MintDockManagerElement extends LitElement {
3391
3007
  }
3392
3008
  activatePane(stack, paneName, path) {
3393
3009
  stack.dataset['activePane'] = paneName;
3394
- const headerButtons = stack.querySelectorAll('.dock-tab');
3395
- headerButtons.forEach((button) => {
3396
- const isSelected = button.dataset['pane'] === paneName;
3397
- button.classList.toggle('dock-tab--active', isSelected);
3398
- button.setAttribute('aria-selected', String(isSelected));
3399
- });
3400
- const panes = stack.querySelectorAll('.dock-stack__pane');
3401
- panes.forEach((pane) => {
3402
- if (pane.dataset['pane'] === paneName) {
3403
- pane.removeAttribute('hidden');
3404
- }
3405
- else {
3406
- pane.setAttribute('hidden', '');
3010
+ // Reflect to mp-tab-control's `active-tab` attribute. The WC handles
3011
+ // strip button styling (active class, aria-selected) + body-slot
3012
+ // projection automatically via the named-slot pattern.
3013
+ if (stack.tagName === 'MP-TAB-CONTROL') {
3014
+ const headerSpan = stack.querySelector(`:scope > .dock-tab[data-pane="${CSS.escape(paneName)}"]`);
3015
+ const tabId = headerSpan?.dataset['tabId'];
3016
+ if (tabId) {
3017
+ stack.setAttribute('active-tab', tabId);
3407
3018
  }
3408
- });
3019
+ }
3409
3020
  const location = this.resolveStackLocation(path);
3410
3021
  if (!location) {
3411
3022
  return;
@@ -3611,7 +3222,10 @@ class MintDockManagerElement extends LitElement {
3611
3222
  return { type: 'docked', segments: [...path.segments] };
3612
3223
  }
3613
3224
  parsePath(path) {
3614
- if (!path) {
3225
+ // The root splitter is tagged with data-path="" (raw segments-join of an
3226
+ // empty array) so empty string is a valid path representing root docked.
3227
+ // Only null/undefined is "no path".
3228
+ if (path == null) {
3615
3229
  return null;
3616
3230
  }
3617
3231
  if (path.startsWith('f:')) {
@@ -3845,12 +3459,12 @@ class BsDockManagerComponent {
3845
3459
  return this.cloneLayout(this._layout);
3846
3460
  }
3847
3461
  constructor() {
3848
- this.layout = input(null, ...(ngDevMode ? [{ debugName: "layout" }] : []));
3462
+ this.layout = input(null, ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
3849
3463
  this.layoutChange = output();
3850
3464
  this.layoutSnapshotChange = output();
3851
- this.layoutString = signal(null, ...(ngDevMode ? [{ debugName: "layoutString" }] : []));
3852
- this.panes = contentChildren(BsDockPaneComponent, ...(ngDevMode ? [{ debugName: "panes" }] : []));
3853
- this.managerRef = viewChild('manager', ...(ngDevMode ? [{ debugName: "managerRef" }] : []));
3465
+ this.layoutString = signal(null, ...(ngDevMode ? [{ debugName: "layoutString" }] : /* istanbul ignore next */ []));
3466
+ this.panes = contentChildren(BsDockPaneComponent, ...(ngDevMode ? [{ debugName: "panes" }] : /* istanbul ignore next */ []));
3467
+ this.managerRef = viewChild('manager', ...(ngDevMode ? [{ debugName: "managerRef" }] : /* istanbul ignore next */ []));
3854
3468
  this.trackByPane = (_, pane) => pane.name();
3855
3469
  this._layout = { root: null, floating: [] };
3856
3470
  const documentRef = inject(DOCUMENT);
@@ -3913,10 +3527,10 @@ class BsDockManagerComponent {
3913
3527
  cloneLayout(layout) {
3914
3528
  return JSON.parse(JSON.stringify(layout));
3915
3529
  }
3916
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: BsDockManagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3917
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.6", type: BsDockManagerComponent, isStandalone: true, selector: "bs-dock-manager", inputs: { layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { layoutChange: "layoutChange", layoutSnapshotChange: "layoutSnapshotChange" }, queries: [{ propertyName: "panes", predicate: BsDockPaneComponent, isSignal: true }], viewQueries: [{ propertyName: "managerRef", first: true, predicate: ["manager"], descendants: true, isSignal: true }], ngImport: i0, template: "<mint-dock-manager\n #manager\n class=\"bs-dock-manager\"\n [attr.layout]=\"layoutString()\"\n (dock-layout-changed)=\"onLayoutChanged($event)\"\n >\n @for (pane of panes(); track trackByPane($index, pane)) {\n <div class=\"bs-dock-pane\" [attr.slot]=\"pane.name()\">\n <ng-container *ngTemplateOutlet=\"pane.template()\"></ng-container>\n </div>\n }\n</mint-dock-manager>\n", styles: [":host{display:block;width:100%;height:100%}.bs-dock-manager{display:block;width:100%;height:100%}.bs-dock-pane{display:contents}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
3530
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: BsDockManagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3531
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: BsDockManagerComponent, isStandalone: true, selector: "bs-dock-manager", inputs: { layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { layoutChange: "layoutChange", layoutSnapshotChange: "layoutSnapshotChange" }, queries: [{ propertyName: "panes", predicate: BsDockPaneComponent, isSignal: true }], viewQueries: [{ propertyName: "managerRef", first: true, predicate: ["manager"], descendants: true, isSignal: true }], ngImport: i0, template: "<mint-dock-manager\n #manager\n class=\"bs-dock-manager\"\n [attr.layout]=\"layoutString()\"\n (dock-layout-changed)=\"onLayoutChanged($event)\"\n >\n @for (pane of panes(); track trackByPane($index, pane)) {\n <div class=\"bs-dock-pane\" [attr.slot]=\"pane.name()\">\n <ng-container *ngTemplateOutlet=\"pane.template()\"></ng-container>\n </div>\n }\n</mint-dock-manager>\n", styles: [":host{display:block;width:100%;height:100%}.bs-dock-manager{display:block;width:100%;height:100%}.bs-dock-pane{display:contents}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
3918
3532
  }
3919
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: BsDockManagerComponent, decorators: [{
3533
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: BsDockManagerComponent, decorators: [{
3920
3534
  type: Component,
3921
3535
  args: [{ selector: 'bs-dock-manager', imports: [NgTemplateOutlet], schemas: [CUSTOM_ELEMENTS_SCHEMA], changeDetection: ChangeDetectionStrategy.OnPush, template: "<mint-dock-manager\n #manager\n class=\"bs-dock-manager\"\n [attr.layout]=\"layoutString()\"\n (dock-layout-changed)=\"onLayoutChanged($event)\"\n >\n @for (pane of panes(); track trackByPane($index, pane)) {\n <div class=\"bs-dock-pane\" [attr.slot]=\"pane.name()\">\n <ng-container *ngTemplateOutlet=\"pane.template()\"></ng-container>\n </div>\n }\n</mint-dock-manager>\n", styles: [":host{display:block;width:100%;height:100%}.bs-dock-manager{display:block;width:100%;height:100%}.bs-dock-pane{display:contents}\n"] }]
3922
3536
  }], ctorParameters: () => [], propDecorators: { layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: false }] }], layoutChange: [{ type: i0.Output, args: ["layoutChange"] }], layoutSnapshotChange: [{ type: i0.Output, args: ["layoutSnapshotChange"] }], panes: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => BsDockPaneComponent), { isSignal: true }] }], managerRef: [{ type: i0.ViewChild, args: ['manager', { isSignal: true }] }] } });