@plastic-software/three 0.181.2 → 0.182.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/README.md +3 -4
  2. package/build/three.cjs +1192 -522
  3. package/build/three.core.js +345 -219
  4. package/build/three.core.min.js +1 -1
  5. package/build/three.module.js +864 -328
  6. package/build/three.module.min.js +1 -1
  7. package/build/three.tsl.js +15 -3
  8. package/build/three.tsl.min.js +1 -1
  9. package/build/three.webgpu.js +3660 -1545
  10. package/build/three.webgpu.min.js +1 -1
  11. package/build/three.webgpu.nodes.js +3659 -1544
  12. package/build/three.webgpu.nodes.min.js +1 -1
  13. package/examples/jsm/controls/MapControls.js +55 -1
  14. package/examples/jsm/controls/OrbitControls.js +6 -6
  15. package/examples/jsm/controls/TrackballControls.js +6 -6
  16. package/examples/jsm/csm/CSM.js +2 -1
  17. package/examples/jsm/environments/RoomEnvironment.js +2 -0
  18. package/examples/jsm/geometries/DecalGeometry.js +1 -1
  19. package/examples/jsm/helpers/LightProbeHelperGPU.js +1 -1
  20. package/examples/jsm/helpers/TextureHelperGPU.js +1 -1
  21. package/examples/jsm/inspector/Inspector.js +53 -9
  22. package/examples/jsm/inspector/RendererInspector.js +12 -2
  23. package/examples/jsm/inspector/tabs/Console.js +2 -2
  24. package/examples/jsm/inspector/tabs/Parameters.js +2 -2
  25. package/examples/jsm/inspector/tabs/Performance.js +2 -2
  26. package/examples/jsm/inspector/tabs/Viewer.js +4 -4
  27. package/examples/jsm/inspector/ui/Profiler.js +1836 -31
  28. package/examples/jsm/inspector/ui/Style.js +948 -13
  29. package/examples/jsm/inspector/ui/Tab.js +188 -1
  30. package/examples/jsm/inspector/ui/Values.js +17 -1
  31. package/examples/jsm/loaders/3DMLoader.js +5 -4
  32. package/examples/jsm/loaders/DRACOLoader.js +5 -5
  33. package/examples/jsm/loaders/FBXLoader.js +0 -2
  34. package/examples/jsm/loaders/HDRLoader.js +0 -1
  35. package/examples/jsm/loaders/KTX2Loader.js +16 -0
  36. package/examples/jsm/loaders/LDrawLoader.js +2 -3
  37. package/examples/jsm/loaders/PCDLoader.js +1 -0
  38. package/examples/jsm/loaders/SVGLoader.js +1 -1
  39. package/examples/jsm/loaders/TDSLoader.js +0 -2
  40. package/examples/jsm/loaders/TGALoader.js +0 -2
  41. package/examples/jsm/loaders/UltraHDRLoader.js +110 -137
  42. package/examples/jsm/loaders/VOXLoader.js +660 -117
  43. package/examples/jsm/loaders/VRMLLoader.js +2 -2
  44. package/examples/jsm/loaders/usd/USDCParser.js +1 -1
  45. package/examples/jsm/materials/LDrawConditionalLineNodeMaterial.js +1 -1
  46. package/examples/jsm/materials/MeshGouraudMaterial.js +0 -1
  47. package/examples/jsm/materials/WoodNodeMaterial.js +11 -11
  48. package/examples/jsm/math/Octree.js +131 -1
  49. package/examples/jsm/misc/Volume.js +0 -1
  50. package/examples/jsm/misc/VolumeSlice.js +0 -1
  51. package/examples/jsm/objects/SkyMesh.js +13 -3
  52. package/examples/jsm/physics/AmmoPhysics.js +12 -7
  53. package/examples/jsm/physics/JoltPhysics.js +3 -1
  54. package/examples/jsm/physics/RapierPhysics.js +3 -1
  55. package/examples/jsm/postprocessing/OutputPass.js +9 -0
  56. package/examples/jsm/postprocessing/RenderPass.js +10 -0
  57. package/examples/jsm/postprocessing/UnrealBloomPass.js +48 -18
  58. package/examples/jsm/renderers/Projector.js +268 -30
  59. package/examples/jsm/renderers/SVGRenderer.js +191 -58
  60. package/examples/jsm/shaders/UnpackDepthRGBAShader.js +2 -4
  61. package/examples/jsm/transpiler/AST.js +44 -0
  62. package/examples/jsm/transpiler/GLSLDecoder.js +61 -4
  63. package/examples/jsm/transpiler/ShaderToyDecoder.js +2 -0
  64. package/examples/jsm/transpiler/TSLEncoder.js +46 -3
  65. package/examples/jsm/transpiler/TranspilerUtils.js +3 -3
  66. package/examples/jsm/transpiler/WGSLEncoder.js +27 -0
  67. package/examples/jsm/tsl/display/AnaglyphPassNode.js +2 -0
  68. package/examples/jsm/tsl/display/BloomNode.js +11 -1
  69. package/examples/jsm/tsl/display/GTAONode.js +3 -2
  70. package/examples/jsm/tsl/display/PixelationPassNode.js +2 -1
  71. package/examples/jsm/tsl/display/SSGINode.js +7 -19
  72. package/examples/jsm/tsl/display/SSRNode.js +1 -1
  73. package/examples/jsm/tsl/display/SSSNode.js +4 -2
  74. package/examples/jsm/tsl/display/StereoCompositePassNode.js +8 -1
  75. package/examples/jsm/tsl/display/TRAANode.js +265 -114
  76. package/examples/jsm/tsl/display/radialBlur.js +68 -0
  77. package/examples/jsm/utils/ShadowMapViewer.js +24 -10
  78. package/examples/jsm/utils/ShadowMapViewerGPU.js +1 -1
  79. package/examples/jsm/utils/WebGPUTextureUtils.js +1 -1
  80. package/package.json +14 -12
  81. package/src/Three.Core.js +1 -0
  82. package/src/Three.TSL.js +14 -2
  83. package/src/animation/AnimationUtils.js +1 -12
  84. package/src/animation/KeyframeTrack.js +1 -1
  85. package/src/animation/tracks/BooleanKeyframeTrack.js +1 -1
  86. package/src/animation/tracks/ColorKeyframeTrack.js +1 -1
  87. package/src/animation/tracks/NumberKeyframeTrack.js +1 -1
  88. package/src/animation/tracks/QuaternionKeyframeTrack.js +1 -1
  89. package/src/animation/tracks/StringKeyframeTrack.js +1 -1
  90. package/src/animation/tracks/VectorKeyframeTrack.js +1 -1
  91. package/src/constants.js +61 -5
  92. package/src/core/BufferGeometry.js +14 -2
  93. package/src/core/Raycaster.js +2 -2
  94. package/src/extras/PMREMGenerator.js +3 -10
  95. package/src/extras/TextureUtils.js +5 -1
  96. package/src/geometries/ExtrudeGeometry.js +2 -2
  97. package/src/geometries/PolyhedronGeometry.js +1 -1
  98. package/src/helpers/PointLightHelper.js +1 -1
  99. package/src/lights/DirectionalLight.js +13 -0
  100. package/src/lights/HemisphereLight.js +10 -0
  101. package/src/lights/Light.js +1 -11
  102. package/src/lights/LightProbe.js +0 -15
  103. package/src/lights/LightShadow.js +0 -3
  104. package/src/lights/PointLight.js +15 -0
  105. package/src/lights/PointLightShadow.js +0 -86
  106. package/src/lights/SpotLight.js +22 -1
  107. package/src/loaders/MaterialLoader.js +2 -1
  108. package/src/loaders/ObjectLoader.js +3 -1
  109. package/src/loaders/nodes/NodeLoader.js +2 -2
  110. package/src/materials/Material.js +2 -0
  111. package/src/materials/ShaderMaterial.js +20 -1
  112. package/src/materials/nodes/Line2NodeMaterial.js +2 -2
  113. package/src/materials/nodes/MeshPhysicalNodeMaterial.js +3 -2
  114. package/src/materials/nodes/MeshStandardNodeMaterial.js +5 -4
  115. package/src/materials/nodes/NodeMaterial.js +59 -3
  116. package/src/materials/nodes/manager/NodeMaterialObserver.js +1 -1
  117. package/src/math/Matrix4.js +40 -40
  118. package/src/math/Sphere.js +1 -1
  119. package/src/math/Vector3.js +0 -2
  120. package/src/nodes/TSL.js +4 -1
  121. package/src/nodes/accessors/BatchNode.js +10 -10
  122. package/src/nodes/accessors/BufferAttributeNode.js +98 -12
  123. package/src/nodes/accessors/BufferNode.js +29 -2
  124. package/src/nodes/accessors/ClippingNode.js +4 -4
  125. package/src/nodes/accessors/CubeTextureNode.js +20 -1
  126. package/src/nodes/accessors/InstanceNode.js +69 -29
  127. package/src/nodes/accessors/MaterialNode.js +9 -1
  128. package/src/nodes/accessors/MaterialReferenceNode.js +1 -2
  129. package/src/nodes/accessors/ModelNode.js +1 -1
  130. package/src/nodes/accessors/Normal.js +2 -2
  131. package/src/nodes/accessors/ReferenceBaseNode.js +4 -4
  132. package/src/nodes/accessors/ReferenceNode.js +4 -4
  133. package/src/nodes/accessors/RendererReferenceNode.js +1 -2
  134. package/src/nodes/accessors/SkinningNode.js +15 -2
  135. package/src/nodes/accessors/StorageBufferNode.js +4 -2
  136. package/src/nodes/accessors/Tangent.js +1 -11
  137. package/src/nodes/accessors/Texture3DNode.js +26 -1
  138. package/src/nodes/accessors/UniformArrayNode.js +2 -2
  139. package/src/nodes/accessors/UserDataNode.js +1 -2
  140. package/src/nodes/accessors/VertexColorNode.js +1 -2
  141. package/src/nodes/code/FunctionNode.js +1 -2
  142. package/src/nodes/core/ArrayNode.js +20 -1
  143. package/src/nodes/core/AssignNode.js +2 -2
  144. package/src/nodes/core/AttributeNode.js +2 -2
  145. package/src/nodes/core/ContextNode.js +103 -4
  146. package/src/nodes/core/NodeBuilder.js +56 -14
  147. package/src/nodes/core/NodeFrame.js +12 -4
  148. package/src/nodes/core/NodeUtils.js +5 -5
  149. package/src/nodes/core/ParameterNode.js +1 -2
  150. package/src/nodes/core/PropertyNode.js +19 -3
  151. package/src/nodes/core/StackNode.js +56 -8
  152. package/src/nodes/core/StructNode.js +1 -2
  153. package/src/nodes/core/StructTypeNode.js +11 -17
  154. package/src/nodes/core/UniformNode.js +19 -4
  155. package/src/nodes/core/VarNode.js +46 -21
  156. package/src/nodes/display/NormalMapNode.js +37 -2
  157. package/src/nodes/display/PassNode.js +77 -7
  158. package/src/nodes/display/ScreenNode.js +1 -0
  159. package/src/nodes/functions/BSDF/BRDF_GGX_Multiscatter.js +3 -3
  160. package/src/nodes/functions/BSDF/DFGLUT.js +56 -0
  161. package/src/nodes/functions/BSDF/EnvironmentBRDF.js +2 -2
  162. package/src/nodes/functions/BSDF/V_GGX_SmithCorrelated_Anisotropic.js +1 -1
  163. package/src/nodes/functions/PhysicalLightingModel.js +102 -43
  164. package/src/nodes/gpgpu/ComputeBuiltinNode.js +1 -2
  165. package/src/nodes/gpgpu/SubgroupFunctionNode.js +1 -1
  166. package/src/nodes/gpgpu/WorkgroupInfoNode.js +2 -3
  167. package/src/nodes/lighting/AnalyticLightNode.js +53 -0
  168. package/src/nodes/lighting/LightsNode.js +2 -2
  169. package/src/nodes/lighting/PointShadowNode.js +141 -140
  170. package/src/nodes/lighting/ShadowFilterNode.js +53 -37
  171. package/src/nodes/lighting/ShadowNode.js +53 -19
  172. package/src/nodes/math/BitcountNode.js +433 -0
  173. package/src/nodes/math/PackFloatNode.js +98 -0
  174. package/src/nodes/math/UnpackFloatNode.js +96 -0
  175. package/src/nodes/pmrem/PMREMNode.js +1 -1
  176. package/src/nodes/tsl/TSLCore.js +4 -4
  177. package/src/nodes/utils/ArrayElementNode.js +13 -0
  178. package/src/nodes/utils/EventNode.js +1 -2
  179. package/src/nodes/utils/Packing.js +13 -1
  180. package/src/nodes/utils/PostProcessingUtils.js +33 -1
  181. package/src/nodes/utils/ReflectorNode.js +1 -1
  182. package/src/nodes/utils/SampleNode.js +1 -1
  183. package/src/nodes/utils/UVUtils.js +26 -0
  184. package/src/objects/BatchedMesh.js +5 -2
  185. package/src/objects/Line.js +1 -1
  186. package/src/objects/Mesh.js +1 -1
  187. package/src/objects/Points.js +1 -1
  188. package/src/objects/Skeleton.js +9 -0
  189. package/src/renderers/WebGLRenderer.js +145 -33
  190. package/src/renderers/common/Backend.js +8 -0
  191. package/src/renderers/common/Background.js +19 -9
  192. package/src/renderers/common/Binding.js +11 -0
  193. package/src/renderers/common/Bindings.js +7 -7
  194. package/src/renderers/common/Buffer.js +40 -0
  195. package/src/renderers/common/ChainMap.js +30 -6
  196. package/src/renderers/common/Geometries.js +12 -0
  197. package/src/renderers/common/RenderContexts.js +8 -1
  198. package/src/renderers/common/RenderObject.js +14 -1
  199. package/src/renderers/common/Renderer.js +53 -35
  200. package/src/renderers/common/Textures.js +1 -1
  201. package/src/renderers/common/UniformsGroup.js +1 -0
  202. package/src/renderers/common/XRManager.js +1 -0
  203. package/src/renderers/common/extras/PMREMGenerator.js +2 -8
  204. package/src/renderers/common/nodes/NodeUniformBuffer.js +52 -0
  205. package/src/renderers/shaders/DFGLUTData.js +19 -34
  206. package/src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js +5 -2
  207. package/src/renderers/shaders/ShaderChunk/lights_physical_fragment.glsl.js +8 -4
  208. package/src/renderers/shaders/ShaderChunk/lights_physical_pars_fragment.glsl.js +90 -51
  209. package/src/renderers/shaders/ShaderChunk/shadowmap_pars_fragment.glsl.js +194 -186
  210. package/src/renderers/shaders/ShaderChunk/shadowmask_pars_fragment.glsl.js +1 -1
  211. package/src/renderers/shaders/ShaderChunk/transmission_fragment.glsl.js +1 -1
  212. package/src/renderers/shaders/ShaderChunk.js +3 -3
  213. package/src/renderers/shaders/ShaderLib/depth.glsl.js +3 -0
  214. package/src/renderers/shaders/ShaderLib/{distanceRGBA.glsl.js → distance.glsl.js} +1 -2
  215. package/src/renderers/shaders/ShaderLib/meshlambert.glsl.js +0 -1
  216. package/src/renderers/shaders/ShaderLib/meshnormal.glsl.js +1 -2
  217. package/src/renderers/shaders/ShaderLib/meshphong.glsl.js +0 -1
  218. package/src/renderers/shaders/ShaderLib/meshphysical.glsl.js +4 -9
  219. package/src/renderers/shaders/ShaderLib/meshtoon.glsl.js +0 -1
  220. package/src/renderers/shaders/ShaderLib/shadow.glsl.js +0 -1
  221. package/src/renderers/shaders/ShaderLib/vsm.glsl.js +4 -6
  222. package/src/renderers/shaders/ShaderLib.js +3 -3
  223. package/src/renderers/webgl/WebGLCapabilities.js +3 -4
  224. package/src/renderers/webgl/WebGLLights.js +18 -1
  225. package/src/renderers/webgl/WebGLOutput.js +267 -0
  226. package/src/renderers/webgl/WebGLProgram.js +43 -107
  227. package/src/renderers/webgl/WebGLPrograms.js +35 -45
  228. package/src/renderers/webgl/WebGLShadowMap.js +188 -25
  229. package/src/renderers/webgl/WebGLState.js +20 -20
  230. package/src/renderers/webgl/WebGLTextures.js +89 -28
  231. package/src/renderers/webgl/WebGLUniforms.js +40 -3
  232. package/src/renderers/webgl/WebGLUtils.js +6 -2
  233. package/src/renderers/webgl-fallback/WebGLBackend.js +79 -13
  234. package/src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js +59 -7
  235. package/src/renderers/webgl-fallback/utils/WebGLState.js +18 -3
  236. package/src/renderers/webgl-fallback/utils/WebGLTextureUtils.js +5 -3
  237. package/src/renderers/webgl-fallback/utils/WebGLTimestampQueryPool.js +9 -9
  238. package/src/renderers/webgl-fallback/utils/WebGLUtils.js +6 -2
  239. package/src/renderers/webgpu/WebGPUBackend.js +61 -4
  240. package/src/renderers/webgpu/WebGPURenderer.js +1 -1
  241. package/src/renderers/webgpu/nodes/WGSLNodeBuilder.js +65 -23
  242. package/src/renderers/webgpu/utils/WebGPUAttributeUtils.js +4 -17
  243. package/src/renderers/webgpu/utils/WebGPUBindingUtils.js +354 -186
  244. package/src/renderers/webgpu/utils/WebGPUConstants.js +2 -0
  245. package/src/renderers/webgpu/utils/WebGPUPipelineUtils.js +20 -7
  246. package/src/renderers/webgpu/utils/WebGPUTextureUtils.js +40 -17
  247. package/src/renderers/webgpu/utils/WebGPUTimestampQueryPool.js +7 -7
  248. package/src/renderers/webgpu/utils/WebGPUUtils.js +7 -5
  249. package/src/textures/CubeDepthTexture.js +76 -0
  250. package/src/textures/Source.js +1 -1
  251. package/src/textures/Texture.js +1 -1
  252. package/src/utils.js +13 -1
  253. package/src/nodes/functions/BSDF/DFGApprox.js +0 -71
@@ -7,13 +7,176 @@ export class Profiler {
7
7
  this.tabs = {};
8
8
  this.activeTabId = null;
9
9
  this.isResizing = false;
10
- this.lastHeight = 350;
10
+ this.lastHeightBottom = 350; // Height for bottom position
11
+ this.lastWidthRight = 450; // Width for right position
12
+ this.position = 'bottom'; // 'bottom' or 'right'
13
+ this.detachedWindows = []; // Array to store detached tab windows
14
+ this.isMobile = this.detectMobile();
15
+ this.maxZIndex = 1002; // Track the highest z-index for detached windows (starts at base z-index from CSS)
16
+ this.nextTabOriginalIndex = 0; // Track the original order of tabs as they are added
11
17
 
12
18
  Style.init();
13
19
 
14
20
  this.setupShell();
15
21
  this.setupResizing();
16
22
 
23
+ // Setup orientation change listener for mobile devices
24
+ if ( this.isMobile ) {
25
+
26
+ this.setupOrientationListener();
27
+
28
+ }
29
+
30
+ // Setup window resize listener to constrain detached windows
31
+ this.setupWindowResizeListener();
32
+
33
+ }
34
+
35
+ detectMobile() {
36
+
37
+ // Check for mobile devices
38
+ const userAgent = navigator.userAgent || navigator.vendor || window.opera;
39
+ const isMobileUA = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test( userAgent );
40
+ const isTouchDevice = ( 'ontouchstart' in window ) || ( navigator.maxTouchPoints > 0 );
41
+ const isSmallScreen = window.innerWidth <= 768;
42
+
43
+ return isMobileUA || ( isTouchDevice && isSmallScreen );
44
+
45
+ }
46
+
47
+ setupOrientationListener() {
48
+
49
+ const handleOrientationChange = () => {
50
+
51
+ // Check if device is in landscape or portrait mode
52
+ const isLandscape = window.innerWidth > window.innerHeight;
53
+
54
+ // In landscape mode, use right position (vertical panel)
55
+ // In portrait mode, use bottom position (horizontal panel)
56
+ const targetPosition = isLandscape ? 'right' : 'bottom';
57
+
58
+ if ( this.position !== targetPosition ) {
59
+
60
+ this.setPosition( targetPosition );
61
+
62
+ }
63
+
64
+ };
65
+
66
+ // Initial check
67
+ handleOrientationChange();
68
+
69
+ // Listen for orientation changes
70
+ window.addEventListener( 'orientationchange', handleOrientationChange );
71
+ window.addEventListener( 'resize', handleOrientationChange );
72
+
73
+ }
74
+
75
+ setupWindowResizeListener() {
76
+
77
+ const constrainDetachedWindows = () => {
78
+
79
+ this.detachedWindows.forEach( detachedWindow => {
80
+
81
+ this.constrainWindowToBounds( detachedWindow.panel );
82
+
83
+ } );
84
+
85
+ };
86
+
87
+ const constrainMainPanel = () => {
88
+
89
+ // Skip if panel is maximized (it should always fill the screen)
90
+ if ( this.panel.classList.contains( 'maximized' ) ) return;
91
+
92
+ const windowWidth = window.innerWidth;
93
+ const windowHeight = window.innerHeight;
94
+
95
+ if ( this.position === 'bottom' ) {
96
+
97
+ const currentHeight = this.panel.offsetHeight;
98
+ const maxHeight = windowHeight - 50; // Leave 50px margin
99
+
100
+ if ( currentHeight > maxHeight ) {
101
+
102
+ this.panel.style.height = `${ maxHeight }px`;
103
+ this.lastHeightBottom = maxHeight;
104
+
105
+ }
106
+
107
+ } else if ( this.position === 'right' ) {
108
+
109
+ const currentWidth = this.panel.offsetWidth;
110
+ const maxWidth = windowWidth - 50; // Leave 50px margin
111
+
112
+ if ( currentWidth > maxWidth ) {
113
+
114
+ this.panel.style.width = `${ maxWidth }px`;
115
+ this.lastWidthRight = maxWidth;
116
+
117
+ }
118
+
119
+ }
120
+
121
+ };
122
+
123
+ // Listen for window resize events
124
+ window.addEventListener( 'resize', () => {
125
+
126
+ constrainDetachedWindows();
127
+ constrainMainPanel();
128
+
129
+ } );
130
+
131
+ }
132
+
133
+ constrainWindowToBounds( windowPanel ) {
134
+
135
+ const windowWidth = window.innerWidth;
136
+ const windowHeight = window.innerHeight;
137
+
138
+ const panelWidth = windowPanel.offsetWidth;
139
+ const panelHeight = windowPanel.offsetHeight;
140
+
141
+ let left = parseFloat( windowPanel.style.left ) || windowPanel.offsetLeft || 0;
142
+ let top = parseFloat( windowPanel.style.top ) || windowPanel.offsetTop || 0;
143
+
144
+ // Allow window to extend half its width/height outside the screen
145
+ const halfWidth = panelWidth / 2;
146
+ const halfHeight = panelHeight / 2;
147
+
148
+ // Constrain horizontal position (allow half width to extend beyond right edge)
149
+ if ( left + panelWidth > windowWidth + halfWidth ) {
150
+
151
+ left = windowWidth + halfWidth - panelWidth;
152
+
153
+ }
154
+
155
+ // Constrain horizontal position (allow half width to extend beyond left edge)
156
+ if ( left < - halfWidth ) {
157
+
158
+ left = - halfWidth;
159
+
160
+ }
161
+
162
+ // Constrain vertical position (allow half height to extend beyond bottom edge)
163
+ if ( top + panelHeight > windowHeight + halfHeight ) {
164
+
165
+ top = windowHeight + halfHeight - panelHeight;
166
+
167
+ }
168
+
169
+ // Constrain vertical position (allow half height to extend beyond top edge)
170
+ if ( top < - halfHeight ) {
171
+
172
+ top = - halfHeight;
173
+
174
+ }
175
+
176
+ // Apply constrained position
177
+ windowPanel.style.left = `${ left }px`;
178
+ windowPanel.style.top = `${ top }px`;
179
+
17
180
  }
18
181
 
19
182
  setupShell() {
@@ -24,17 +187,24 @@ export class Profiler {
24
187
  this.toggleButton = document.createElement( 'button' );
25
188
  this.toggleButton.id = 'profiler-toggle';
26
189
  this.toggleButton.innerHTML = `
190
+ <span id="builtin-tabs-container"></span>
27
191
  <span id="toggle-text">
28
192
  <span id="fps-counter">-</span>
29
193
  <span class="fps-label">FPS</span>
30
194
  </span>
31
- <!-- <span class="toggle-separator"></span> -->
32
195
  <span id="toggle-icon">
33
196
  <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-device-ipad-horizontal-search"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M11.5 20h-6.5a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v5.5" /><path d="M9 17h2" /><path d="M18 18m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0" /><path d="M20.2 20.2l1.8 1.8" /></svg>
34
197
  </span>
35
198
  `;
36
199
  this.toggleButton.onclick = () => this.togglePanel();
37
200
 
201
+ this.builtinTabsContainer = this.toggleButton.querySelector( '#builtin-tabs-container' );
202
+
203
+ // Create mini-panel for builtin tabs (shown when panel is hidden)
204
+ this.miniPanel = document.createElement( 'div' );
205
+ this.miniPanel.id = 'profiler-mini-panel';
206
+ this.miniPanel.className = 'profiler-mini-panel';
207
+
38
208
  this.panel = document.createElement( 'div' );
39
209
  this.panel.id = 'profiler-panel';
40
210
 
@@ -46,6 +216,20 @@ export class Profiler {
46
216
  const controls = document.createElement( 'div' );
47
217
  controls.className = 'profiler-controls';
48
218
 
219
+ this.floatingBtn = document.createElement( 'button' );
220
+ this.floatingBtn.id = 'floating-btn';
221
+ this.floatingBtn.title = 'Switch to Right Side';
222
+ this.floatingBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="15" y1="3" x2="15" y2="21"></line></svg>';
223
+ this.floatingBtn.onclick = () => this.togglePosition();
224
+
225
+ // Hide position toggle button on mobile devices
226
+ if ( this.isMobile ) {
227
+
228
+ this.floatingBtn.style.display = 'none';
229
+ this.panel.classList.add( 'hide-position-toggle' );
230
+
231
+ }
232
+
49
233
  this.maximizeBtn = document.createElement( 'button' );
50
234
  this.maximizeBtn.id = 'maximize-btn';
51
235
  this.maximizeBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>';
@@ -56,7 +240,7 @@ export class Profiler {
56
240
  hideBtn.textContent = '-';
57
241
  hideBtn.onclick = () => this.togglePanel();
58
242
 
59
- controls.append( this.maximizeBtn, hideBtn );
243
+ controls.append( this.floatingBtn, this.maximizeBtn, hideBtn );
60
244
  header.append( this.tabsContainer, controls );
61
245
 
62
246
  this.contentWrapper = document.createElement( 'div' );
@@ -67,7 +251,10 @@ export class Profiler {
67
251
 
68
252
  this.panel.append( resizer, header, this.contentWrapper );
69
253
 
70
- this.domElement.append( this.toggleButton, this.panel );
254
+ this.domElement.append( this.toggleButton, this.miniPanel, this.panel );
255
+
256
+ // Set initial position class
257
+ this.panel.classList.add( `position-${this.position}` );
71
258
 
72
259
  }
73
260
 
@@ -79,18 +266,40 @@ export class Profiler {
79
266
 
80
267
  this.isResizing = true;
81
268
  this.panel.classList.add( 'resizing' );
82
- const startY = e.clientY || e.touches[ 0 ].clientY;
269
+ resizer.setPointerCapture( e.pointerId );
270
+ const startX = e.clientX;
271
+ const startY = e.clientY;
83
272
  const startHeight = this.panel.offsetHeight;
273
+ const startWidth = this.panel.offsetWidth;
84
274
 
85
275
  const onMove = ( moveEvent ) => {
86
276
 
87
277
  if ( ! this.isResizing ) return;
88
278
  moveEvent.preventDefault();
89
- const currentY = moveEvent.clientY || moveEvent.touches[ 0 ].clientY;
90
- const newHeight = startHeight - ( currentY - startY );
91
- if ( newHeight > 100 && newHeight < window.innerHeight - 50 ) {
279
+ const currentX = moveEvent.clientX;
280
+ const currentY = moveEvent.clientY;
281
+
282
+ if ( this.position === 'bottom' ) {
283
+
284
+ // Vertical resize for bottom position
285
+ const newHeight = startHeight - ( currentY - startY );
286
+
287
+ if ( newHeight > 100 && newHeight < window.innerHeight - 50 ) {
92
288
 
93
- this.panel.style.height = `${newHeight}px`;
289
+ this.panel.style.height = `${ newHeight }px`;
290
+
291
+ }
292
+
293
+ } else if ( this.position === 'right' ) {
294
+
295
+ // Horizontal resize for right position
296
+ const newWidth = startWidth - ( currentX - startX );
297
+
298
+ if ( newWidth > 200 && newWidth < window.innerWidth - 50 ) {
299
+
300
+ this.panel.style.width = `${ newWidth }px`;
301
+
302
+ }
94
303
 
95
304
  }
96
305
 
@@ -100,27 +309,36 @@ export class Profiler {
100
309
 
101
310
  this.isResizing = false;
102
311
  this.panel.classList.remove( 'resizing' );
103
- document.removeEventListener( 'mousemove', onMove );
104
- document.removeEventListener( 'mouseup', onEnd );
105
- document.removeEventListener( 'touchmove', onMove );
106
- document.removeEventListener( 'touchend', onEnd );
312
+ resizer.removeEventListener( 'pointermove', onMove );
313
+ resizer.removeEventListener( 'pointerup', onEnd );
314
+ resizer.removeEventListener( 'pointercancel', onEnd );
107
315
  if ( ! this.panel.classList.contains( 'maximized' ) ) {
108
316
 
109
- this.lastHeight = this.panel.offsetHeight;
317
+ // Save dimensions based on current position
318
+ if ( this.position === 'bottom' ) {
319
+
320
+ this.lastHeightBottom = this.panel.offsetHeight;
321
+
322
+ } else if ( this.position === 'right' ) {
323
+
324
+ this.lastWidthRight = this.panel.offsetWidth;
325
+
326
+ }
327
+
328
+ // Save layout after resize
329
+ this.saveLayout();
110
330
 
111
331
  }
112
332
 
113
333
  };
114
334
 
115
- document.addEventListener( 'mousemove', onMove );
116
- document.addEventListener( 'mouseup', onEnd );
117
- document.addEventListener( 'touchmove', onMove, { passive: false } );
118
- document.addEventListener( 'touchend', onEnd );
335
+ resizer.addEventListener( 'pointermove', onMove );
336
+ resizer.addEventListener( 'pointerup', onEnd );
337
+ resizer.addEventListener( 'pointercancel', onEnd );
119
338
 
120
339
  };
121
340
 
122
- resizer.addEventListener( 'mousedown', onStart );
123
- resizer.addEventListener( 'touchstart', onStart );
341
+ resizer.addEventListener( 'pointerdown', onStart );
124
342
 
125
343
  }
126
344
 
@@ -129,14 +347,50 @@ export class Profiler {
129
347
  if ( this.panel.classList.contains( 'maximized' ) ) {
130
348
 
131
349
  this.panel.classList.remove( 'maximized' );
132
- this.panel.style.height = `${ this.lastHeight }px`;
350
+
351
+ // Restore size based on current position
352
+ if ( this.position === 'bottom' ) {
353
+
354
+ this.panel.style.height = `${ this.lastHeightBottom }px`;
355
+ this.panel.style.width = '100%';
356
+
357
+ } else if ( this.position === 'right' ) {
358
+
359
+ this.panel.style.height = '100%';
360
+ this.panel.style.width = `${ this.lastWidthRight }px`;
361
+
362
+ }
363
+
133
364
  this.maximizeBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>';
134
365
 
135
366
  } else {
136
367
 
137
- this.lastHeight = this.panel.offsetHeight;
368
+ // Save current size before maximizing
369
+ if ( this.position === 'bottom' ) {
370
+
371
+ this.lastHeightBottom = this.panel.offsetHeight;
372
+
373
+ } else if ( this.position === 'right' ) {
374
+
375
+ this.lastWidthRight = this.panel.offsetWidth;
376
+
377
+ }
378
+
138
379
  this.panel.classList.add( 'maximized' );
139
- this.panel.style.height = '100vh';
380
+
381
+ // Maximize based on current position
382
+ if ( this.position === 'bottom' ) {
383
+
384
+ this.panel.style.height = '100vh';
385
+ this.panel.style.width = '100%';
386
+
387
+ } else if ( this.position === 'right' ) {
388
+
389
+ this.panel.style.height = '100%';
390
+ this.panel.style.width = '100vw';
391
+
392
+ }
393
+
140
394
  this.maximizeBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="8" y="8" width="12" height="12" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg>';
141
395
 
142
396
  }
@@ -146,24 +400,1575 @@ export class Profiler {
146
400
  addTab( tab ) {
147
401
 
148
402
  this.tabs[ tab.id ] = tab;
149
- tab.button.onclick = () => this.setActiveTab( tab.id );
403
+
404
+ // Assign a permanent original index to this tab
405
+ tab.originalIndex = this.nextTabOriginalIndex ++;
406
+
407
+ // Add visual indicator for tabs that cannot be detached
408
+ if ( tab.allowDetach === false ) {
409
+
410
+ tab.button.classList.add( 'no-detach' );
411
+
412
+ }
413
+
414
+ // Set visibility change callback
415
+ tab.onVisibilityChange = () => this.updatePanelSize();
416
+
417
+ this.setupTabDragAndDrop( tab );
418
+
150
419
  this.tabsContainer.appendChild( tab.button );
151
420
  this.contentWrapper.appendChild( tab.content );
152
421
 
422
+ // Apply the current visibility state to the DOM elements
423
+ if ( ! tab.isVisible ) {
424
+
425
+ tab.button.style.display = 'none';
426
+ tab.content.style.display = 'none';
427
+
428
+ }
429
+
430
+ // If tab is builtin, add it to the profiler-toggle button
431
+ if ( tab.builtin ) {
432
+
433
+ this.addBuiltinTab( tab );
434
+
435
+ }
436
+
437
+ // Update panel size when tabs change
438
+ this.updatePanelSize();
439
+
153
440
  }
154
441
 
155
- setActiveTab( id ) {
442
+ addBuiltinTab( tab ) {
156
443
 
157
- if ( this.activeTabId ) this.tabs[ this.activeTabId ].setActive( false );
158
- this.activeTabId = id;
159
- this.tabs[ id ].setActive( true );
444
+ // Create a button for the builtin tab in the profiler-toggle
445
+ const builtinButton = document.createElement( 'button' );
446
+ builtinButton.className = 'builtin-tab-btn';
447
+
448
+ // Use icon if provided, otherwise use first letter
449
+ if ( tab.icon ) {
450
+
451
+ builtinButton.innerHTML = tab.icon;
452
+
453
+ } else {
454
+
455
+ builtinButton.textContent = tab.button.textContent.charAt( 0 ).toUpperCase();
456
+
457
+ }
458
+
459
+ builtinButton.title = tab.button.textContent;
460
+
461
+ // Create mini-panel content container for this tab
462
+ const miniContent = document.createElement( 'div' );
463
+ miniContent.className = 'mini-panel-content';
464
+ miniContent.style.display = 'none';
465
+
466
+ // Store references in the tab object
467
+ tab.builtinButton = builtinButton;
468
+ tab.miniContent = miniContent;
469
+
470
+ this.miniPanel.appendChild( miniContent );
471
+
472
+ builtinButton.onclick = ( e ) => {
473
+
474
+ e.stopPropagation(); // Prevent toggle panel from triggering
475
+
476
+ const isPanelVisible = this.panel.classList.contains( 'visible' );
477
+
478
+ if ( isPanelVisible ) {
479
+
480
+ // Panel is visible - navigate to tab
481
+ if ( ! tab.isVisible ) {
482
+
483
+ tab.show();
484
+
485
+ }
486
+
487
+ if ( tab.isDetached ) {
488
+
489
+ // If tab is detached, just bring its window to front
490
+ if ( tab.detachedWindow ) {
491
+
492
+ this.bringWindowToFront( tab.detachedWindow.panel );
493
+
494
+ }
495
+
496
+ } else {
497
+
498
+ // Activate the tab
499
+ this.setActiveTab( tab.id );
500
+
501
+ }
502
+
503
+ } else {
504
+
505
+ // Panel is hidden - toggle mini-panel for this tab
506
+ const isCurrentlyActive = miniContent.style.display !== 'none' && miniContent.children.length > 0;
507
+
508
+ // Hide all other mini-panel contents
509
+ this.miniPanel.querySelectorAll( '.mini-panel-content' ).forEach( content => {
510
+
511
+ content.style.display = 'none';
512
+
513
+ } );
514
+
515
+ // Remove active state from all builtin buttons
516
+ this.builtinTabsContainer.querySelectorAll( '.builtin-tab-btn' ).forEach( btn => {
517
+
518
+ btn.classList.remove( 'active' );
519
+
520
+ } );
521
+
522
+ if ( isCurrentlyActive ) {
523
+
524
+ // Toggle off - hide mini-panel and move content back
525
+ this.miniPanel.classList.remove( 'visible' );
526
+ miniContent.style.display = 'none';
527
+
528
+ // Move content back to main panel
529
+ if ( miniContent.firstChild ) {
530
+
531
+ tab.content.appendChild( miniContent.firstChild );
532
+
533
+ }
534
+
535
+ } else {
536
+
537
+ // Toggle on - show mini-panel with this tab's content
538
+ builtinButton.classList.add( 'active' );
539
+
540
+ // Move actual content to mini-panel (not clone) if not already there
541
+ if ( ! miniContent.firstChild ) {
542
+
543
+ const actualContent = tab.content.querySelector( '.list-scroll-wrapper' ) || tab.content.firstElementChild;
544
+
545
+ if ( actualContent ) {
546
+
547
+ miniContent.appendChild( actualContent );
548
+
549
+ }
550
+
551
+ }
552
+
553
+ // Show after content is moved
554
+ miniContent.style.display = 'block';
555
+ this.miniPanel.classList.add( 'visible' );
556
+
557
+ }
558
+
559
+ }
560
+
561
+ };
562
+
563
+ this.builtinTabsContainer.appendChild( builtinButton );
564
+
565
+ // Store references
566
+ tab.builtinButton = builtinButton;
567
+ tab.miniContent = miniContent;
568
+ tab.profiler = this;
569
+
570
+ // If the tab was hidden before being added, hide the builtin button
571
+ if ( ! tab.isVisible ) {
572
+
573
+ builtinButton.style.display = 'none';
574
+ miniContent.style.display = 'none';
575
+
576
+ // Hide the builtin-tabs-container if all builtin buttons are hidden
577
+ const hasVisibleBuiltinButtons = Array.from( this.builtinTabsContainer.querySelectorAll( '.builtin-tab-btn' ) )
578
+ .some( btn => btn.style.display !== 'none' );
579
+
580
+ if ( ! hasVisibleBuiltinButtons ) {
581
+
582
+ this.builtinTabsContainer.style.display = 'none';
583
+
584
+ }
585
+
586
+ }
160
587
 
161
588
  }
162
589
 
163
- togglePanel() {
590
+ updatePanelSize() {
164
591
 
165
- this.panel.classList.toggle( 'visible' );
166
- this.toggleButton.classList.toggle( 'hidden' );
592
+ // Check if there are any visible tabs in the panel
593
+ const hasVisibleTabs = Object.values( this.tabs ).some( tab => ! tab.isDetached && tab.isVisible );
594
+
595
+ // Add or remove CSS class to indicate no tabs state
596
+ if ( ! hasVisibleTabs ) {
597
+
598
+ this.panel.classList.add( 'no-tabs' );
599
+
600
+ // If maximized and no tabs, restore to normal size
601
+ if ( this.panel.classList.contains( 'maximized' ) ) {
602
+
603
+ this.panel.classList.remove( 'maximized' );
604
+ this.maximizeBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>';
605
+
606
+ }
607
+
608
+ // No tabs visible - set to minimum size
609
+ if ( this.position === 'bottom' ) {
610
+
611
+ this.panel.style.height = '38px';
612
+
613
+ } else if ( this.position === 'right' ) {
614
+
615
+ // 45px = width of one button column
616
+ this.panel.style.width = '45px';
617
+
618
+ }
619
+
620
+ } else {
621
+
622
+ this.panel.classList.remove( 'no-tabs' );
623
+
624
+ if ( Object.keys( this.tabs ).length > 0 ) {
625
+
626
+ // Has tabs - restore to saved size only if we had set it to minimum before
627
+ if ( this.position === 'bottom' ) {
628
+
629
+ const currentHeight = parseInt( this.panel.style.height );
630
+ if ( currentHeight === 38 ) {
631
+
632
+ this.panel.style.height = `${ this.lastHeightBottom }px`;
633
+
634
+ }
635
+
636
+ } else if ( this.position === 'right' ) {
637
+
638
+ const currentWidth = parseInt( this.panel.style.width );
639
+ if ( currentWidth === 45 ) {
640
+
641
+ this.panel.style.width = `${ this.lastWidthRight }px`;
642
+
643
+ }
644
+
645
+ }
646
+
647
+ }
648
+
649
+ }
650
+
651
+ }
652
+
653
+ setupTabDragAndDrop( tab ) {
654
+
655
+ // Disable drag and drop on mobile devices
656
+ if ( this.isMobile ) {
657
+
658
+ tab.button.addEventListener( 'click', () => {
659
+
660
+ this.setActiveTab( tab.id );
661
+
662
+ } );
663
+
664
+ return;
665
+
666
+ }
667
+
668
+ // Disable drag and drop if tab doesn't allow detach
669
+ if ( tab.allowDetach === false ) {
670
+
671
+ tab.button.addEventListener( 'click', () => {
672
+
673
+ this.setActiveTab( tab.id );
674
+
675
+ } );
676
+
677
+ tab.button.style.cursor = 'default';
678
+
679
+ return;
680
+
681
+ }
682
+
683
+ let isDragging = false;
684
+ let startX, startY;
685
+ let hasMoved = false;
686
+ let previewWindow = null;
687
+ const dragThreshold = 10; // pixels to move before starting drag
688
+
689
+ const onDragStart = ( e ) => {
690
+
691
+ startX = e.clientX;
692
+ startY = e.clientY;
693
+ isDragging = false;
694
+ hasMoved = false;
695
+ tab.button.setPointerCapture( e.pointerId );
696
+
697
+ };
698
+
699
+ const onDragMove = ( e ) => {
700
+
701
+ const currentX = e.clientX;
702
+ const currentY = e.clientY;
703
+
704
+ const deltaX = Math.abs( currentX - startX );
705
+ const deltaY = Math.abs( currentY - startY );
706
+
707
+ if ( ! isDragging && ( deltaX > dragThreshold || deltaY > dragThreshold ) ) {
708
+
709
+ isDragging = true;
710
+ tab.button.style.cursor = 'grabbing';
711
+ tab.button.style.opacity = '0.5';
712
+ tab.button.style.transform = 'scale(1.05)';
713
+
714
+ previewWindow = this.createPreviewWindow( tab, currentX, currentY );
715
+ previewWindow.style.opacity = '0.8';
716
+
717
+ }
718
+
719
+ if ( isDragging && previewWindow ) {
720
+
721
+ hasMoved = true;
722
+ e.preventDefault();
723
+
724
+ previewWindow.style.left = `${ currentX - 200 }px`;
725
+ previewWindow.style.top = `${ currentY - 20 }px`;
726
+
727
+ }
728
+
729
+ };
730
+
731
+ const onDragEnd = () => {
732
+
733
+ if ( isDragging && hasMoved && previewWindow ) {
734
+
735
+ if ( previewWindow.parentNode ) {
736
+
737
+ previewWindow.parentNode.removeChild( previewWindow );
738
+
739
+ }
740
+
741
+ const finalX = parseInt( previewWindow.style.left ) + 200;
742
+ const finalY = parseInt( previewWindow.style.top ) + 20;
743
+
744
+ this.detachTab( tab, finalX, finalY );
745
+
746
+ } else if ( ! hasMoved ) {
747
+
748
+ this.setActiveTab( tab.id );
749
+
750
+ if ( previewWindow && previewWindow.parentNode ) {
751
+
752
+ previewWindow.parentNode.removeChild( previewWindow );
753
+
754
+ }
755
+
756
+ } else if ( previewWindow ) {
757
+
758
+ if ( previewWindow.parentNode ) {
759
+
760
+ previewWindow.parentNode.removeChild( previewWindow );
761
+
762
+ }
763
+
764
+ }
765
+
766
+ tab.button.style.opacity = '';
767
+ tab.button.style.transform = '';
768
+ tab.button.style.cursor = '';
769
+ isDragging = false;
770
+ hasMoved = false;
771
+ previewWindow = null;
772
+
773
+ tab.button.removeEventListener( 'pointermove', onDragMove );
774
+ tab.button.removeEventListener( 'pointerup', onDragEnd );
775
+ tab.button.removeEventListener( 'pointercancel', onDragEnd );
776
+
777
+ };
778
+
779
+ tab.button.addEventListener( 'pointerdown', ( e ) => {
780
+
781
+ onDragStart( e );
782
+ tab.button.addEventListener( 'pointermove', onDragMove );
783
+ tab.button.addEventListener( 'pointerup', onDragEnd );
784
+ tab.button.addEventListener( 'pointercancel', onDragEnd );
785
+
786
+ } );
787
+
788
+ // Set cursor to grab for tabs that can be detached
789
+ tab.button.style.cursor = 'grab';
790
+
791
+ }
792
+
793
+ createPreviewWindow( tab, x, y ) {
794
+
795
+ const windowPanel = document.createElement( 'div' );
796
+ windowPanel.className = 'detached-tab-panel';
797
+ windowPanel.style.left = `${ x - 200 }px`;
798
+ windowPanel.style.top = `${ y - 20 }px`;
799
+ windowPanel.style.pointerEvents = 'none'; // Preview only
800
+
801
+ // Set z-index for preview window to be on top
802
+ this.maxZIndex ++;
803
+ windowPanel.style.setProperty( 'z-index', this.maxZIndex, 'important' );
804
+
805
+ const windowHeader = document.createElement( 'div' );
806
+ windowHeader.className = 'detached-tab-header';
807
+
808
+ const title = document.createElement( 'span' );
809
+ title.textContent = tab.button.textContent.replace( '⇱', '' ).trim();
810
+ windowHeader.appendChild( title );
811
+
812
+ const headerControls = document.createElement( 'div' );
813
+ headerControls.className = 'detached-header-controls';
814
+
815
+ const reattachBtn = document.createElement( 'button' );
816
+ reattachBtn.className = 'detached-reattach-btn';
817
+ reattachBtn.innerHTML = '↩';
818
+ headerControls.appendChild( reattachBtn );
819
+ windowHeader.appendChild( headerControls );
820
+
821
+ const windowContent = document.createElement( 'div' );
822
+ windowContent.className = 'detached-tab-content';
823
+
824
+ const resizer = document.createElement( 'div' );
825
+ resizer.className = 'detached-tab-resizer';
826
+
827
+ windowPanel.appendChild( resizer );
828
+ windowPanel.appendChild( windowHeader );
829
+ windowPanel.appendChild( windowContent );
830
+
831
+ document.body.appendChild( windowPanel );
832
+
833
+ return windowPanel;
834
+
835
+ }
836
+
837
+ detachTab( tab, x, y ) {
838
+
839
+ if ( tab.isDetached ) return;
840
+
841
+ // Check if tab allows detachment
842
+ if ( tab.allowDetach === false ) return;
843
+
844
+ const allButtons = Array.from( this.tabsContainer.children );
845
+
846
+ const tabIdsInOrder = allButtons.map( btn => {
847
+
848
+ return Object.keys( this.tabs ).find( id => this.tabs[ id ].button === btn );
849
+
850
+ } ).filter( id => id !== undefined );
851
+
852
+ const currentIndex = tabIdsInOrder.indexOf( tab.id );
853
+
854
+ let newActiveTab = null;
855
+
856
+ if ( this.activeTabId === tab.id ) {
857
+
858
+ tab.setActive( false );
859
+
860
+ const remainingTabs = tabIdsInOrder.filter( id =>
861
+ id !== tab.id &&
862
+ ! this.tabs[ id ].isDetached &&
863
+ this.tabs[ id ].isVisible
864
+ );
865
+
866
+ if ( remainingTabs.length > 0 ) {
867
+
868
+ for ( let i = currentIndex - 1; i >= 0; i -- ) {
869
+
870
+ if ( remainingTabs.includes( tabIdsInOrder[ i ] ) ) {
871
+
872
+ newActiveTab = tabIdsInOrder[ i ];
873
+ break;
874
+
875
+ }
876
+
877
+ }
878
+
879
+ if ( ! newActiveTab ) {
880
+
881
+ for ( let i = currentIndex + 1; i < tabIdsInOrder.length; i ++ ) {
882
+
883
+ if ( remainingTabs.includes( tabIdsInOrder[ i ] ) ) {
884
+
885
+ newActiveTab = tabIdsInOrder[ i ];
886
+ break;
887
+
888
+ }
889
+
890
+ }
891
+
892
+ }
893
+
894
+ if ( ! newActiveTab ) {
895
+
896
+ newActiveTab = remainingTabs[ 0 ];
897
+
898
+ }
899
+
900
+ }
901
+
902
+ }
903
+
904
+ if ( tab.button.parentNode ) {
905
+
906
+ tab.button.parentNode.removeChild( tab.button );
907
+
908
+ }
909
+
910
+ if ( tab.content.parentNode ) {
911
+
912
+ tab.content.parentNode.removeChild( tab.content );
913
+
914
+ }
915
+
916
+ const detachedWindow = this.createDetachedWindow( tab, x, y );
917
+ this.detachedWindows.push( detachedWindow );
918
+
919
+ tab.isDetached = true;
920
+ tab.detachedWindow = detachedWindow;
921
+
922
+ if ( newActiveTab ) {
923
+
924
+ this.setActiveTab( newActiveTab );
925
+
926
+ } else if ( this.activeTabId === tab.id ) {
927
+
928
+ this.activeTabId = null;
929
+
930
+ }
931
+
932
+ // Update panel size after detaching
933
+ this.updatePanelSize();
934
+
935
+ this.saveLayout();
936
+
937
+ }
938
+
939
+ createDetachedWindow( tab, x, y ) {
940
+
941
+ // Constrain initial position to window bounds
942
+ const windowWidth = window.innerWidth;
943
+ const windowHeight = window.innerHeight;
944
+ const estimatedWidth = 400; // Default detached window width
945
+ const estimatedHeight = 300; // Default detached window height
946
+
947
+ let constrainedX = x - 200;
948
+ let constrainedY = y - 20;
949
+
950
+ if ( constrainedX + estimatedWidth > windowWidth ) {
951
+
952
+ constrainedX = windowWidth - estimatedWidth;
953
+
954
+ }
955
+
956
+ if ( constrainedX < 0 ) {
957
+
958
+ constrainedX = 0;
959
+
960
+ }
961
+
962
+ if ( constrainedY + estimatedHeight > windowHeight ) {
963
+
964
+ constrainedY = windowHeight - estimatedHeight;
965
+
966
+ }
967
+
968
+ if ( constrainedY < 0 ) {
969
+
970
+ constrainedY = 0;
971
+
972
+ }
973
+
974
+ const windowPanel = document.createElement( 'div' );
975
+ windowPanel.className = 'detached-tab-panel';
976
+ windowPanel.style.left = `${ constrainedX }px`;
977
+ windowPanel.style.top = `${ constrainedY }px`;
978
+
979
+ if ( ! this.panel.classList.contains( 'visible' ) ) {
980
+
981
+ windowPanel.style.opacity = '0';
982
+ windowPanel.style.visibility = 'hidden';
983
+ windowPanel.style.pointerEvents = 'none';
984
+
985
+ }
986
+
987
+ // Hide detached window if tab is not visible
988
+ if ( ! tab.isVisible ) {
989
+
990
+ windowPanel.style.display = 'none';
991
+
992
+ }
993
+
994
+ const windowHeader = document.createElement( 'div' );
995
+ windowHeader.className = 'detached-tab-header';
996
+
997
+ const title = document.createElement( 'span' );
998
+ title.textContent = tab.button.textContent.replace( '⇱', '' ).trim();
999
+ windowHeader.appendChild( title );
1000
+
1001
+ const headerControls = document.createElement( 'div' );
1002
+ headerControls.className = 'detached-header-controls';
1003
+
1004
+ const reattachBtn = document.createElement( 'button' );
1005
+ reattachBtn.className = 'detached-reattach-btn';
1006
+ reattachBtn.innerHTML = '↩';
1007
+ reattachBtn.title = 'Reattach to main panel';
1008
+ reattachBtn.onclick = () => this.reattachTab( tab );
1009
+
1010
+ headerControls.appendChild( reattachBtn );
1011
+ windowHeader.appendChild( headerControls );
1012
+
1013
+ const windowContent = document.createElement( 'div' );
1014
+ windowContent.className = 'detached-tab-content';
1015
+ windowContent.appendChild( tab.content );
1016
+
1017
+ // Make sure content is visible
1018
+ tab.content.style.display = 'block';
1019
+ tab.content.classList.add( 'active' );
1020
+
1021
+ // Create resize handles for all edges
1022
+ const resizerTop = document.createElement( 'div' );
1023
+ resizerTop.className = 'detached-tab-resizer-top';
1024
+
1025
+ const resizerRight = document.createElement( 'div' );
1026
+ resizerRight.className = 'detached-tab-resizer-right';
1027
+
1028
+ const resizerBottom = document.createElement( 'div' );
1029
+ resizerBottom.className = 'detached-tab-resizer-bottom';
1030
+
1031
+ const resizerLeft = document.createElement( 'div' );
1032
+ resizerLeft.className = 'detached-tab-resizer-left';
1033
+
1034
+ const resizerCorner = document.createElement( 'div' );
1035
+ resizerCorner.className = 'detached-tab-resizer';
1036
+
1037
+ windowPanel.appendChild( resizerTop );
1038
+ windowPanel.appendChild( resizerRight );
1039
+ windowPanel.appendChild( resizerBottom );
1040
+ windowPanel.appendChild( resizerLeft );
1041
+ windowPanel.appendChild( resizerCorner );
1042
+ windowPanel.appendChild( windowHeader );
1043
+ windowPanel.appendChild( windowContent );
1044
+
1045
+ document.body.appendChild( windowPanel );
1046
+
1047
+ // Setup window dragging
1048
+ this.setupDetachedWindowDrag( windowPanel, windowHeader, tab );
1049
+
1050
+ // Setup window resizing
1051
+ this.setupDetachedWindowResize( windowPanel, resizerTop, resizerRight, resizerBottom, resizerLeft, resizerCorner );
1052
+
1053
+ // Use the same z-index that was set on the preview window
1054
+ windowPanel.style.setProperty( 'z-index', this.maxZIndex, 'important' );
1055
+
1056
+ return { panel: windowPanel, tab: tab };
1057
+
1058
+ }
1059
+
1060
+ bringWindowToFront( windowPanel ) {
1061
+
1062
+ // Increment the max z-index and apply it to the clicked window
1063
+ this.maxZIndex ++;
1064
+ windowPanel.style.setProperty( 'z-index', this.maxZIndex, 'important' );
1065
+
1066
+ }
1067
+
1068
+ setupDetachedWindowDrag( windowPanel, header, tab ) {
1069
+
1070
+ let isDragging = false;
1071
+ let startX, startY, startLeft, startTop;
1072
+
1073
+ // Bring window to front when clicking anywhere on it
1074
+ windowPanel.addEventListener( 'pointerdown', () => {
1075
+
1076
+ this.bringWindowToFront( windowPanel );
1077
+
1078
+ } );
1079
+
1080
+ const onDragStart = ( e ) => {
1081
+
1082
+ if ( e.target.classList.contains( 'detached-reattach-btn' ) ) {
1083
+
1084
+ return;
1085
+
1086
+ }
1087
+
1088
+ // Bring window to front when starting to drag
1089
+ this.bringWindowToFront( windowPanel );
1090
+
1091
+ isDragging = true;
1092
+ header.style.cursor = 'grabbing';
1093
+ header.setPointerCapture( e.pointerId );
1094
+
1095
+ startX = e.clientX;
1096
+ startY = e.clientY;
1097
+
1098
+ const rect = windowPanel.getBoundingClientRect();
1099
+ startLeft = rect.left;
1100
+ startTop = rect.top;
1101
+
1102
+ };
1103
+
1104
+ const onDragMove = ( e ) => {
1105
+
1106
+ if ( ! isDragging ) return;
1107
+
1108
+ e.preventDefault();
1109
+
1110
+ const currentX = e.clientX;
1111
+ const currentY = e.clientY;
1112
+
1113
+ const deltaX = currentX - startX;
1114
+ const deltaY = currentY - startY;
1115
+
1116
+ let newLeft = startLeft + deltaX;
1117
+ let newTop = startTop + deltaY;
1118
+
1119
+ // Constrain to window bounds (allow half width/height to extend outside)
1120
+ const windowWidth = window.innerWidth;
1121
+ const windowHeight = window.innerHeight;
1122
+ const panelWidth = windowPanel.offsetWidth;
1123
+ const panelHeight = windowPanel.offsetHeight;
1124
+ const halfWidth = panelWidth / 2;
1125
+ const halfHeight = panelHeight / 2;
1126
+
1127
+ // Allow window to extend half its width beyond right edge
1128
+ if ( newLeft + panelWidth > windowWidth + halfWidth ) {
1129
+
1130
+ newLeft = windowWidth + halfWidth - panelWidth;
1131
+
1132
+ }
1133
+
1134
+ // Allow window to extend half its width beyond left edge
1135
+ if ( newLeft < - halfWidth ) {
1136
+
1137
+ newLeft = - halfWidth;
1138
+
1139
+ }
1140
+
1141
+ // Allow window to extend half its height beyond bottom edge
1142
+ if ( newTop + panelHeight > windowHeight + halfHeight ) {
1143
+
1144
+ newTop = windowHeight + halfHeight - panelHeight;
1145
+
1146
+ }
1147
+
1148
+ // Allow window to extend half its height beyond top edge
1149
+ if ( newTop < - halfHeight ) {
1150
+
1151
+ newTop = - halfHeight;
1152
+
1153
+ }
1154
+
1155
+ windowPanel.style.left = `${ newLeft }px`;
1156
+ windowPanel.style.top = `${ newTop }px`;
1157
+
1158
+ // Check if cursor is over the inspector panel
1159
+ const panelRect = this.panel.getBoundingClientRect();
1160
+ const isOverPanel = currentX >= panelRect.left && currentX <= panelRect.right &&
1161
+ currentY >= panelRect.top && currentY <= panelRect.bottom;
1162
+
1163
+ if ( isOverPanel ) {
1164
+
1165
+ windowPanel.style.opacity = '0.5';
1166
+ this.panel.style.outline = '2px solid var(--accent-color)';
1167
+
1168
+ } else {
1169
+
1170
+ windowPanel.style.opacity = '';
1171
+ this.panel.style.outline = '';
1172
+
1173
+ }
1174
+
1175
+ };
1176
+
1177
+ const onDragEnd = ( e ) => {
1178
+
1179
+ if ( ! isDragging ) return;
1180
+
1181
+ isDragging = false;
1182
+ header.style.cursor = '';
1183
+ windowPanel.style.opacity = '';
1184
+ this.panel.style.outline = '';
1185
+
1186
+ // Check if dropped over the inspector panel
1187
+ const currentX = e.clientX;
1188
+ const currentY = e.clientY;
1189
+
1190
+ if ( currentX !== undefined && currentY !== undefined ) {
1191
+
1192
+ const panelRect = this.panel.getBoundingClientRect();
1193
+ const isOverPanel = currentX >= panelRect.left && currentX <= panelRect.right &&
1194
+ currentY >= panelRect.top && currentY <= panelRect.bottom;
1195
+
1196
+ if ( isOverPanel && tab ) {
1197
+
1198
+ // Reattach the tab
1199
+ this.reattachTab( tab );
1200
+
1201
+ } else {
1202
+
1203
+ // Save layout after moving detached window
1204
+ this.saveLayout();
1205
+
1206
+ }
1207
+
1208
+ }
1209
+
1210
+ header.removeEventListener( 'pointermove', onDragMove );
1211
+ header.removeEventListener( 'pointerup', onDragEnd );
1212
+ header.removeEventListener( 'pointercancel', onDragEnd );
1213
+
1214
+ };
1215
+
1216
+ header.addEventListener( 'pointerdown', ( e ) => {
1217
+
1218
+ onDragStart( e );
1219
+ header.addEventListener( 'pointermove', onDragMove );
1220
+ header.addEventListener( 'pointerup', onDragEnd );
1221
+ header.addEventListener( 'pointercancel', onDragEnd );
1222
+
1223
+ } );
1224
+
1225
+ header.style.cursor = 'grab';
1226
+
1227
+ }
1228
+
1229
+ setupDetachedWindowResize( windowPanel, resizerTop, resizerRight, resizerBottom, resizerLeft, resizerCorner ) {
1230
+
1231
+ const minWidth = 250;
1232
+ const minHeight = 150;
1233
+
1234
+ const setupResizer = ( resizer, direction ) => {
1235
+
1236
+ let isResizing = false;
1237
+ let startX, startY, startWidth, startHeight, startLeft, startTop;
1238
+
1239
+ const onResizeStart = ( e ) => {
1240
+
1241
+ e.preventDefault();
1242
+ e.stopPropagation();
1243
+ isResizing = true;
1244
+
1245
+ // Bring window to front when resizing
1246
+ this.bringWindowToFront( windowPanel );
1247
+
1248
+ resizer.setPointerCapture( e.pointerId );
1249
+
1250
+ startX = e.clientX;
1251
+ startY = e.clientY;
1252
+ startWidth = windowPanel.offsetWidth;
1253
+ startHeight = windowPanel.offsetHeight;
1254
+ startLeft = windowPanel.offsetLeft;
1255
+ startTop = windowPanel.offsetTop;
1256
+
1257
+ };
1258
+
1259
+ const onResizeMove = ( e ) => {
1260
+
1261
+ if ( ! isResizing ) return;
1262
+
1263
+ e.preventDefault();
1264
+
1265
+ const currentX = e.clientX;
1266
+ const currentY = e.clientY;
1267
+
1268
+ const deltaX = currentX - startX;
1269
+ const deltaY = currentY - startY;
1270
+
1271
+ const windowWidth = window.innerWidth;
1272
+ const windowHeight = window.innerHeight;
1273
+
1274
+ if ( direction === 'right' || direction === 'corner' ) {
1275
+
1276
+ const newWidth = startWidth + deltaX;
1277
+ const maxWidth = windowWidth - startLeft;
1278
+
1279
+ if ( newWidth >= minWidth && newWidth <= maxWidth ) {
1280
+
1281
+ windowPanel.style.width = `${ newWidth }px`;
1282
+
1283
+ }
1284
+
1285
+ }
1286
+
1287
+ if ( direction === 'bottom' || direction === 'corner' ) {
1288
+
1289
+ const newHeight = startHeight + deltaY;
1290
+ const maxHeight = windowHeight - startTop;
1291
+
1292
+ if ( newHeight >= minHeight && newHeight <= maxHeight ) {
1293
+
1294
+ windowPanel.style.height = `${ newHeight }px`;
1295
+
1296
+ }
1297
+
1298
+ }
1299
+
1300
+ if ( direction === 'left' ) {
1301
+
1302
+ const newWidth = startWidth - deltaX;
1303
+ const maxLeft = startLeft + startWidth - minWidth;
1304
+
1305
+ if ( newWidth >= minWidth ) {
1306
+
1307
+ const newLeft = startLeft + deltaX;
1308
+
1309
+ if ( newLeft >= 0 && newLeft <= maxLeft ) {
1310
+
1311
+ windowPanel.style.width = `${ newWidth }px`;
1312
+ windowPanel.style.left = `${ newLeft }px`;
1313
+
1314
+ }
1315
+
1316
+ }
1317
+
1318
+ }
1319
+
1320
+ if ( direction === 'top' ) {
1321
+
1322
+ const newHeight = startHeight - deltaY;
1323
+ const maxTop = startTop + startHeight - minHeight;
1324
+
1325
+ if ( newHeight >= minHeight ) {
1326
+
1327
+ const newTop = startTop + deltaY;
1328
+
1329
+ if ( newTop >= 0 && newTop <= maxTop ) {
1330
+
1331
+ windowPanel.style.height = `${ newHeight }px`;
1332
+ windowPanel.style.top = `${ newTop }px`;
1333
+
1334
+ }
1335
+
1336
+ }
1337
+
1338
+ }
1339
+
1340
+ };
1341
+
1342
+ const onResizeEnd = () => {
1343
+
1344
+ isResizing = false;
1345
+
1346
+ resizer.removeEventListener( 'pointermove', onResizeMove );
1347
+ resizer.removeEventListener( 'pointerup', onResizeEnd );
1348
+ resizer.removeEventListener( 'pointercancel', onResizeEnd );
1349
+
1350
+ // Save layout after resizing detached window
1351
+ this.saveLayout();
1352
+
1353
+ };
1354
+
1355
+ resizer.addEventListener( 'pointerdown', ( e ) => {
1356
+
1357
+ onResizeStart( e );
1358
+ resizer.addEventListener( 'pointermove', onResizeMove );
1359
+ resizer.addEventListener( 'pointerup', onResizeEnd );
1360
+ resizer.addEventListener( 'pointercancel', onResizeEnd );
1361
+
1362
+ } );
1363
+
1364
+ };
1365
+
1366
+ // Setup all resizers
1367
+ setupResizer( resizerTop, 'top' );
1368
+ setupResizer( resizerRight, 'right' );
1369
+ setupResizer( resizerBottom, 'bottom' );
1370
+ setupResizer( resizerLeft, 'left' );
1371
+ setupResizer( resizerCorner, 'corner' );
1372
+
1373
+ }
1374
+
1375
+ reattachTab( tab ) {
1376
+
1377
+ if ( ! tab.isDetached ) return;
1378
+
1379
+ if ( tab.detachedWindow ) {
1380
+
1381
+ const index = this.detachedWindows.indexOf( tab.detachedWindow );
1382
+
1383
+ if ( index > - 1 ) {
1384
+
1385
+ this.detachedWindows.splice( index, 1 );
1386
+
1387
+ }
1388
+
1389
+ if ( tab.detachedWindow.panel.parentNode ) {
1390
+
1391
+ tab.detachedWindow.panel.parentNode.removeChild( tab.detachedWindow.panel );
1392
+
1393
+ }
1394
+
1395
+ tab.detachedWindow = null;
1396
+
1397
+ }
1398
+
1399
+ tab.isDetached = false;
1400
+
1401
+ // Get all tabs and sort by their original index to determine the correct order
1402
+ const allTabs = Object.values( this.tabs );
1403
+ const allTabsSorted = allTabs
1404
+ .filter( t => t.originalIndex !== undefined && t.isVisible )
1405
+ .sort( ( a, b ) => a.originalIndex - b.originalIndex );
1406
+
1407
+ // Get currently attached tab buttons
1408
+ const currentButtons = Array.from( this.tabsContainer.children );
1409
+
1410
+ // Find the correct position for this tab
1411
+ let insertIndex = 0;
1412
+ for ( const t of allTabsSorted ) {
1413
+
1414
+ if ( t.id === tab.id ) {
1415
+
1416
+ break;
1417
+
1418
+ }
1419
+
1420
+ // Count only non-detached tabs that come before this one
1421
+ if ( ! t.isDetached ) {
1422
+
1423
+ insertIndex ++;
1424
+
1425
+ }
1426
+
1427
+ }
1428
+
1429
+ // Insert the button at the correct position
1430
+ if ( insertIndex >= currentButtons.length || currentButtons.length === 0 ) {
1431
+
1432
+ // If insert index is beyond current buttons, or no buttons exist, append at the end
1433
+ this.tabsContainer.appendChild( tab.button );
1434
+
1435
+ } else {
1436
+
1437
+ // Insert before the button at the insert index
1438
+ this.tabsContainer.insertBefore( tab.button, currentButtons[ insertIndex ] );
1439
+
1440
+ }
1441
+
1442
+ this.contentWrapper.appendChild( tab.content );
1443
+
1444
+ this.setActiveTab( tab.id );
1445
+
1446
+ // Update panel size after reattaching
1447
+ this.updatePanelSize();
1448
+
1449
+ this.saveLayout();
1450
+
1451
+ }
1452
+
1453
+ setActiveTab( id ) {
1454
+
1455
+ if ( this.activeTabId && this.tabs[ this.activeTabId ] && ! this.tabs[ this.activeTabId ].isDetached ) {
1456
+
1457
+ this.tabs[ this.activeTabId ].setActive( false );
1458
+
1459
+ }
1460
+
1461
+ this.activeTabId = id;
1462
+
1463
+ if ( this.tabs[ id ] ) {
1464
+
1465
+ this.tabs[ id ].setActive( true );
1466
+
1467
+ }
1468
+
1469
+ }
1470
+
1471
+ togglePanel() {
1472
+
1473
+ this.panel.classList.toggle( 'visible' );
1474
+ this.toggleButton.classList.toggle( 'hidden' );
1475
+
1476
+ const isVisible = this.panel.classList.contains( 'visible' );
1477
+
1478
+ if ( isVisible ) {
1479
+
1480
+ // Save mini-panel state before hiding
1481
+ this.savedMiniPanelState = {
1482
+ isVisible: this.miniPanel.classList.contains( 'visible' ),
1483
+ activeTabId: null,
1484
+ contentMap: {}
1485
+ };
1486
+
1487
+ // Find which tab was active in mini-panel
1488
+ this.miniPanel.querySelectorAll( '.mini-panel-content' ).forEach( content => {
1489
+
1490
+ if ( content.style.display !== 'none' && content.firstChild ) {
1491
+
1492
+ // Find the tab that owns this content
1493
+ Object.values( this.tabs ).forEach( tab => {
1494
+
1495
+ if ( tab.miniContent === content ) {
1496
+
1497
+ this.savedMiniPanelState.activeTabId = tab.id;
1498
+ // Move content back to main panel
1499
+ tab.content.appendChild( content.firstChild );
1500
+
1501
+ }
1502
+
1503
+ } );
1504
+
1505
+ }
1506
+
1507
+ } );
1508
+
1509
+ // Hide mini-panel temporarily
1510
+ this.miniPanel.classList.remove( 'visible' );
1511
+
1512
+ // Hide all mini-panel contents
1513
+ this.miniPanel.querySelectorAll( '.mini-panel-content' ).forEach( content => {
1514
+
1515
+ content.style.display = 'none';
1516
+
1517
+ } );
1518
+
1519
+ // Remove active state from builtin buttons
1520
+ this.builtinTabsContainer.querySelectorAll( '.builtin-tab-btn' ).forEach( btn => {
1521
+
1522
+ btn.classList.remove( 'active' );
1523
+
1524
+ } );
1525
+
1526
+ } else {
1527
+
1528
+ // Restore mini-panel state when minimizing
1529
+ if ( this.savedMiniPanelState && this.savedMiniPanelState.isVisible && this.savedMiniPanelState.activeTabId ) {
1530
+
1531
+ const tab = this.tabs[ this.savedMiniPanelState.activeTabId ];
1532
+
1533
+ if ( tab && tab.miniContent && tab.builtinButton ) {
1534
+
1535
+ // Restore mini-panel visibility
1536
+ this.miniPanel.classList.add( 'visible' );
1537
+ tab.miniContent.style.display = 'block';
1538
+ tab.builtinButton.classList.add( 'active' );
1539
+
1540
+ // Move content back to mini-panel
1541
+ const actualContent = tab.content.querySelector( '.list-scroll-wrapper, .profiler-content > *' );
1542
+
1543
+ if ( actualContent ) {
1544
+
1545
+ tab.miniContent.appendChild( actualContent );
1546
+
1547
+ }
1548
+
1549
+ }
1550
+
1551
+ }
1552
+
1553
+ }
1554
+
1555
+ this.detachedWindows.forEach( detachedWindow => {
1556
+
1557
+ if ( isVisible ) {
1558
+
1559
+ detachedWindow.panel.style.opacity = '';
1560
+ detachedWindow.panel.style.visibility = '';
1561
+ detachedWindow.panel.style.pointerEvents = '';
1562
+
1563
+ } else {
1564
+
1565
+ detachedWindow.panel.style.opacity = '0';
1566
+ detachedWindow.panel.style.visibility = 'hidden';
1567
+ detachedWindow.panel.style.pointerEvents = 'none';
1568
+
1569
+ }
1570
+
1571
+ } );
1572
+
1573
+ }
1574
+
1575
+ togglePosition() {
1576
+
1577
+ const newPosition = this.position === 'bottom' ? 'right' : 'bottom';
1578
+ this.setPosition( newPosition );
1579
+
1580
+ }
1581
+
1582
+ setPosition( targetPosition ) {
1583
+
1584
+ if ( this.position === targetPosition ) return;
1585
+
1586
+ this.panel.style.transition = 'none';
1587
+
1588
+ // Check if panel is currently maximized
1589
+ const isMaximized = this.panel.classList.contains( 'maximized' );
1590
+
1591
+ if ( targetPosition === 'right' ) {
1592
+
1593
+ this.position = 'right';
1594
+ this.floatingBtn.classList.add( 'active' );
1595
+ this.floatingBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><path d="M3 15h18"></path></svg>';
1596
+ this.floatingBtn.title = 'Switch to Bottom';
1597
+
1598
+ // Apply right position styles
1599
+ this.panel.classList.remove( 'position-bottom' );
1600
+ this.panel.classList.add( 'position-right' );
1601
+ this.panel.style.bottom = '';
1602
+ this.panel.style.top = '0';
1603
+ this.panel.style.right = '0';
1604
+ this.panel.style.left = '';
1605
+
1606
+ // Apply size based on maximized state
1607
+ if ( isMaximized ) {
1608
+
1609
+ this.panel.style.width = '100vw';
1610
+ this.panel.style.height = '100%';
1611
+
1612
+ } else {
1613
+
1614
+ this.panel.style.width = `${ this.lastWidthRight }px`;
1615
+ this.panel.style.height = '100%';
1616
+
1617
+ }
1618
+
1619
+ } else {
1620
+
1621
+ this.position = 'bottom';
1622
+ this.floatingBtn.classList.remove( 'active' );
1623
+ this.floatingBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="15" y1="3" x2="15" y2="21"></line></svg>';
1624
+ this.floatingBtn.title = 'Switch to Right Side';
1625
+
1626
+ // Apply bottom position styles
1627
+ this.panel.classList.remove( 'position-right' );
1628
+ this.panel.classList.add( 'position-bottom' );
1629
+ this.panel.style.top = '';
1630
+ this.panel.style.right = '';
1631
+ this.panel.style.bottom = '0';
1632
+ this.panel.style.left = '0';
1633
+
1634
+ // Apply size based on maximized state
1635
+ if ( isMaximized ) {
1636
+
1637
+ this.panel.style.width = '100%';
1638
+ this.panel.style.height = '100vh';
1639
+
1640
+ } else {
1641
+
1642
+ this.panel.style.width = '100%';
1643
+ this.panel.style.height = `${ this.lastHeightBottom }px`;
1644
+
1645
+ }
1646
+
1647
+ }
1648
+
1649
+ // Re-enable transition after a brief delay
1650
+ setTimeout( () => {
1651
+
1652
+ this.panel.style.transition = '';
1653
+
1654
+ }, 50 );
1655
+
1656
+ // Update panel size based on visible tabs
1657
+ this.updatePanelSize();
1658
+
1659
+ // Save layout after position change
1660
+ this.saveLayout();
1661
+
1662
+ }
1663
+
1664
+ saveLayout() {
1665
+
1666
+ const layout = {
1667
+ position: this.position,
1668
+ lastHeightBottom: this.lastHeightBottom,
1669
+ lastWidthRight: this.lastWidthRight,
1670
+ activeTabId: this.activeTabId,
1671
+ detachedTabs: []
1672
+ };
1673
+
1674
+ // Save detached windows state
1675
+ this.detachedWindows.forEach( detachedWindow => {
1676
+
1677
+ const tab = detachedWindow.tab;
1678
+ const panel = detachedWindow.panel;
1679
+
1680
+ // Get position values, ensuring they're valid numbers
1681
+ const left = parseFloat( panel.style.left ) || panel.offsetLeft || 0;
1682
+ const top = parseFloat( panel.style.top ) || panel.offsetTop || 0;
1683
+ const width = panel.offsetWidth;
1684
+ const height = panel.offsetHeight;
1685
+
1686
+ layout.detachedTabs.push( {
1687
+ tabId: tab.id,
1688
+ originalIndex: tab.originalIndex !== undefined ? tab.originalIndex : 0,
1689
+ left: left,
1690
+ top: top,
1691
+ width: width,
1692
+ height: height
1693
+ } );
1694
+
1695
+ } );
1696
+
1697
+ try {
1698
+
1699
+ localStorage.setItem( 'profiler-layout', JSON.stringify( layout ) );
1700
+
1701
+ } catch ( e ) {
1702
+
1703
+ console.warn( 'Failed to save profiler layout:', e );
1704
+
1705
+ }
1706
+
1707
+ }
1708
+
1709
+ loadLayout() {
1710
+
1711
+ try {
1712
+
1713
+ const savedLayout = localStorage.getItem( 'profiler-layout' );
1714
+
1715
+ if ( ! savedLayout ) return;
1716
+
1717
+ const layout = JSON.parse( savedLayout );
1718
+
1719
+ // Constrain detached tabs positions to current screen bounds
1720
+ if ( layout.detachedTabs && layout.detachedTabs.length > 0 ) {
1721
+
1722
+ const windowWidth = window.innerWidth;
1723
+ const windowHeight = window.innerHeight;
1724
+
1725
+ layout.detachedTabs = layout.detachedTabs.map( detachedTabData => {
1726
+
1727
+ let { left, top, width, height } = detachedTabData;
1728
+
1729
+ // Ensure width and height are within bounds
1730
+ if ( width > windowWidth ) {
1731
+
1732
+ width = windowWidth - 100; // Leave some margin
1733
+
1734
+ }
1735
+
1736
+ if ( height > windowHeight ) {
1737
+
1738
+ height = windowHeight - 100; // Leave some margin
1739
+
1740
+ }
1741
+
1742
+ // Allow window to extend half its width/height outside the screen
1743
+ const halfWidth = width / 2;
1744
+ const halfHeight = height / 2;
1745
+
1746
+ // Constrain horizontal position (allow half width to extend beyond right edge)
1747
+ if ( left + width > windowWidth + halfWidth ) {
1748
+
1749
+ left = windowWidth + halfWidth - width;
1750
+
1751
+ }
1752
+
1753
+ // Constrain horizontal position (allow half width to extend beyond left edge)
1754
+ if ( left < - halfWidth ) {
1755
+
1756
+ left = - halfWidth;
1757
+
1758
+ }
1759
+
1760
+ // Constrain vertical position (allow half height to extend beyond bottom edge)
1761
+ if ( top + height > windowHeight + halfHeight ) {
1762
+
1763
+ top = windowHeight + halfHeight - height;
1764
+
1765
+ }
1766
+
1767
+ // Constrain vertical position (allow half height to extend beyond top edge)
1768
+ if ( top < - halfHeight ) {
1769
+
1770
+ top = - halfHeight;
1771
+
1772
+ }
1773
+
1774
+ return {
1775
+ ...detachedTabData,
1776
+ left,
1777
+ top,
1778
+ width,
1779
+ height
1780
+ };
1781
+
1782
+ } );
1783
+
1784
+ }
1785
+
1786
+ // Restore position and dimensions
1787
+ if ( layout.position ) {
1788
+
1789
+ this.position = layout.position;
1790
+
1791
+ }
1792
+
1793
+ if ( layout.lastHeightBottom ) {
1794
+
1795
+ this.lastHeightBottom = layout.lastHeightBottom;
1796
+
1797
+ }
1798
+
1799
+ if ( layout.lastWidthRight ) {
1800
+
1801
+ this.lastWidthRight = layout.lastWidthRight;
1802
+
1803
+ }
1804
+
1805
+ // Constrain saved dimensions to current screen bounds
1806
+ const windowWidth = window.innerWidth;
1807
+ const windowHeight = window.innerHeight;
1808
+
1809
+ if ( this.lastHeightBottom > windowHeight - 50 ) {
1810
+
1811
+ this.lastHeightBottom = windowHeight - 50;
1812
+
1813
+ }
1814
+
1815
+ if ( this.lastWidthRight > windowWidth - 50 ) {
1816
+
1817
+ this.lastWidthRight = windowWidth - 50;
1818
+
1819
+ }
1820
+
1821
+ // Apply the saved position after shell is set up
1822
+ if ( this.position === 'right' ) {
1823
+
1824
+ this.floatingBtn.classList.add( 'active' );
1825
+ this.floatingBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><path d="M3 15h18"></path></svg>';
1826
+ this.floatingBtn.title = 'Switch to Bottom';
1827
+
1828
+ this.panel.classList.remove( 'position-bottom' );
1829
+ this.panel.classList.add( 'position-right' );
1830
+ this.panel.style.bottom = '';
1831
+ this.panel.style.top = '0';
1832
+ this.panel.style.right = '0';
1833
+ this.panel.style.left = '';
1834
+ this.panel.style.width = `${ this.lastWidthRight }px`;
1835
+ this.panel.style.height = '100%';
1836
+
1837
+ } else {
1838
+
1839
+ this.panel.style.height = `${ this.lastHeightBottom }px`;
1840
+
1841
+ }
1842
+
1843
+ if ( layout.activeTabId ) {
1844
+
1845
+ const willBeDetached = layout.detachedTabs &&
1846
+ layout.detachedTabs.some( dt => dt.tabId === layout.activeTabId );
1847
+
1848
+ if ( willBeDetached ) {
1849
+
1850
+ this.setActiveTab( layout.activeTabId );
1851
+
1852
+ }
1853
+
1854
+ }
1855
+
1856
+ if ( layout.detachedTabs && layout.detachedTabs.length > 0 ) {
1857
+
1858
+ this.pendingDetachedTabs = layout.detachedTabs;
1859
+ this.restoreDetachedTabs();
1860
+
1861
+ }
1862
+
1863
+ // Update panel size after loading layout
1864
+ this.updatePanelSize();
1865
+
1866
+ } catch ( e ) {
1867
+
1868
+ console.warn( 'Failed to load profiler layout:', e );
1869
+
1870
+ }
1871
+
1872
+ }
1873
+
1874
+ restoreDetachedTabs() {
1875
+
1876
+ if ( ! this.pendingDetachedTabs || this.pendingDetachedTabs.length === 0 ) return;
1877
+
1878
+ this.pendingDetachedTabs.forEach( detachedTabData => {
1879
+
1880
+ const tab = this.tabs[ detachedTabData.tabId ];
1881
+
1882
+ if ( ! tab || tab.isDetached ) return;
1883
+
1884
+ // Restore originalIndex if saved
1885
+ if ( detachedTabData.originalIndex !== undefined ) {
1886
+
1887
+ tab.originalIndex = detachedTabData.originalIndex;
1888
+
1889
+ }
1890
+
1891
+ if ( tab.button.parentNode ) {
1892
+
1893
+ tab.button.parentNode.removeChild( tab.button );
1894
+
1895
+ }
1896
+
1897
+ if ( tab.content.parentNode ) {
1898
+
1899
+ tab.content.parentNode.removeChild( tab.content );
1900
+
1901
+ }
1902
+
1903
+ const detachedWindow = this.createDetachedWindow( tab, 0, 0 );
1904
+
1905
+ detachedWindow.panel.style.left = `${ detachedTabData.left }px`;
1906
+ detachedWindow.panel.style.top = `${ detachedTabData.top }px`;
1907
+ detachedWindow.panel.style.width = `${ detachedTabData.width }px`;
1908
+ detachedWindow.panel.style.height = `${ detachedTabData.height }px`;
1909
+
1910
+ // Constrain window to bounds after restoring position and size
1911
+ this.constrainWindowToBounds( detachedWindow.panel );
1912
+
1913
+ this.detachedWindows.push( detachedWindow );
1914
+
1915
+ tab.isDetached = true;
1916
+ tab.detachedWindow = detachedWindow;
1917
+
1918
+ } );
1919
+
1920
+ this.pendingDetachedTabs = null;
1921
+
1922
+ // Update maxZIndex to be higher than all existing windows
1923
+ this.detachedWindows.forEach( detachedWindow => {
1924
+
1925
+ const currentZIndex = parseInt( getComputedStyle( detachedWindow.panel ).zIndex ) || 0;
1926
+ if ( currentZIndex > this.maxZIndex ) {
1927
+
1928
+ this.maxZIndex = currentZIndex;
1929
+
1930
+ }
1931
+
1932
+ } );
1933
+
1934
+ const needsNewActiveTab = ! this.activeTabId ||
1935
+ ! this.tabs[ this.activeTabId ] ||
1936
+ this.tabs[ this.activeTabId ].isDetached ||
1937
+ ! this.tabs[ this.activeTabId ].isVisible;
1938
+
1939
+ if ( needsNewActiveTab ) {
1940
+
1941
+ const tabIds = Object.keys( this.tabs );
1942
+ const availableTabs = tabIds.filter( id =>
1943
+ ! this.tabs[ id ].isDetached &&
1944
+ this.tabs[ id ].isVisible
1945
+ );
1946
+
1947
+ if ( availableTabs.length > 0 ) {
1948
+
1949
+ const buttons = Array.from( this.tabsContainer.children );
1950
+ const orderedTabIds = buttons.map( btn => {
1951
+
1952
+ return Object.keys( this.tabs ).find( id => this.tabs[ id ].button === btn );
1953
+
1954
+ } ).filter( id =>
1955
+ id !== undefined &&
1956
+ ! this.tabs[ id ].isDetached &&
1957
+ this.tabs[ id ].isVisible
1958
+ );
1959
+
1960
+ this.setActiveTab( orderedTabIds[ 0 ] || availableTabs[ 0 ] );
1961
+
1962
+ } else {
1963
+
1964
+ this.activeTabId = null;
1965
+
1966
+ }
1967
+
1968
+ }
1969
+
1970
+ // Update panel size after restoring detached tabs
1971
+ this.updatePanelSize();
167
1972
 
168
1973
  }
169
1974