@mintplayer/ng-bootstrap 21.22.0 → 21.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +798 -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,28 @@ 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
+ touch-action: none;
160
+ background: linear-gradient(to bottom, rgba(var(--bs-primary-rgb), 0.15), rgba(var(--bs-primary-rgb), 0.05));
161
+ border-bottom: 1px solid var(--bs-primary-border-subtle);
157
162
  user-select: none;
158
163
  -webkit-user-select: none;
159
164
  }
@@ -162,7 +167,7 @@ const styles = unsafeCSS(`:host {
162
167
  flex: 1 1 auto;
163
168
  font-size: 0.875rem;
164
169
  font-weight: 500;
165
- color: rgba(30, 41, 59, 0.95);
170
+ color: var(--bs-body-color);
166
171
  overflow: hidden;
167
172
  text-overflow: ellipsis;
168
173
  white-space: nowrap;
@@ -178,13 +183,14 @@ const styles = unsafeCSS(`:host {
178
183
  position: absolute;
179
184
  pointer-events: auto;
180
185
  z-index: 2;
181
- background: rgba(148, 163, 184, 0.25);
186
+ background: rgba(var(--bs-primary-rgb), 0.1);
182
187
  transition: background 120ms ease;
188
+ touch-action: none;
183
189
  }
184
190
 
185
191
  .dock-floating__resizer:hover,
186
192
  .dock-floating__resizer[data-resizing=true] {
187
- background: rgba(148, 163, 184, 0.4);
193
+ background: rgba(var(--bs-primary-rgb), 0.3);
188
194
  }
189
195
 
190
196
  .dock-floating__resizer--top,
@@ -254,75 +260,10 @@ const styles = unsafeCSS(`:host {
254
260
  }
255
261
 
256
262
  .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
263
  flex: 1 1 0;
274
264
  position: relative;
275
265
  }
276
266
 
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
267
  .dock-intersection-handle {
327
268
  position: absolute;
328
269
  width: 1rem;
@@ -330,12 +271,13 @@ const styles = unsafeCSS(`:host {
330
271
  margin-left: -0.5rem;
331
272
  margin-top: -0.5rem;
332
273
  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);
274
+ background: var(--bs-primary-bg-subtle);
275
+ border: 1px solid var(--bs-primary-border-subtle);
276
+ box-shadow: var(--bs-box-shadow-sm);
336
277
  cursor: all-scroll;
337
278
  pointer-events: auto;
338
- opacity: 0;
279
+ touch-action: none;
280
+ opacity: 0.45;
339
281
  transition: background 120ms ease, border-color 120ms ease, opacity 120ms ease;
340
282
  }
341
283
 
@@ -343,8 +285,8 @@ const styles = unsafeCSS(`:host {
343
285
  .dock-intersection-handle:focus-visible,
344
286
  .dock-intersection-handle[data-visible=true],
345
287
  .dock-intersection-handle[data-resizing=true] {
346
- background: rgba(59, 130, 246, 0.35);
347
- border-color: rgba(59, 130, 246, 0.9);
288
+ background: rgba(var(--bs-primary-rgb), 0.35);
289
+ border-color: var(--bs-primary);
348
290
  opacity: 1;
349
291
  outline: none;
350
292
  }
@@ -356,84 +298,45 @@ const styles = unsafeCSS(`:host {
356
298
  margin-left: -3px;
357
299
  margin-top: -3px;
358
300
  border-radius: 50%;
359
- background: rgba(59, 130, 246, 0.7);
360
- box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15);
301
+ background: var(--bs-primary);
302
+ box-shadow: 0 0 0 2px var(--bs-primary-bg-subtle);
361
303
  pointer-events: none;
362
304
  z-index: 130;
363
305
  }
364
306
 
365
307
  .dock-stack {
366
- display: flex;
367
- flex-direction: column;
368
308
  flex: 1 1 0;
369
- border: 1px solid rgba(0, 0, 0, 0.2);
309
+ border: 1px solid var(--bs-border-color);
370
310
  border-radius: 0.25rem;
371
- background: rgba(255, 255, 255, 0.75);
311
+ background: var(--bs-body-bg);
372
312
  backdrop-filter: blur(4px);
373
313
  }
374
314
 
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
315
  .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
316
  cursor: grab;
393
- transition: background 160ms ease;
317
+ display: block;
318
+ padding: 0.5rem 1rem;
319
+ margin: -0.5rem -1rem;
320
+ touch-action: none;
394
321
  }
395
322
 
396
323
  .dock-tab:active {
397
324
  cursor: grabbing;
398
325
  }
399
326
 
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
327
  .dock-stack__pane {
421
328
  position: relative;
422
- flex: 1 1 100%;
423
329
  display: flex;
424
330
  flex-direction: column;
425
331
  overflow: hidden;
426
- }
427
-
428
- .dock-stack__pane[hidden] {
429
- display: none !important;
332
+ height: 100%;
430
333
  }
431
334
 
432
335
  .dock-drop-indicator {
433
336
  position: absolute;
434
337
  pointer-events: none;
435
- border: 2px solid rgba(59, 130, 246, 0.9);
436
- background: rgba(59, 130, 246, 0.2);
338
+ border: 2px solid var(--bs-primary);
339
+ background: rgba(var(--bs-primary-rgb), 0.2);
437
340
  border-radius: 0.25rem;
438
341
  opacity: 0;
439
342
  transition: opacity 120ms ease;
@@ -452,8 +355,8 @@ const styles = unsafeCSS(`:host {
452
355
  gap: 0.125rem;
453
356
  padding: 0.125rem;
454
357
  border-radius: 999px;
455
- background: rgba(15, 23, 42, 0.15);
456
- box-shadow: 0 4px 12px rgba(15, 23, 42, 0.25);
358
+ background: var(--bs-tertiary-bg);
359
+ box-shadow: var(--bs-box-shadow);
457
360
  pointer-events: none;
458
361
  transform: translate(-50%, -50%);
459
362
  z-index: 110;
@@ -472,9 +375,9 @@ const styles = unsafeCSS(`:host {
472
375
  align-items: center;
473
376
  justify-content: center;
474
377
  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);
378
+ border: 1px solid var(--bs-primary-border-subtle);
379
+ background: var(--bs-body-bg);
380
+ color: var(--bs-primary);
478
381
  font-size: 0.75rem;
479
382
  line-height: 1;
480
383
  font-weight: 600;
@@ -486,13 +389,13 @@ const styles = unsafeCSS(`:host {
486
389
  .dock-drop-joystick__button[data-active=true],
487
390
  .dock-drop-joystick__button:hover,
488
391
  .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);
392
+ background: var(--bs-primary-bg-subtle);
393
+ border-color: var(--bs-primary);
394
+ color: var(--bs-primary);
492
395
  }
493
396
 
494
397
  .dock-drop-joystick__button:focus-visible {
495
- outline: 2px solid rgba(59, 130, 246, 0.9);
398
+ outline: 2px solid var(--bs-primary);
496
399
  outline-offset: 1px;
497
400
  }
498
401
 
@@ -519,39 +422,6 @@ class MintDockManagerElement extends LitElement {
519
422
  return [...(super.observedAttributes ?? []), 'layout'];
520
423
  }
521
424
  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
425
  renderSnapMarkersForCorner() {
556
426
  if (!this.showSnapMarkers)
557
427
  return;
@@ -562,17 +432,23 @@ class MintDockManagerElement extends LitElement {
562
432
  if (!this.cornerResizeState)
563
433
  return;
564
434
  const rootRect = this.rootEl.getBoundingClientRect();
565
- // Compute representative center lines from first entries
435
+ // Compute representative center lines from the dividers being resized.
436
+ // st.{hs,vs}[i].container is the <mp-splitter>; the divider lives in its
437
+ // shadow at getSplitterDividers(splitter)[index].
566
438
  let centerX = null;
567
439
  let centerY = null;
568
440
  const st = this.cornerResizeState;
569
441
  if (st.vs.length > 0) {
570
- const vRect = st.vs[0].container.querySelector(':scope > .dock-split__divider')?.getBoundingClientRect();
442
+ const v0 = st.vs[0];
443
+ const vDiv = this.getSplitterDividers(v0.container)[v0.index];
444
+ const vRect = vDiv?.getBoundingClientRect();
571
445
  if (vRect)
572
446
  centerX = vRect.left + vRect.width / 2 - rootRect.left;
573
447
  }
574
448
  if (st.hs.length > 0) {
575
- const hRect = st.hs[0].container.querySelector(':scope > .dock-split__divider')?.getBoundingClientRect();
449
+ const h0 = st.hs[0];
450
+ const hDiv = this.getSplitterDividers(h0.container)[h0.index];
451
+ const hRect = hDiv?.getBoundingClientRect();
576
452
  if (hRect)
577
453
  centerY = hRect.top + hRect.height / 2 - rootRect.top;
578
454
  }
@@ -610,19 +486,16 @@ class MintDockManagerElement extends LitElement {
610
486
  this.floatingLayouts = [];
611
487
  this.titles = {};
612
488
  this.pendingTabDragMetrics = null;
613
- this.resizeState = null;
614
489
  this.dragState = null;
615
490
  this.floatingDragState = null;
616
491
  this.floatingResizeState = null;
617
492
  this.intersectionRaf = null;
618
- this.intersectionHandles = new Map();
493
+ this.rootResizeObserver = null;
494
+ this.dockedMutationObserver = null;
619
495
  this.cornerResizeState = null;
620
496
  this.pointerTrackingActive = false;
621
497
  this.dragPointerTrackingActive = false;
622
498
  this.lastDragPointerPosition = null;
623
- // Localized snapping while dragging a divider
624
- this.activeSnapAxis = null;
625
- this.activeSnapTargets = [];
626
499
  // Localized snapping while dragging an intersection handle
627
500
  this.cornerSnapXTargets = [];
628
501
  this.cornerSnapYTargets = [];
@@ -631,19 +504,19 @@ class MintDockManagerElement extends LitElement {
631
504
  this.pendingDragEndTimeout = null;
632
505
  this.previousSplitSizes = new Map();
633
506
  this.instanceId = `mint-dock-${++MintDockManagerElement.instanceCounter}`;
507
+ // Set windowRef eagerly so connectedCallback's window-level drag listeners
508
+ // (added before firstUpdated runs) can actually attach. Without this,
509
+ // win?.addEventListener was a silent no-op on first connect and HTML5
510
+ // drag-to-detach gestures never reached the dock — the floating wrapper
511
+ // was created but stayed at its conversion-time coordinates because the
512
+ // 'drag' listener that updates its position was never attached.
513
+ this.windowRef = typeof window !== 'undefined' ? window : null;
634
514
  this.onPointerMove = this.onPointerMove.bind(this);
635
515
  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);
516
+ this.onDragPointerMove = this.onDragPointerMove.bind(this);
517
+ this.onDragPointerUp = this.onDragPointerUp.bind(this);
518
+ this.onDragPointerCancel = this.onDragPointerCancel.bind(this);
519
+ this.onSplitterResize = this.onSplitterResize.bind(this);
647
520
  }
648
521
  render() {
649
522
  return template;
@@ -688,56 +561,47 @@ class MintDockManagerElement extends LitElement {
688
561
  // Tag the docked surface with a root path so it can act as
689
562
  // a drop target when the main layout is empty.
690
563
  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
- });
564
+ // Drop targeting (drop indicator + joystick zone selection) runs entirely
565
+ // off pointer-based hit-testing in updatePaneDragDropTargetFromPoint and
566
+ // findDropZoneByPoint — no HTML5 dragover/drop/dragleave listeners needed.
711
567
  // Render any layout that was set before the shadow DOM existed.
712
568
  this.renderLayout();
569
+ // Reactive triggers for intersection-handle re-rendering. Each observer
570
+ // wakes scheduleRenderIntersectionHandles() (rAF-coalesced), which means
571
+ // multiple notifications in the same frame collapse to one render and
572
+ // the rAF tick gives <mp-splitter> elements time to populate their
573
+ // shadow roots before we query their dividers.
574
+ this.rootResizeObserver = new ResizeObserver(() => this.scheduleRenderIntersectionHandles());
575
+ this.rootResizeObserver.observe(this.rootEl);
576
+ this.dockedMutationObserver = new MutationObserver(() => this.scheduleRenderIntersectionHandles());
577
+ this.dockedMutationObserver.observe(this.dockedEl, { childList: true, subtree: true });
578
+ // mp-splitter dispatches bubbling 'resizing' / 'resize-end' on user drag;
579
+ // delegating on dockedEl catches every nested splitter without per-instance wiring.
580
+ this.dockedEl.addEventListener('resizing', this.onSplitterResize);
581
+ this.dockedEl.addEventListener('resize-end', this.onSplitterResize);
713
582
  }
714
583
  connectedCallback() {
715
584
  super.connectedCallback();
716
585
  if (!this.hasAttribute('role')) {
717
586
  this.setAttribute('role', 'application');
718
587
  }
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
588
  }
725
589
  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
590
  const win = this.windowRef;
733
- win?.removeEventListener('dragover', this.onGlobalDragOver);
734
- win?.removeEventListener('drag', this.onDrag);
735
- win?.removeEventListener('dragend', this.onGlobalDragEnd, true);
736
591
  this.stopDragPointerTracking();
737
592
  win?.removeEventListener('pointermove', this.onPointerMove);
738
593
  win?.removeEventListener('pointerup', this.onPointerUp);
739
594
  this.pointerTrackingActive = false;
740
- win?.removeEventListener('resize', this.onWindowResize);
595
+ this.rootResizeObserver?.disconnect();
596
+ this.rootResizeObserver = null;
597
+ this.dockedMutationObserver?.disconnect();
598
+ this.dockedMutationObserver = null;
599
+ this.dockedEl?.removeEventListener('resizing', this.onSplitterResize);
600
+ this.dockedEl?.removeEventListener('resize-end', this.onSplitterResize);
601
+ if (this.intersectionRaf !== null) {
602
+ this.windowRef?.clearTimeout(this.intersectionRaf);
603
+ this.intersectionRaf = null;
604
+ }
741
605
  super.disconnectedCallback();
742
606
  }
743
607
  attributeChangedCallback(name, _oldValue, newValue) {
@@ -761,11 +625,52 @@ class MintDockManagerElement extends LitElement {
761
625
  }
762
626
  set layout(value) {
763
627
  const snapshot = this.ensureSnapshot(value);
628
+ // While a drag/resize is in flight, the dock manager is the source of
629
+ // truth for layout state — its mid-drag mutations (e.g. floating bounds
630
+ // updated every mousemove, or a stack split during a pane-drag-to-floating
631
+ // conversion) race the host's two-way binding round-trip. The host re-
632
+ // feeds the layout we *just* dispatched via `dock-layout-changed`, but by
633
+ // the time the round-trip arrives the user has moved the cursor again, so
634
+ // the structural-equality guard below would let it through and clobber the
635
+ // in-progress state (e.g. snap a freshly-detached floating window back to
636
+ // the converted-at coordinates instead of letting it follow the cursor).
637
+ // Reject any external layout write during interaction; the host will sync
638
+ // back to the dock's final state when interaction ends and the dock fires
639
+ // a fresh dock-layout-changed event.
640
+ if (this.isInteracting())
641
+ return;
642
+ // Skip renderLayout when the incoming layout is structurally identical
643
+ // to the current state. After a divider drag the dock dispatches
644
+ // dock-layout-changed; an Angular host doing two-way binding will feed
645
+ // that snapshot right back through `[layout]` (and through the
646
+ // `[attr.layout]` round-trip). Without this guard, every drag-end
647
+ // tears down and rebuilds the whole splitter tree, giving a one-frame
648
+ // flash of `flex: 1 1 0` equal-share before the pin restores sizes.
649
+ const currentJson = JSON.stringify({
650
+ root: this.rootLayout,
651
+ floating: this.floatingLayouts,
652
+ titles: this.titles,
653
+ });
654
+ const newJson = JSON.stringify(snapshot);
655
+ if (currentJson === newJson)
656
+ return;
764
657
  this.rootLayout = this.cloneLayoutNode(snapshot.root);
765
658
  this.floatingLayouts = this.cloneFloatingArray(snapshot.floating);
766
659
  this.titles = snapshot.titles ? { ...snapshot.titles } : {};
767
660
  this.renderLayout();
768
661
  }
662
+ /**
663
+ * True while the user is actively interacting with the dock — pane drag,
664
+ * floating window drag, floating window resize, or intersection corner
665
+ * resize. The `set layout` setter consults this to refuse external
666
+ * round-trips that would overwrite in-progress drag state.
667
+ */
668
+ isInteracting() {
669
+ return !!(this.dragState ||
670
+ this.floatingDragState ||
671
+ this.floatingResizeState ||
672
+ this.cornerResizeState);
673
+ }
769
674
  get snapshot() {
770
675
  return this.layout;
771
676
  }
@@ -831,7 +736,10 @@ class MintDockManagerElement extends LitElement {
831
736
  this.dockedEl.appendChild(fragment);
832
737
  }
833
738
  this.renderFloatingPanes();
834
- this.scheduleRenderIntersectionHandles();
739
+ // Note: intersection handles are repositioned reactively via observers
740
+ // wired up in firstUpdated (rootResizeObserver, dockedMutationObserver,
741
+ // and delegated 'resizing' / 'resize-end' events). The MutationObserver
742
+ // on dockedEl fires when the renderNode subtree above is appended.
835
743
  }
836
744
  renderNode(node, path, floatingIndex) {
837
745
  if (node.kind === 'split') {
@@ -926,104 +834,88 @@ class MintDockManagerElement extends LitElement {
926
834
  this.floatingLayerEl.appendChild(wrapper);
927
835
  });
928
836
  }
929
- onWindowResize() {
930
- // Recompute intersection handles on window resize
837
+ onSplitterResize() {
838
+ // mp-splitter dispatches 'resizing' continuously during a divider drag
839
+ // and 'resize-end' when the user releases. Both keep the handle glued
840
+ // to the new intersection coordinate.
931
841
  this.scheduleRenderIntersectionHandles();
932
842
  }
933
843
  scheduleRenderIntersectionHandles() {
934
- this.intersectionRaf = null;
935
- this.renderIntersectionHandles();
844
+ if (this.intersectionRaf !== null)
845
+ return;
846
+ const win = this.windowRef;
847
+ if (!win)
848
+ return;
849
+ // Defer with setTimeout(5) instead of rAF so we run AFTER any
850
+ // flex-redistribution settles. Sequence we have to wait through:
851
+ // (1) DOM mutation (e.g. panel removed by drop)
852
+ // (2) microtasks: <mp-splitter>'s slotchange + size-pinning rAF
853
+ // (3) layout flush
854
+ // A bare rAF can fire before (2) resolves, so getBoundingClientRect on
855
+ // the dividers reads a transient flex-distributed position and the
856
+ // glyph lands ~tens of pixels off. 5ms is past the microtask queue and
857
+ // past splitter's pinning rAF in practice, so the divider rects we
858
+ // read are the settled, post-pin values.
859
+ this.intersectionRaf = win.setTimeout(() => {
860
+ this.intersectionRaf = null;
861
+ this.renderIntersectionHandles();
862
+ }, 5);
936
863
  }
937
864
  renderIntersectionHandles() {
938
865
  const layer = this.shadowRoot?.querySelector('.dock-intersections-layer, .dock-intersection-layer');
939
866
  if (!layer)
940
867
  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
868
  const rootRect = this.rootEl.getBoundingClientRect();
967
- // If a corner resize is active, only update that handle's position and avoid creating new ones
869
+ // Active corner-resize: keep st.handle alive (it owns pointer capture and
870
+ // the cornerResizeState references it). Update its position from current
871
+ // divider rects, drop every other handle by reference.
968
872
  if (this.cornerResizeState) {
969
873
  const st = this.cornerResizeState;
970
874
  const h0 = st.hs[0];
971
875
  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}"]`);
876
+ const hSplitter = this.findSplitterByPath(h0.path.segments);
877
+ const vSplitter = this.findSplitterByPath(v0.path.segments);
878
+ const hDiv = hSplitter ? this.getSplitterDividers(hSplitter)[h0.index] : null;
879
+ const vDiv = vSplitter ? this.getSplitterDividers(vSplitter)[v0.index] : null;
978
880
  if (hDiv && vDiv) {
979
881
  const hr = hDiv.getBoundingClientRect();
980
882
  const vr = vDiv.getBoundingClientRect();
981
883
  const x = vr.left + vr.width / 2 - rootRect.left;
982
884
  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
885
+ st.handle.style.left = `${x}px`;
886
+ st.handle.style.top = `${y}px`;
991
887
  Array.from(layer.querySelectorAll('.dock-intersection-handle')).forEach((el) => {
992
- if ((el.dataset['key'] ?? '') !== key) {
888
+ if (el !== st.handle)
993
889
  el.remove();
994
- }
995
890
  });
996
- // Normalize internal map as well
997
- this.intersectionHandles = new Map([[key, handle]]);
998
891
  }
999
892
  return;
1000
893
  }
1001
- const allDividers = Array.from(this.shadowRoot?.querySelectorAll('.dock-split__divider') ?? []);
894
+ // Idle path: full clear + rebuild. Cheaper to reason about than incremental
895
+ // diffing, and handles' positions are always derived from current divider
896
+ // rects so a layout change (drop, splitter restructure, flex redistribution)
897
+ // can never leave a stale glyph behind.
898
+ layer.replaceChildren();
1002
899
  const hDividers = [];
1003
900
  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
- }
901
+ const allSplitters = Array.from(this.shadowRoot?.querySelectorAll('.dock-split') ?? []);
902
+ allSplitters.forEach((splitter) => {
903
+ const direction = splitter.dataset['direction'];
904
+ const pathStr = splitter.dataset['path'] ?? '';
905
+ this.getSplitterDividers(splitter).forEach((el, index) => {
906
+ const info = { rect: el.getBoundingClientRect(), pathStr, index };
907
+ // direction='horizontal' means children flow left-to-right, so the
908
+ // divider bars between them are VERTICAL (and vice-versa).
909
+ if (direction === 'horizontal')
910
+ vDividers.push(info);
911
+ else if (direction === 'vertical')
912
+ hDividers.push(info);
913
+ });
1022
914
  });
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();
915
+ // Group intersections that round to the same on-screen pixel, so two
916
+ // sibling splitters whose dividers happen to overlap share one handle.
917
+ const tol = 24;
918
+ const groups = new Map();
1027
919
  hDividers.forEach((h) => {
1028
920
  const hCenterY = h.rect.top + h.rect.height / 2;
1029
921
  vDividers.forEach((v) => {
@@ -1034,53 +926,33 @@ class MintDockManagerElement extends LitElement {
1034
926
  return;
1035
927
  const x = vCenterX - rootRect.left;
1036
928
  const y = hCenterY - rootRect.top;
1037
- const key = `${h.pathStr}:${h.index}|${v.pathStr}:${v.index}`;
1038
929
  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`;
930
+ const group = groups.get(gk) ?? { x, y, pairs: [] };
931
+ group.pairs.push({ h: { pathStr: h.pathStr, index: h.index }, v: { pathStr: v.pathStr, index: v.index } });
932
+ groups.set(gk, group);
1064
933
  });
1065
934
  });
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);
935
+ groups.forEach((group, gk) => {
936
+ const handle = this.documentRef.createElement('div');
937
+ handle.classList.add('dock-intersection-handle', 'glyph');
938
+ handle.setAttribute('role', 'separator');
939
+ handle.setAttribute('aria-label', 'Resize split intersection');
940
+ const firstPair = group.pairs[0];
941
+ const key = `${firstPair.h.pathStr}:${firstPair.h.index}|${firstPair.v.pathStr}:${firstPair.v.index}`;
942
+ handle.dataset['key'] = key;
943
+ handle.dataset['pairs'] = JSON.stringify(group.pairs);
944
+ handle.style.left = `${group.x}px`;
945
+ handle.style.top = `${group.y}px`;
946
+ // beginCornerResize/onIntersectionDoubleClick read data-pairs to
947
+ // reconstruct the (h, v) pair list, so the (h, v) args we pass here
948
+ // are only used as a fallback when data-pairs is empty — safe to use
949
+ // the first pair's structure as the seed.
950
+ 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() };
951
+ 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() };
952
+ handle.addEventListener('pointerdown', (ev) => this.beginCornerResize(ev, seedH, seedV, handle));
953
+ handle.addEventListener('dblclick', (ev) => this.onIntersectionDoubleClick(ev, handle));
954
+ layer.appendChild(handle);
1071
955
  });
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
956
  }
1085
957
  beginCornerResize(event, h, v, handle) {
1086
958
  event.preventDefault();
@@ -1093,20 +965,22 @@ class MintDockManagerElement extends LitElement {
1093
965
  const path = this.parsePath(pathStr);
1094
966
  if (!path)
1095
967
  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)
968
+ const splitter = this.findSplitterByPath(path.segments);
969
+ if (!splitter)
1099
970
  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
- }
971
+ // Initial pixel sizes come from each panel-wrapper inside the splitter's
972
+ // shadow root. We capture them once on pointerdown and feed deltas to
973
+ // setPanelSizes() during the drag.
974
+ const panels = this.getSplitterPanels(splitter);
975
+ if (panels.length === 0)
976
+ return;
977
+ const dim = axis === 'h' ? 'height' : 'width';
978
+ const initial = panels.map((p) => p.getBoundingClientRect()[dim]);
979
+ const entry = { path, index, container: splitter, initialSizes: initial, before: initial[index], after: initial[index + 1] };
980
+ if (axis === 'h')
981
+ hs.push(entry);
982
+ else
983
+ vs.push(entry);
1110
984
  };
1111
985
  if (parsed.length > 0) {
1112
986
  parsed.forEach((p) => { ensureHV(p.h.pathStr, p.h.index, 'h'); ensureHV(p.v.pathStr, p.v.index, 'v'); });
@@ -1138,44 +1012,47 @@ class MintDockManagerElement extends LitElement {
1138
1012
  // Compute localized snap targets for this intersection
1139
1013
  try {
1140
1014
  const rootRect = this.rootEl.getBoundingClientRect();
1141
- // Use first pair to define the crossing lines
1015
+ // Use first pair to define the crossing lines. Resolve dividers via
1016
+ // each splitter's shadow root.
1142
1017
  let centerX = null;
1143
1018
  let centerY = null;
1144
- // Resolve one vertical bar (from vs) and one horizontal bar (from hs)
1145
1019
  if (vs.length > 0) {
1146
1020
  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;
1021
+ const vDiv = this.getSplitterDividers(vPair.container)[vPair.index];
1149
1022
  const vr = vDiv?.getBoundingClientRect();
1150
1023
  if (vr)
1151
1024
  centerX = vr.left + vr.width / 2;
1152
1025
  }
1153
1026
  if (hs.length > 0) {
1154
1027
  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;
1028
+ const hDiv = this.getSplitterDividers(hPair.container)[hPair.index];
1157
1029
  const hr = hDiv?.getBoundingClientRect();
1158
1030
  if (hr)
1159
1031
  centerY = hr.top + hr.height / 2;
1160
1032
  }
1161
1033
  const xTargets = [];
1162
1034
  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);
1035
+ // Iterate every splitter, then flat-map its shadow dividers — a
1036
+ // splitter's data-direction tells us whether its bars are vertical
1037
+ // (horizontal split) or horizontal (vertical split).
1038
+ const allSplitters = Array.from(this.shadowRoot?.querySelectorAll('.dock-split') ?? []);
1039
+ allSplitters.forEach((splitter) => {
1040
+ const direction = splitter.dataset['direction'] ?? undefined;
1041
+ this.getSplitterDividers(splitter).forEach((el) => {
1042
+ const r = el.getBoundingClientRect();
1043
+ if (direction === 'horizontal' && centerY != null) {
1044
+ // vertical bar → contributes X if it crosses centerY
1045
+ if (centerY >= r.top && centerY <= r.bottom) {
1046
+ xTargets.push(r.left + r.width / 2 - rootRect.left);
1047
+ }
1171
1048
  }
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);
1049
+ else if (direction === 'vertical' && centerX != null) {
1050
+ // horizontal bar contributes Y if it crosses centerX
1051
+ if (centerX >= r.left && centerX <= r.right) {
1052
+ yTargets.push(r.top + r.height / 2 - rootRect.top);
1053
+ }
1177
1054
  }
1178
- }
1055
+ });
1179
1056
  });
1180
1057
  this.cornerSnapXTargets = xTargets;
1181
1058
  this.cornerSnapYTargets = yTargets;
@@ -1236,49 +1113,37 @@ class MintDockManagerElement extends LitElement {
1236
1113
  if (bestDist <= tol)
1237
1114
  clientY = best;
1238
1115
  }
1239
- // Update all horizontal bars (vertical splits) with Y delta
1240
- state.hs.forEach((h) => {
1241
- const node = this.resolveSplitNode(h.path);
1116
+ // Apply the new pair sizes to one splitter's panel-wrappers via
1117
+ // mp-splitter's setPanelSizes(pixels) API. We persist the normalized
1118
+ // ratios on the layout node so renderSplit's initial sizing stays in sync.
1119
+ const applyPairSize = (entry, delta) => {
1120
+ const node = this.resolveSplitNode(entry.path);
1242
1121
  if (!node)
1243
1122
  return;
1244
- const deltaY = clientY - h.startY;
1245
1123
  const minSize = 48;
1246
- const pairTotal = h.beforeSize + h.afterSize;
1247
- let newBefore = Math.min(Math.max(h.beforeSize + deltaY, minSize), pairTotal - minSize);
1124
+ const pairTotal = entry.beforeSize + entry.afterSize;
1125
+ let newBefore = Math.min(Math.max(entry.beforeSize + delta, minSize), pairTotal - minSize);
1248
1126
  newBefore = snapValue(newBefore, pairTotal, event.shiftKey);
1249
1127
  const newAfter = pairTotal - newBefore;
1250
- const sizesPx = [...h.initialSizes];
1251
- sizesPx[h.index] = newBefore;
1252
- sizesPx[h.index + 1] = newAfter;
1128
+ const sizesPx = [...entry.initialSizes];
1129
+ sizesPx[entry.index] = newBefore;
1130
+ sizesPx[entry.index + 1] = newAfter;
1253
1131
  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
- });
1132
+ node.sizes = total > 0 ? sizesPx.map((s) => s / total) : [];
1133
+ entry.container
1134
+ .setPanelSizes?.(sizesPx);
1135
+ };
1136
+ // Update all horizontal bars (vertical splits) with Y delta, then all
1137
+ // vertical bars (horizontal splits) with X delta.
1138
+ state.hs.forEach((h) => applyPairSize(h, clientY - h.startY));
1139
+ state.vs.forEach((v) => applyPairSize(v, clientX - v.startX));
1281
1140
  this.dispatchLayoutChanged();
1141
+ // setPanelSizes() is programmatic and doesn't fire 'resizing' events, so
1142
+ // the delegated listener on dockedEl doesn't wake during a corner drag.
1143
+ // Schedule the handle repositioning ourselves; renderIntersectionHandles
1144
+ // has a fast-path for the active cornerResizeState that just updates
1145
+ // left/top from the new divider rects.
1146
+ this.scheduleRenderIntersectionHandles();
1282
1147
  }
1283
1148
  endCornerResize(pointerId) {
1284
1149
  const state = this.cornerResizeState;
@@ -1322,7 +1187,28 @@ class MintDockManagerElement extends LitElement {
1322
1187
  let hasStored = false;
1323
1188
  splitKeys.forEach((k) => { if (this.previousSplitSizes.has(k))
1324
1189
  hasStored = true; });
1325
- const applySizes = (pathStr, mutate) => {
1190
+ // Persist `node.sizes` (normalized) and push pixel sizes into the
1191
+ // matching <mp-splitter> via setPanelSizes(). The splitter's panel
1192
+ // wrappers live in its shadow DOM, so direct flex mutation is no
1193
+ // longer an option.
1194
+ const pushSizesToSplitter = (path, normalized) => {
1195
+ const splitter = this.findSplitterByPath(path.segments);
1196
+ if (!splitter)
1197
+ return;
1198
+ const direction = splitter.dataset['direction'] ?? 'horizontal';
1199
+ const containerSize = direction === 'horizontal'
1200
+ ? splitter.getBoundingClientRect().width
1201
+ : splitter.getBoundingClientRect().height;
1202
+ if (!Number.isFinite(containerSize) || containerSize <= 0)
1203
+ return;
1204
+ const totalWeight = normalized.reduce((s, w) => s + Math.max(w, 0), 0);
1205
+ if (totalWeight <= 0)
1206
+ return;
1207
+ const px = normalized.map((w) => (Math.max(w, 0) / totalWeight) * containerSize);
1208
+ splitter
1209
+ .setPanelSizes?.(px);
1210
+ };
1211
+ const applySizes = (pathStr, dividerIndex, mutate) => {
1326
1212
  const path = this.parsePath(pathStr);
1327
1213
  if (!path)
1328
1214
  return;
@@ -1330,35 +1216,20 @@ class MintDockManagerElement extends LitElement {
1330
1216
  if (!node)
1331
1217
  return;
1332
1218
  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);
1219
+ const newSizes = mutate([...sizes], dividerIndex);
1337
1220
  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
- }
1221
+ pushSizesToSplitter(path, newSizes);
1345
1222
  };
1346
1223
  if (hasStored) {
1347
1224
  // Restore stored sizes
1348
1225
  this.previousSplitSizes.forEach((sizes, pathStr) => {
1349
1226
  const path = this.parsePath(pathStr);
1350
1227
  const node = path ? this.resolveSplitNode(path) : null;
1351
- if (!node)
1228
+ if (!node || !path)
1352
1229
  return;
1353
1230
  const norm = this.normalizeSizesArray(sizes, node.children.length);
1354
1231
  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
- }
1232
+ pushSizesToSplitter(path, norm);
1362
1233
  });
1363
1234
  this.previousSplitSizes.clear();
1364
1235
  }
@@ -1376,7 +1247,7 @@ class MintDockManagerElement extends LitElement {
1376
1247
  }
1377
1248
  touched.add(key);
1378
1249
  });
1379
- applySizes(p.h.pathStr, (sizes, idx) => {
1250
+ const equalize = (sizes, idx) => {
1380
1251
  const total = (sizes[idx] ?? 0) + (sizes[idx + 1] ?? 0);
1381
1252
  if (total <= 0)
1382
1253
  return sizes;
@@ -1384,16 +1255,9 @@ class MintDockManagerElement extends LitElement {
1384
1255
  sizes[idx + 1] = total / 2;
1385
1256
  const sum = sizes.reduce((a, s) => a + s, 0);
1386
1257
  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
- });
1258
+ };
1259
+ applySizes(p.h.pathStr, p.h.index, equalize);
1260
+ applySizes(p.v.pathStr, p.v.index, equalize);
1397
1261
  });
1398
1262
  }
1399
1263
  this.dispatchLayoutChanged();
@@ -1481,11 +1345,13 @@ class MintDockManagerElement extends LitElement {
1481
1345
  }
1482
1346
  try {
1483
1347
  state.handle.releasePointerCapture(state.pointerId);
1484
- delete state.handle.dataset['resizing'];
1485
1348
  }
1486
1349
  catch (err) {
1487
1350
  /* no-op */
1488
1351
  }
1352
+ // Clear outside the try so a thrown releasePointerCapture (capture
1353
+ // already lost) doesn't strand the handle in its visual drag state.
1354
+ delete state.handle.dataset['resizing'];
1489
1355
  const dropHandled = state.dropTarget
1490
1356
  ? this.handleFloatingStackDrop(state.index, state.dropTarget.path, state.dropTarget.zone)
1491
1357
  : false;
@@ -1581,6 +1447,12 @@ class MintDockManagerElement extends LitElement {
1581
1447
  catch (err) {
1582
1448
  /* no-op */
1583
1449
  }
1450
+ // Clear `data-resizing` outside the try — releasePointerCapture can
1451
+ // throw if the capture was already lost (e.g., the pointer left the
1452
+ // window), and we still need to drop the resizing attribute or the
1453
+ // CSS rule `.dock-floating__resizer[data-resizing='true']` keeps the
1454
+ // border dark-blue forever.
1455
+ delete state.handle.dataset['resizing'];
1584
1456
  this.floatingResizeState = null;
1585
1457
  this.dispatchLayoutChanged();
1586
1458
  }
@@ -1629,7 +1501,6 @@ class MintDockManagerElement extends LitElement {
1629
1501
  }
1630
1502
  stopPointerTrackingIfIdle() {
1631
1503
  if (this.pointerTrackingActive &&
1632
- !this.resizeState &&
1633
1504
  !this.floatingDragState &&
1634
1505
  !this.floatingResizeState &&
1635
1506
  !this.cornerResizeState) {
@@ -1671,63 +1542,102 @@ class MintDockManagerElement extends LitElement {
1671
1542
  titleEl.textContent = this.getFloatingWindowTitle(floating);
1672
1543
  }
1673
1544
  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 : [];
1545
+ // Each DockSplitNode renders as <mp-splitter>. The dock keeps its `.dock-split`
1546
+ // class on the host so existing `closest('.dock-split')` queries continue to
1547
+ // resolve, and stamps `data-direction` / `data-path` for the tree-driven
1548
+ // intersection-handle math.
1549
+ const splitter = this.documentRef.createElement('mp-splitter');
1550
+ splitter.classList.add('dock-split');
1551
+ splitter.dataset['direction'] = node.direction;
1552
+ splitter.dataset['path'] = path.join('/');
1553
+ // mp-splitter uses 'horizontal' (left-right) and 'vertical' (top-bottom).
1554
+ // The dock's DockSplitNode.direction matches that vocabulary 1:1.
1555
+ splitter.setAttribute('orientation', node.direction);
1556
+ const splitPath = typeof floatingIndex === 'number'
1557
+ ? { type: 'floating', index: floatingIndex, segments: [...path] }
1558
+ : { type: 'docked', segments: [...path] };
1679
1559
  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);
1560
+ // mp-splitter accepts direct children — it wraps each in a panel-wrapper
1561
+ // inside its shadow DOM and projects via a named slot per index.
1562
+ splitter.appendChild(this.renderNode(child, [...path, index], floatingIndex));
1563
+ });
1564
+ // Apply persisted sizes from the layout tree once mp-splitter has built
1565
+ // its panel wrappers. mp-splitter's setPanelSizes interprets values as
1566
+ // pixel widths/heights; the dock's saved sizes are flex weights, so
1567
+ // convert using the splitter's measured cross-axis container size.
1568
+ const sizes = Array.isArray(node.sizes) ? node.sizes : [];
1569
+ if (sizes.length > 0) {
1570
+ requestAnimationFrame(() => {
1571
+ const totalWeight = sizes.reduce((s, w) => s + Math.max(w, 0), 0);
1572
+ if (totalWeight <= 0)
1573
+ return;
1574
+ const containerSize = node.direction === 'horizontal'
1575
+ ? splitter.getBoundingClientRect().width
1576
+ : splitter.getBoundingClientRect().height;
1577
+ if (!Number.isFinite(containerSize) || containerSize <= 0)
1578
+ return;
1579
+ const px = sizes.map((w) => (Math.max(w, 0) / totalWeight) * containerSize);
1580
+ splitter
1581
+ .setPanelSizes?.(px);
1582
+ });
1583
+ }
1584
+ // mp-splitter fires resize-end with pixel sizes after a divider drag.
1585
+ // Convert back to flex weights (sum to a stable total — keep current sum
1586
+ // so future renders interpret consistently) and persist to the layout tree.
1587
+ splitter.addEventListener('resize-end', (event) => {
1588
+ // resize-end bubbles, so a nested mp-splitter's drag end would also
1589
+ // reach this listener. Only react to events from THIS splitter, not
1590
+ // from a descendant — otherwise we'd apply the inner's sizes to the
1591
+ // outer's splitNode and mangle the outer's weights.
1592
+ if (event.target !== splitter)
1593
+ return;
1594
+ const detail = event.detail;
1595
+ if (!Array.isArray(detail?.sizes) || detail.sizes.length === 0)
1596
+ return;
1597
+ const splitNode = this.resolveSplitNode(splitPath);
1598
+ if (!splitNode)
1599
+ return;
1600
+ const previousTotal = (splitNode.sizes ?? []).reduce((s, w) => s + Math.max(w, 0), 0);
1601
+ const total = detail.sizes.reduce((s, v) => s + Math.max(v, 0), 0);
1602
+ const targetTotal = previousTotal > 0 ? previousTotal : detail.sizes.length;
1603
+ if (total > 0) {
1604
+ splitNode.sizes = detail.sizes.map((px) => (Math.max(px, 0) / total) * targetTotal);
1605
+ this.dispatchLayoutChanged();
1708
1606
  }
1709
1607
  });
1710
- return container;
1608
+ return splitter;
1711
1609
  }
1712
1610
  renderStack(node, path, floatingIndex) {
1713
- const stack = this.documentRef.createElement('div');
1611
+ // Dock stacks are rendered as <mp-tab-control>. The dock keeps `.dock-stack`
1612
+ // as a class on the host so existing `closest('.dock-stack')` queries
1613
+ // continue to resolve. The tab strip + body slot projection are owned by
1614
+ // mp-tab-control; the dock just provides the slotted header/content
1615
+ // elements and listens for tab-activate to drive layout-tree updates.
1616
+ const stack = this.documentRef.createElement('mp-tab-control');
1714
1617
  stack.classList.add('dock-stack');
1618
+ // Dock controls activation; tell mp-tab-control not to auto-pick.
1619
+ stack.setAttribute('select-first-tab', 'false');
1620
+ // `border="top"` gives us the strip-cutout line under the tabs (so the
1621
+ // active tab visually punches through into the body) without adding the
1622
+ // full Bootstrap frame, which would double up with the dock's own outer
1623
+ // chrome border on `.dock-stack` (and on `.dock-floating` for floating
1624
+ // panels).
1625
+ stack.setAttribute('border', 'top');
1715
1626
  const location = typeof floatingIndex === 'number'
1716
1627
  ? { type: 'floating', index: floatingIndex, segments: [...path] }
1717
1628
  : { type: 'docked', segments: [...path] };
1718
1629
  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
1630
  const panes = Array.from(new Set(node.panes));
1725
1631
  if (panes.length === 0) {
1632
+ const emptyHeader = this.documentRef.createElement('span');
1633
+ emptyHeader.setAttribute('slot', '__empty__-header');
1634
+ emptyHeader.textContent = '(empty)';
1726
1635
  const empty = this.documentRef.createElement('div');
1636
+ empty.setAttribute('slot', '__empty__-content');
1727
1637
  empty.classList.add('dock-stack__pane');
1728
1638
  empty.textContent = 'No panes configured';
1729
- content.appendChild(empty);
1730
- stack.append(header, content);
1639
+ stack.append(emptyHeader, empty);
1640
+ stack.setAttribute('active-tab', '__empty__');
1731
1641
  return stack;
1732
1642
  }
1733
1643
  const activePane = panes.includes(node.activePane ?? '')
@@ -1740,228 +1650,119 @@ class MintDockManagerElement extends LitElement {
1740
1650
  const paneSlug = paneSlugRaw.length > 0 ? paneSlugRaw : 'pane';
1741
1651
  const tabId = `${this.instanceId}-tab-${pathSlug}-${paneSlug}`;
1742
1652
  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);
1653
+ // Header span — projected via mp-tab-control's `${tabId}-header` slot
1654
+ // into the strip's button content. Carries the dock's drag handlers.
1655
+ const headerSpan = this.documentRef.createElement('span');
1656
+ headerSpan.setAttribute('slot', `${tabId}-header`);
1657
+ headerSpan.classList.add('dock-tab');
1658
+ headerSpan.dataset['pane'] = paneName;
1659
+ headerSpan.dataset['tabId'] = tabId;
1660
+ headerSpan.textContent = this.titles[paneName] ?? paneName;
1661
+ // Pointer-only drag (no HTML5 dnd). pointerdown captures metrics + arms
1662
+ // a threshold gesture; once the pointer moves >threshold pixels we
1663
+ // promote it to a real pane drag via beginPaneDrag. Using pointer
1664
+ // events sidesteps the entire class of HTML5 dnd quirks (cancellation
1665
+ // when source DOM is removed mid-drag, suppressed mousemove, bogus 0/0
1666
+ // coordinates in Firefox, browser-specific drag-image behavior).
1667
+ headerSpan.addEventListener('pointerdown', (event) => {
1668
+ this.captureTabDragMetrics(event, stack);
1669
+ this.armPaneDragGesture(event, this.clonePath(location), paneName, stack);
1759
1670
  event.stopPropagation();
1671
+ // Do NOT call event.preventDefault() here. On touch, that suppresses
1672
+ // the synthesized click on the parent .nav-link button — which is how
1673
+ // mp-tab-control fires `tab-activate`. `touch-action: none` on
1674
+ // `.dock-tab` already prevents the browser from arbitrating the
1675
+ // gesture for scroll, so preventDefault would be redundant + harmful.
1760
1676
  });
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);
1677
+ // Content wrapper — projected via mp-tab-control's `${tabId}-content`
1678
+ // slot only when this tab is active. Holds the dock manager's per-pane
1679
+ // <slot> for the consumer's content.
1780
1680
  const paneHost = this.documentRef.createElement('div');
1681
+ paneHost.setAttribute('slot', `${tabId}-content`);
1781
1682
  paneHost.classList.add('dock-stack__pane');
1782
1683
  paneHost.dataset['pane'] = paneName;
1684
+ paneHost.dataset['tabId'] = tabId;
1783
1685
  paneHost.id = panelId;
1784
- paneHost.setAttribute('role', 'tabpanel');
1785
- paneHost.setAttribute('aria-labelledby', tabId);
1786
- if (paneName !== activePane) {
1787
- paneHost.setAttribute('hidden', '');
1788
- }
1789
1686
  const slotEl = this.documentRef.createElement('slot');
1790
1687
  slotEl.name = paneName;
1791
1688
  paneHost.appendChild(slotEl);
1792
- content.appendChild(paneHost);
1689
+ stack.append(headerSpan, paneHost);
1690
+ if (paneName === activePane) {
1691
+ stack.setAttribute('active-tab', tabId);
1692
+ }
1793
1693
  });
1794
1694
  stack.dataset['activePane'] = activePane;
1795
- stack.append(header, content);
1695
+ // Drive activatePane from mp-tab-control's tab-activate event. We map the
1696
+ // tabId back to the original paneName via the header span's data-pane.
1697
+ stack.addEventListener('tab-activate', (event) => {
1698
+ const detail = event.detail;
1699
+ const headerSpan = stack.querySelector(`:scope > [data-tab-id="${detail.tabId}"]`);
1700
+ const paneName = headerSpan?.dataset['pane'];
1701
+ if (paneName) {
1702
+ this.activatePane(stack, paneName, this.clonePath(location));
1703
+ this.dispatchEvent(new CustomEvent('dock-pane-activated', {
1704
+ detail: { pane: paneName },
1705
+ bubbles: true,
1706
+ composed: true,
1707
+ }));
1708
+ }
1709
+ });
1796
1710
  return stack;
1797
1711
  }
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
- }
1712
+ /**
1713
+ * Returns the strip (`.tsc`) element inside an `<mp-tab-control>`'s shadow
1714
+ * DOM. Used by drag/drop logic that needs the strip's geometry instead of
1715
+ * the host element's bounds.
1716
+ */
1717
+ getStackStripEl(stack) {
1718
+ if (stack.tagName !== 'MP-TAB-CONTROL')
1719
+ return null;
1720
+ return stack.shadowRoot?.querySelector('.tsc') ?? null;
1721
+ }
1722
+ /**
1723
+ * Returns the rendered tab buttons inside an `<mp-tab-control>`'s shadow
1724
+ * strip the light-DOM `.dock-tab` spans the dock owns are projected into
1725
+ * these buttons via `<slot>`. Use these for geometry / position queries
1726
+ * (insert-index computation, drop-indicator placement). Use the light-DOM
1727
+ * `.dock-tab` spans for data queries (paneName, drag listeners).
1728
+ */
1729
+ getStackTabButtons(stack) {
1730
+ if (stack.tagName !== 'MP-TAB-CONTROL')
1731
+ return [];
1732
+ return Array.from(stack.shadowRoot?.querySelectorAll('button.nav-link') ?? []);
1733
+ }
1734
+ /**
1735
+ * Returns the dividers inside an `<mp-splitter>`'s shadow DOM, in DOM order.
1736
+ * mp-splitter renders one `.divider` between each pair of adjacent panels,
1737
+ * so for an N-child split, length N-1.
1738
+ */
1739
+ getSplitterDividers(splitter) {
1740
+ if (splitter.tagName !== 'MP-SPLITTER')
1741
+ return [];
1742
+ return Array.from(splitter.shadowRoot?.querySelectorAll('.divider') ?? []);
1743
+ }
1744
+ /**
1745
+ * Returns the panel wrappers inside an `<mp-splitter>`'s shadow DOM, in
1746
+ * DOM order. These are the elements mp-splitter sizes (via setPanelSizes)
1747
+ * during a divider drag — the dock reads their geometry for intersection
1748
+ * handle math and snap markers.
1749
+ */
1750
+ getSplitterPanels(splitter) {
1751
+ if (splitter.tagName !== 'MP-SPLITTER')
1752
+ return [];
1753
+ return Array.from(splitter.shadowRoot?.querySelectorAll('.panel-wrapper') ?? []);
1754
+ }
1755
+ /**
1756
+ * Locate the rendered `<mp-splitter>` element for a given DockPath
1757
+ * `segments` value (the split-tree path). Searches the dock's shadow.
1758
+ */
1759
+ findSplitterByPath(segments) {
1760
+ return (this.shadowRoot?.querySelector(`.dock-split[data-path="${segments.join('/')}"]`) ?? null);
1872
1761
  }
1873
1762
  onPointerMove(event) {
1874
1763
  if (this.cornerResizeState && event.pointerId === this.cornerResizeState.pointerId) {
1875
1764
  this.handleCornerResizeMove(event);
1876
1765
  }
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
1766
  if (this.floatingResizeState && event.pointerId === this.floatingResizeState.pointerId) {
1966
1767
  this.handleFloatingResizeMove(event);
1967
1768
  }
@@ -1973,15 +1774,6 @@ class MintDockManagerElement extends LitElement {
1973
1774
  if (this.cornerResizeState && event.pointerId === this.cornerResizeState.pointerId) {
1974
1775
  this.endCornerResize(event.pointerId);
1975
1776
  }
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
1777
  if (this.floatingDragState && event.pointerId === this.floatingDragState.pointerId) {
1986
1778
  this.endFloatingDrag(event.pointerId);
1987
1779
  }
@@ -2021,30 +1813,55 @@ class MintDockManagerElement extends LitElement {
2021
1813
  clearPendingTabDragMetrics() {
2022
1814
  this.pendingTabDragMetrics = null;
2023
1815
  }
1816
+ /**
1817
+ * Pointerdown handler arms a "may become a drag" gesture. Once the pointer
1818
+ * moves past `threshold` pixels we promote it to an actual pane drag via
1819
+ * {@link beginPaneDrag}; if the user releases first we just clear the
1820
+ * pending tab metrics. All listeners self-clean on resolve so the gesture
1821
+ * stays scoped to a single pointerdown.
1822
+ */
1823
+ armPaneDragGesture(startEvent, path, pane, stackEl) {
1824
+ if (startEvent.pointerType === 'mouse' && startEvent.button !== 0)
1825
+ return;
1826
+ const win = this.windowRef;
1827
+ if (!win)
1828
+ return;
1829
+ const startX = startEvent.clientX;
1830
+ const startY = startEvent.clientY;
1831
+ const pointerId = startEvent.pointerId;
1832
+ const threshold = 5;
1833
+ let resolved = false;
1834
+ const cleanup = () => {
1835
+ resolved = true;
1836
+ win.removeEventListener('pointermove', onMove, true);
1837
+ win.removeEventListener('pointerup', onRelease, true);
1838
+ win.removeEventListener('pointercancel', onRelease, true);
1839
+ };
1840
+ const onMove = (event) => {
1841
+ if (resolved || event.pointerId !== pointerId)
1842
+ return;
1843
+ const dx = event.clientX - startX;
1844
+ const dy = event.clientY - startY;
1845
+ if (Math.hypot(dx, dy) < threshold)
1846
+ return;
1847
+ cleanup();
1848
+ this.beginPaneDrag(event, path, pane, stackEl);
1849
+ };
1850
+ const onRelease = (event) => {
1851
+ if (resolved || event.pointerId !== pointerId)
1852
+ return;
1853
+ cleanup();
1854
+ this.clearPendingTabDragMetrics();
1855
+ };
1856
+ win.addEventListener('pointermove', onMove, true);
1857
+ win.addEventListener('pointerup', onRelease, true);
1858
+ win.addEventListener('pointercancel', onRelease, true);
1859
+ }
2024
1860
  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
1861
  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;
1862
+ // Capture header bounds for detecting when to convert to floating.
1863
+ // The strip lives inside the mp-tab-control's shadow as `.tsc`.
1864
+ const headerEl = stackEl ? this.getStackStripEl(stackEl) : null;
2048
1865
  const headerRect = headerEl ? headerEl.getBoundingClientRect() : null;
2049
1866
  const headerBounds = headerRect
2050
1867
  ? { left: headerRect.left, top: headerRect.top, right: headerRect.right, bottom: headerRect.bottom }
@@ -2061,36 +1878,26 @@ class MintDockManagerElement extends LitElement {
2061
1878
  sourceHeaderBounds: headerBounds,
2062
1879
  startClientX: metrics && Number.isFinite(metrics.startClientX)
2063
1880
  ? metrics.startClientX
2064
- : Number.isFinite(event.clientX)
2065
- ? event.clientX
2066
- : undefined,
1881
+ : event.clientX,
2067
1882
  startClientY: metrics && Number.isFinite(metrics.startClientY)
2068
1883
  ? metrics.startClientY
2069
- : Number.isFinite(event.clientY)
2070
- ? event.clientY
2071
- : undefined,
1884
+ : event.clientY,
2072
1885
  };
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);
1886
+ this.lastDragPointerPosition = {
1887
+ x: this.dragState.startClientX,
1888
+ y: this.dragState.startClientY,
1889
+ };
1890
+ // pointerOffsetX/Y from preparePaneDragSource is the offset within the
1891
+ // source stack rect captured at pointerdown by captureTabDragMetrics.
1892
+ // Don't overwrite with event.offsetX/Y here — the threshold-trigger
1893
+ // pointermove fired on window, so its offset is in window-local coords
1894
+ // (≈ clientX/Y) which would crash the conversion math to ~(0,0).
1895
+ this.updateDraggedFloatingPositionFromPoint(event.clientX, event.clientY);
2091
1896
  this.startDragPointerTracking();
2092
- event.dataTransfer.effectAllowed = 'move';
2093
- event.dataTransfer.setData('text/plain', pane);
1897
+ // Mark the source floating wrapper (if any) so its CSS rule kicks in and
1898
+ // pointer-events:none lets findStackAtPoint see through to the docked
1899
+ // stack underneath, enabling drop zones over the dock during the drag.
1900
+ this.markDraggedFloatingWrapper();
2094
1901
  // Preferred UX: if the dragged tab is the only one in its stack,
2095
1902
  // immediately convert to a floating window unless it is already the
2096
1903
  // only pane in a floating window (this case is handled by reuse logic).
@@ -2098,18 +1905,16 @@ class MintDockManagerElement extends LitElement {
2098
1905
  const loc = this.resolveStackLocation(this.dragState.sourcePath);
2099
1906
  if (loc && Array.isArray(loc.node.panes) && loc.node.panes.length === 1) {
2100
1907
  let shouldConvert = false;
2101
- if (loc.context === "docked") {
1908
+ if (loc.context === 'docked') {
2102
1909
  shouldConvert = true;
2103
1910
  }
2104
- else if (loc.context === "floating") {
1911
+ else if (loc.context === 'floating') {
2105
1912
  const floating = this.floatingLayouts[loc.index];
2106
1913
  const totalPanes = floating && floating.root ? this.countPanesInTree(floating.root) : 0;
2107
- shouldConvert = totalPanes > 1; // not the only pane in this floating window
1914
+ shouldConvert = totalPanes > 1;
2108
1915
  }
2109
1916
  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);
1917
+ this.convertPendingTabDragToFloating(event.clientX, event.clientY);
2113
1918
  }
2114
1919
  }
2115
1920
  }
@@ -2191,134 +1996,27 @@ class MintDockManagerElement extends LitElement {
2191
1996
  }
2192
1997
  endPaneDrag() {
2193
1998
  this.clearPendingDragEndTimeout();
1999
+ // Restore the dragged tab's `data-hidden` and remove the placeholder span
2000
+ // BEFORE we null out dragState — clearHeaderDragPlaceholder reads
2001
+ // `dragState.placeholderEl`, `dragState.placeholderHeader`, and
2002
+ // `dragState.pane` to know what to restore. If dragState is nulled first,
2003
+ // this becomes a silent no-op and the dragged pane stays hidden in its
2004
+ // source stack while the placeholder span lingers in the strip — which
2005
+ // is exactly the "Panel disappears, only a small tab-thumb remains"
2006
+ // regression the multi-pane drag-out path can otherwise trigger when
2007
+ // no renderLayout() runs between conversion and end (e.g. user releases
2008
+ // outside any drop zone, or HTML5 dragend fires without a drop).
2009
+ this.clearHeaderDragPlaceholder();
2010
+ this.clearDraggedFloatingWrapperMarkers();
2194
2011
  const state = this.dragState;
2195
2012
  this.dragState = null;
2196
2013
  this.hideDropIndicator();
2197
- this.clearHeaderDragPlaceholder();
2198
2014
  this.stopDragPointerTracking();
2199
2015
  this.lastDragPointerPosition = null;
2200
2016
  if (state && state.floatingIndex !== null && !state.dropHandled) {
2201
2017
  this.dispatchLayoutChanged();
2202
2018
  }
2203
2019
  }
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
2020
  updateDraggedFloatingPositionFromPoint(clientX, clientY) {
2323
2021
  if (!this.dragState) {
2324
2022
  return;
@@ -2326,10 +2024,6 @@ class MintDockManagerElement extends LitElement {
2326
2024
  if (!Number.isFinite(clientX) || !Number.isFinite(clientY)) {
2327
2025
  return;
2328
2026
  }
2329
- // Ignore obviously bogus coordinates sometimes seen during HTML5 drag
2330
- if (clientX === 0 && clientY === 0) {
2331
- return;
2332
- }
2333
2027
  // If still dragging a tab inside its header, only convert to floating once we leave the header.
2334
2028
  if (this.dragState.floatingIndex !== null && this.dragState.floatingIndex < 0) {
2335
2029
  const b = this.dragState.sourceHeaderBounds;
@@ -2400,16 +2094,15 @@ class MintDockManagerElement extends LitElement {
2400
2094
  const inHeaderByBounds = !!this.dragState.sourceHeaderBounds && this.isPointWithinBounds(this.dragState.sourceHeaderBounds, clientX, clientY);
2401
2095
  const inHeaderByHitTest = this.isPointerOverSourceHeader(clientX, clientY);
2402
2096
  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
- }
2097
+ // Ensure placeholder exists and move it as the pointer moves.
2098
+ // Placeholder management mutates the slotted children of the
2099
+ // mp-tab-control stack; the WC re-renders the strip on slotchange.
2100
+ this.ensureHeaderDragPlaceholder(stack, this.dragState.pane);
2101
+ const idx = this.computeHeaderInsertIndex(stack, clientX);
2102
+ if (this.dragState.liveReorderIndex !== idx) {
2103
+ this.updateHeaderDragPlaceholderPosition(stack, idx);
2104
+ // Keep model reordering until drop; only move the placeholder now
2105
+ this.dragState.liveReorderIndex = idx;
2413
2106
  }
2414
2107
  this.hideDropIndicator();
2415
2108
  return;
@@ -2421,81 +2114,125 @@ class MintDockManagerElement extends LitElement {
2421
2114
  const zone = this.computeDropZone(stack, { clientX, clientY }, zoneHint);
2422
2115
  this.showDropIndicator(stack, zone);
2423
2116
  }
2424
- // Returns true when the pointer is currently over the source stack's header (tab strip)
2117
+ // Returns true when the pointer is currently over the source stack's header (tab strip).
2118
+ // The strip lives inside the mp-tab-control's shadow as `.tsc`, so we test
2119
+ // bounds directly rather than using elementsFromPoint(/contains) which won't
2120
+ // pierce the shadow boundary cleanly.
2425
2121
  isPointerOverSourceHeader(clientX, clientY) {
2426
2122
  const state = this.dragState;
2427
2123
  if (!state) {
2428
2124
  return false;
2429
2125
  }
2430
2126
  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
2127
+ const strip = stackEl ? this.getStackStripEl(stackEl) : null;
2128
+ if (!strip) {
2129
+ // Be conservative: if we cannot resolve the strip, treat as inside
2434
2130
  return true;
2435
2131
  }
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;
2132
+ const r = strip.getBoundingClientRect();
2133
+ return clientX >= r.left && clientX <= r.right && clientY >= r.top && clientY <= r.bottom;
2444
2134
  }
2445
2135
  isPointWithinBounds(bounds, x, y) {
2446
2136
  return x >= bounds.left && x <= bounds.right && y >= bounds.top && y <= bounds.bottom;
2447
2137
  }
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);
2138
+ // Ensure a placeholder tab exists during in-header drag and hide the real dragged tab visually.
2139
+ // Operates on the mp-tab-control stack: the dragged content div gets `data-hidden`
2140
+ // (mp-tab-control then skips its tab in the strip), and a placeholder header+content
2141
+ // pair is appended as light-DOM children of the stack. mp-tab-control's mutation
2142
+ // observer picks up the change and renders the placeholder as a tab.
2143
+ ensureHeaderDragPlaceholder(stack, pane) {
2144
+ if (stack.tagName !== 'MP-TAB-CONTROL')
2145
+ return;
2146
+ if (this.dragState?.placeholderHeader === stack && this.dragState.placeholderEl) {
2147
+ return;
2148
+ }
2149
+ const draggedHeader = stack.querySelector(`:scope > .dock-tab[data-pane="${CSS.escape(pane)}"]`);
2150
+ const draggedContent = stack.querySelector(`:scope > .dock-stack__pane[data-pane="${CSS.escape(pane)}"]`);
2151
+ if (!draggedHeader || !draggedContent)
2152
+ return;
2153
+ // Measure the dragged tab's text-only width BEFORE hiding it. The
2154
+ // `.dock-tab` rule applies padding (matching the strip button's padding so
2155
+ // the span fills the button as a drag handle), so `offsetWidth` is
2156
+ // text + padding we subtract the span's own padding to get just the
2157
+ // text width. That's the natural slot content width we want the
2158
+ // placeholder to reserve; the placeholder span will re-apply the same
2159
+ // padding on top, mirroring the original tab's geometry exactly.
2160
+ const draggedCS = this.windowRef
2161
+ ? this.windowRef.getComputedStyle(draggedHeader)
2162
+ : globalThis.getComputedStyle(draggedHeader);
2163
+ const draggedHorizontalPadding = parseFloat(draggedCS.paddingLeft) + parseFloat(draggedCS.paddingRight);
2164
+ const slotContentWidth = Math.max(0, draggedHeader.offsetWidth - draggedHorizontalPadding);
2165
+ // Hide the dragged tab from mp-tab-control's strip (frees up the slot).
2166
+ draggedContent.setAttribute('data-hidden', '');
2167
+ // Build placeholder header + content. The placeholder uses a unique tabId
2168
+ // (`__dock-placeholder__`) so its slot names don't collide with real panes.
2169
+ // We mirror the dragged tab's text into the placeholder (dimmed via opacity)
2170
+ // so the strip reads as "this tab is being dragged" rather than "empty slot".
2171
+ const placeholderTabId = '__dock-placeholder__';
2172
+ const phHeader = this.documentRef.createElement('span');
2173
+ phHeader.setAttribute('slot', `${placeholderTabId}-header`);
2174
+ phHeader.classList.add('dock-tab');
2175
+ phHeader.dataset['placeholder'] = 'true';
2176
+ phHeader.dataset['tabId'] = placeholderTabId;
2177
+ phHeader.setAttribute('aria-hidden', 'true');
2178
+ phHeader.textContent = draggedHeader.textContent;
2179
+ // `display: inline-block` is required for `min-width` to take effect on the
2180
+ // span. Without it, an inline element ignores min-width and the placeholder
2181
+ // collapses to its content width (or 0 if textContent is also empty),
2182
+ // leaving a "mini-thumb" in the strip.
2183
+ phHeader.style.display = 'inline-block';
2184
+ phHeader.style.minWidth = `${slotContentWidth}px`;
2185
+ phHeader.style.opacity = '0.5';
2186
+ const phContent = this.documentRef.createElement('div');
2187
+ phContent.setAttribute('slot', `${placeholderTabId}-content`);
2188
+ phContent.classList.add('dock-stack__pane');
2189
+ phContent.dataset['placeholder'] = 'true';
2190
+ // Insert before the dragged header span so the placeholder appears in
2191
+ // the dragged tab's original strip position. The mutation observer in
2192
+ // mp-tab-control will refresh the tab list automatically.
2193
+ stack.insertBefore(phHeader, draggedHeader);
2194
+ stack.insertBefore(phContent, draggedContent);
2470
2195
  if (this.dragState) {
2471
- this.dragState.placeholderHeader = header;
2472
- this.dragState.placeholderEl = placeholder;
2196
+ this.dragState.placeholderHeader = stack;
2197
+ this.dragState.placeholderEl = phHeader;
2473
2198
  }
2474
2199
  }
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) {
2200
+ // Move the placeholder to the computed target index within the strip.
2201
+ // We reorder light-DOM children (header span + matching content div); the
2202
+ // mp-tab-control then re-renders the strip in the new order on slotchange.
2203
+ updateHeaderDragPlaceholderPosition(stack, targetIndex) {
2204
+ if (stack.tagName !== 'MP-TAB-CONTROL')
2205
+ return;
2206
+ const phHeader = this.dragState?.placeholderEl ?? null;
2207
+ if (!phHeader)
2479
2208
  return;
2480
- }
2481
2209
  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
2210
+ // Find all real header spans (excluding the placeholder + the hidden dragged one).
2211
+ const realHeaders = Array.from(stack.querySelectorAll(':scope > .dock-tab')).filter((h) => h !== phHeader &&
2212
+ (!draggedPane || h.dataset['pane'] !== draggedPane));
2213
+ const clampedTarget = Math.max(0, Math.min(targetIndex, realHeaders.length));
2214
+ const ref = realHeaders[clampedTarget] ?? null;
2215
+ stack.insertBefore(phHeader, ref);
2216
+ // Keep the placeholder content adjacent to its header so child-order
2217
+ // remains predictable for slotchange-driven re-renders.
2218
+ const phContent = stack.querySelector(`:scope > .dock-stack__pane[data-placeholder="true"]`);
2219
+ if (phContent && phHeader.nextElementSibling !== phContent) {
2220
+ stack.insertBefore(phContent, phHeader.nextElementSibling);
2221
+ }
2222
+ }
2223
+ // Remove placeholder and restore the dragged tab's visibility.
2489
2224
  clearHeaderDragPlaceholder() {
2490
2225
  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 = '';
2226
+ const stack = this.dragState?.placeholderHeader ?? null;
2227
+ if (stack) {
2228
+ // Restore the dragged content div's visibility so its strip tab returns.
2229
+ if (this.dragState?.pane) {
2230
+ const draggedContent = stack.querySelector(`:scope > .dock-stack__pane[data-pane="${CSS.escape(this.dragState.pane)}"]`);
2231
+ draggedContent?.removeAttribute('data-hidden');
2498
2232
  }
2233
+ // Remove the placeholder content div sibling.
2234
+ const phContent = stack.querySelector(`:scope > .dock-stack__pane[data-placeholder="true"]`);
2235
+ phContent?.remove();
2499
2236
  }
2500
2237
  if (ph && ph.parentElement) {
2501
2238
  ph.parentElement.removeChild(ph);
@@ -2511,11 +2248,9 @@ class MintDockManagerElement extends LitElement {
2511
2248
  }
2512
2249
  this.lastDragPointerPosition = null;
2513
2250
  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);
2251
+ win?.addEventListener('pointermove', this.onDragPointerMove, true);
2252
+ win?.addEventListener('pointerup', this.onDragPointerUp, true);
2253
+ win?.addEventListener('pointercancel', this.onDragPointerCancel, true);
2519
2254
  this.dragPointerTrackingActive = true;
2520
2255
  }
2521
2256
  stopDragPointerTracking() {
@@ -2523,52 +2258,38 @@ class MintDockManagerElement extends LitElement {
2523
2258
  return;
2524
2259
  }
2525
2260
  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);
2261
+ win?.removeEventListener('pointermove', this.onDragPointerMove, true);
2262
+ win?.removeEventListener('pointerup', this.onDragPointerUp, true);
2263
+ win?.removeEventListener('pointercancel', this.onDragPointerCancel, true);
2531
2264
  this.dragPointerTrackingActive = false;
2532
2265
  this.lastDragPointerPosition = null;
2533
2266
  this.clearPendingDragEndTimeout();
2534
2267
  }
2535
- onDragMouseMove(event) {
2268
+ onDragPointerMove(event) {
2536
2269
  if (!this.dragState) {
2537
2270
  this.stopDragPointerTracking();
2538
2271
  return;
2539
2272
  }
2540
- if (event.buttons === 0) {
2541
- this.scheduleDeferredDragEnd();
2542
- return;
2543
- }
2544
2273
  this.lastDragPointerPosition = { x: event.clientX, y: event.clientY };
2545
2274
  this.updateDraggedFloatingPositionFromPoint(event.clientX, event.clientY);
2546
2275
  }
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
2276
+ onDragPointerUp(event) {
2277
+ // Commit the drop from pointer release; the pointer-up coordinates are
2278
+ // authoritative for which stack/zone the user dropped into.
2564
2279
  if (this.dragState) {
2565
- const pos = this.lastDragPointerPosition;
2566
- if (pos) {
2567
- this.finalizeDropFromPoint(pos.x, pos.y);
2280
+ const x = Number.isFinite(event.clientX) ? event.clientX : this.lastDragPointerPosition?.x;
2281
+ const y = Number.isFinite(event.clientY) ? event.clientY : this.lastDragPointerPosition?.y;
2282
+ if (x !== undefined && y !== undefined) {
2283
+ this.finalizeDropFromPoint(x, y);
2568
2284
  }
2569
2285
  }
2570
2286
  this.handleDragPointerUpCommon();
2571
2287
  }
2288
+ onDragPointerCancel() {
2289
+ // OS-level cancel (e.g. pointer capture lost): end the drag without
2290
+ // committing a drop.
2291
+ this.handleDragPointerUpCommon();
2292
+ }
2572
2293
  // Convert a currently in-header tab drag into a floating window
2573
2294
  convertPendingTabDragToFloating(clientX, clientY) {
2574
2295
  if (!this.dragState) {
@@ -2601,22 +2322,34 @@ class MintDockManagerElement extends LitElement {
2601
2322
  : stackRect && Number.isFinite(stackRect.height)
2602
2323
  ? stackRect.height
2603
2324
  : 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;
2325
+ // Place the floating wrapper exactly where the docked stack was, so the
2326
+ // pane appears in-place at the moment of detach instead of jumping under
2327
+ // the cursor. metrics.left/top are host-relative (captured at pointerdown
2328
+ // in captureTabDragMetrics). Compensate for the floating wrapper's 1px
2329
+ // border so the visible content edge lines up with the original stack's
2330
+ // visible content edge (the docked .dock-stack also has a 1px border, so
2331
+ // the inner content rectangles match after this offset).
2332
+ const FLOATING_BORDER = 1;
2333
+ const initialLeft = metrics && Number.isFinite(metrics.left)
2334
+ ? metrics.left - FLOATING_BORDER
2335
+ : Number.isFinite(clientX)
2336
+ ? clientX - hostRect.left - width / 2
2337
+ : 0;
2338
+ const initialTop = metrics && Number.isFinite(metrics.top)
2339
+ ? metrics.top - FLOATING_BORDER
2340
+ : Number.isFinite(clientY)
2341
+ ? clientY - hostRect.top - height / 2
2342
+ : 0;
2343
+ // Derive pointerOffset from the cursor's actual position relative to the
2344
+ // freshly-placed wrapper (not from pointerdown metrics) so the very next
2345
+ // pointermove translates into a wrapper move of exactly the cursor delta
2346
+ // — no jump, no drift.
2347
+ const pointerOffsetX = Number.isFinite(clientX)
2348
+ ? clientX - hostRect.left - initialLeft
2349
+ : width / 2;
2350
+ const pointerOffsetY = Number.isFinite(clientY)
2351
+ ? clientY - hostRect.top - initialTop
2352
+ : height / 2;
2620
2353
  // Remove pane from its current stack and create a new floating entry
2621
2354
  this.removePaneFromLocation(location, pane);
2622
2355
  const floatingStack = {
@@ -2626,8 +2359,8 @@ class MintDockManagerElement extends LitElement {
2626
2359
  };
2627
2360
  const floatingLayout = {
2628
2361
  bounds: {
2629
- left: pointerLeft,
2630
- top: pointerTop,
2362
+ left: initialLeft,
2363
+ top: initialTop,
2631
2364
  width,
2632
2365
  height,
2633
2366
  },
@@ -2646,32 +2379,54 @@ class MintDockManagerElement extends LitElement {
2646
2379
  state.floatingIndex = newIndex;
2647
2380
  state.pointerOffsetX = pointerOffsetX;
2648
2381
  state.pointerOffsetY = pointerOffsetY;
2382
+ // Now that the wrapper exists, mark it so pointer-events:none kicks in
2383
+ // and findStackAtPoint can see through to docked stacks underneath.
2384
+ this.markDraggedFloatingWrapper();
2649
2385
  this.dispatchLayoutChanged();
2650
2386
  }
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) {
2387
+ // Toggle data-dragging on the floating wrapper currently associated with
2388
+ // the active pane drag (dragState.floatingIndex), if any. Used to make the
2389
+ // wrapper transparent to elementsFromPoint so drop zones can be shown on
2390
+ // stacks underneath. clearDraggedFloatingWrapper() is the inverse.
2391
+ markDraggedFloatingWrapper() {
2392
+ const fi = this.dragState?.floatingIndex;
2393
+ if (fi === null || fi === undefined || fi < 0)
2394
+ return;
2395
+ const wrapper = this.getFloatingWrapper(fi);
2396
+ if (wrapper)
2397
+ wrapper.dataset['dragging'] = 'true';
2398
+ }
2399
+ clearDraggedFloatingWrapperMarkers() {
2400
+ const layer = this.floatingLayerEl;
2401
+ if (!layer)
2402
+ return;
2403
+ layer.querySelectorAll('.dock-floating[data-dragging="true"]').forEach((el) => {
2404
+ delete el.dataset['dragging'];
2405
+ });
2406
+ }
2407
+ // Compute the intended tab insert index within a stack's strip based on pointer X.
2408
+ // Uses the rendered tab buttons inside mp-tab-control's shadow strip for geometry;
2409
+ // the dragged tab is hidden during drag (its content has data-hidden), and the
2410
+ // placeholder button (if present) gives us the dragged-position reference.
2411
+ computeHeaderInsertIndex(stack, clientX) {
2412
+ if (stack.tagName !== 'MP-TAB-CONTROL')
2413
+ return 0;
2414
+ const allTabButtons = this.getStackTabButtons(stack);
2415
+ if (allTabButtons.length === 0) {
2657
2416
  return 0;
2658
2417
  }
2659
- const draggedPane = this.dragState?.pane ?? null;
2660
- const draggedEl = draggedPane
2661
- ? (allTabs.find((t) => t.dataset['pane'] === draggedPane) ?? null)
2418
+ const placeholderHeader = stack.querySelector(':scope > .dock-tab[data-placeholder="true"]');
2419
+ const placeholderTabId = placeholderHeader?.dataset['tabId'];
2420
+ const placeholderButton = placeholderTabId
2421
+ ? allTabButtons.find((b) => b.id === `${placeholderTabId}-header-button`) ?? null
2662
2422
  : null;
2663
- const placeholderEl = header.querySelector('.dock-tab[data-placeholder="true"]');
2664
- const targets = allTabs.filter((t) => t !== draggedEl && t !== placeholderEl);
2423
+ const targets = allTabButtons.filter((b) => b !== placeholderButton);
2665
2424
  if (targets.length === 0) {
2666
2425
  return 0;
2667
2426
  }
2668
2427
  const rightBias = 12;
2669
2428
  const leftBias = 0;
2670
- const baseRect = placeholderEl
2671
- ? placeholderEl.getBoundingClientRect()
2672
- : draggedEl
2673
- ? draggedEl.getBoundingClientRect()
2674
- : null;
2429
+ const baseRect = placeholderButton ? placeholderButton.getBoundingClientRect() : null;
2675
2430
  const rectValid = !!baseRect && Number.isFinite(baseRect.width) && baseRect.width > 0;
2676
2431
  const draggedCenter = rectValid && baseRect ? baseRect.left + baseRect.width / 2 : null;
2677
2432
  for (let i = 0; i < targets.length; i += 1) {
@@ -2705,9 +2460,6 @@ class MintDockManagerElement extends LitElement {
2705
2460
  }
2706
2461
  }
2707
2462
  }
2708
- onDragTouchEnd() {
2709
- this.handleDragPointerUpCommon();
2710
- }
2711
2463
  // Commit a drop using current pointer coordinates and joystick state
2712
2464
  finalizeDropFromPoint(clientX, clientY) {
2713
2465
  if (!this.dragState) {
@@ -2733,17 +2485,14 @@ class MintDockManagerElement extends LitElement {
2733
2485
  stackPath &&
2734
2486
  this.pathsEqual(stackPath, this.dragState.sourcePath) &&
2735
2487
  (!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
- }
2488
+ const location = this.resolveStackLocation(path);
2489
+ if (location) {
2490
+ const idx = this.computeHeaderInsertIndex(stack, clientX);
2491
+ this.reorderPaneInLocationAtIndex(location, this.dragState.pane, idx);
2492
+ this.renderLayout();
2493
+ this.dispatchLayoutChanged();
2494
+ this.dragState.dropHandled = true;
2495
+ return;
2747
2496
  }
2748
2497
  }
2749
2498
  if (path && this.isDropZone(zone)) {
@@ -2782,112 +2531,6 @@ class MintDockManagerElement extends LitElement {
2782
2531
  ? win.setTimeout(completeDrag, 0)
2783
2532
  : setTimeout(completeDrag, 0);
2784
2533
  }
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
2534
  handleDrop(targetPath, zone) {
2892
2535
  if (!this.dragState || !targetPath) {
2893
2536
  return;
@@ -3354,24 +2997,6 @@ class MintDockManagerElement extends LitElement {
3354
2997
  }
3355
2998
  return null;
3356
2999
  }
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
3000
  findStackInTargets(targets) {
3376
3001
  for (const element of targets) {
3377
3002
  if (!(element instanceof HTMLElement)) {
@@ -3391,21 +3016,16 @@ class MintDockManagerElement extends LitElement {
3391
3016
  }
3392
3017
  activatePane(stack, paneName, path) {
3393
3018
  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', '');
3019
+ // Reflect to mp-tab-control's `active-tab` attribute. The WC handles
3020
+ // strip button styling (active class, aria-selected) + body-slot
3021
+ // projection automatically via the named-slot pattern.
3022
+ if (stack.tagName === 'MP-TAB-CONTROL') {
3023
+ const headerSpan = stack.querySelector(`:scope > .dock-tab[data-pane="${CSS.escape(paneName)}"]`);
3024
+ const tabId = headerSpan?.dataset['tabId'];
3025
+ if (tabId) {
3026
+ stack.setAttribute('active-tab', tabId);
3407
3027
  }
3408
- });
3028
+ }
3409
3029
  const location = this.resolveStackLocation(path);
3410
3030
  if (!location) {
3411
3031
  return;
@@ -3611,7 +3231,10 @@ class MintDockManagerElement extends LitElement {
3611
3231
  return { type: 'docked', segments: [...path.segments] };
3612
3232
  }
3613
3233
  parsePath(path) {
3614
- if (!path) {
3234
+ // The root splitter is tagged with data-path="" (raw segments-join of an
3235
+ // empty array) so empty string is a valid path representing root docked.
3236
+ // Only null/undefined is "no path".
3237
+ if (path == null) {
3615
3238
  return null;
3616
3239
  }
3617
3240
  if (path.startsWith('f:')) {
@@ -3845,12 +3468,12 @@ class BsDockManagerComponent {
3845
3468
  return this.cloneLayout(this._layout);
3846
3469
  }
3847
3470
  constructor() {
3848
- this.layout = input(null, ...(ngDevMode ? [{ debugName: "layout" }] : []));
3471
+ this.layout = input(null, ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
3849
3472
  this.layoutChange = output();
3850
3473
  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" }] : []));
3474
+ this.layoutString = signal(null, ...(ngDevMode ? [{ debugName: "layoutString" }] : /* istanbul ignore next */ []));
3475
+ this.panes = contentChildren(BsDockPaneComponent, ...(ngDevMode ? [{ debugName: "panes" }] : /* istanbul ignore next */ []));
3476
+ this.managerRef = viewChild('manager', ...(ngDevMode ? [{ debugName: "managerRef" }] : /* istanbul ignore next */ []));
3854
3477
  this.trackByPane = (_, pane) => pane.name();
3855
3478
  this._layout = { root: null, floating: [] };
3856
3479
  const documentRef = inject(DOCUMENT);
@@ -3913,10 +3536,10 @@ class BsDockManagerComponent {
3913
3536
  cloneLayout(layout) {
3914
3537
  return JSON.parse(JSON.stringify(layout));
3915
3538
  }
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 }); }
3539
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: BsDockManagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
3540
+ 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
3541
  }
3919
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: BsDockManagerComponent, decorators: [{
3542
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: BsDockManagerComponent, decorators: [{
3920
3543
  type: Component,
3921
3544
  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
3545
  }], 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 }] }] } });