From 014108770b29bf432981879e892f3eddc521c889 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 26 Mar 2026 14:33:18 +0100 Subject: [PATCH 01/21] opened 2.13-dev --- composer.json | 2 +- src/Tracy/Debugger/Debugger.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index a4f2e7530..440f5f3e0 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.12-dev" + "dev-master": "2.13-dev" } }, "config": { diff --git a/src/Tracy/Debugger/Debugger.php b/src/Tracy/Debugger/Debugger.php index b48dbb75e..37716fa71 100644 --- a/src/Tracy/Debugger/Debugger.php +++ b/src/Tracy/Debugger/Debugger.php @@ -17,7 +17,7 @@ */ class Debugger { - public const Version = '2.12.1'; + public const Version = '2.13-dev'; /** server modes for Debugger::enable() */ public const From 216a4c147d961278ff2ab375a31dec69971223c1 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 18 Jul 2026 03:59:57 +0200 Subject: [PATCH 02/21] assets: real event target is obtained via Tracy.retarget() Single helper replacing e.target everywhere; with shadow DOM coming, composedPath()[0] is the element the event actually originated from. --- src/Tracy/BlueScreen/assets/bluescreen.js | 5 ++-- src/Tracy/Dumper/assets/dumper.js | 30 +++++++++++++---------- src/Tracy/assets/helpers.js | 6 ++++- src/Tracy/assets/table-sort.js | 5 ++-- src/Tracy/assets/tabs.js | 6 ++--- src/Tracy/assets/toggle.js | 2 +- 6 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/Tracy/BlueScreen/assets/bluescreen.js b/src/Tracy/BlueScreen/assets/bluescreen.js index 9e53d88bc..6103927e8 100644 --- a/src/Tracy/BlueScreen/assets/bluescreen.js +++ b/src/Tracy/BlueScreen/assets/bluescreen.js @@ -14,10 +14,11 @@ class BlueScreen { } blueScreen.addEventListener('tracy-toggle', (e) => { - if (e.target.matches('#tracy-bs-toggle')) { // blue screen toggle + let target = Tracy.retarget(e); + if (target.matches('#tracy-bs-toggle')) { // blue screen toggle document.documentElement.classList.toggle('tracy-bs-visible', !e.detail.collapsed); - } else if (!e.target.matches('.tracy-dump *') && e.detail.originalEvent) { // panel toggle + } else if (!target.matches('.tracy-dump *') && e.detail.originalEvent) { // panel toggle e.detail.relatedTarget.classList.toggle('tracy-panel-fadein', !e.detail.collapsed); } }); diff --git a/src/Tracy/Dumper/assets/dumper.js b/src/Tracy/Dumper/assets/dumper.js index 29bd5a80b..249648e19 100644 --- a/src/Tracy/Dumper/assets/dumper.js +++ b/src/Tracy/Dumper/assets/dumper.js @@ -54,9 +54,9 @@ class Dumper { Dumper.inited = true; document.documentElement.addEventListener('click', (e) => { - let el; + let el, target = Tracy.retarget(e); // enables & ctrl or cmd key - if ((e.ctrlKey || e.metaKey) && (el = e.target.closest('[data-tracy-href]'))) { + if ((e.ctrlKey || e.metaKey) && (el = target.closest('[data-tracy-href]'))) { location.href = el.getAttribute('data-tracy-href'); return false; } @@ -64,9 +64,9 @@ class Dumper { }); document.documentElement.addEventListener('tracy-beforetoggle', (e) => { - let el; + let el, target = Tracy.retarget(e); // initializes lazy inside
-			if ((el = e.target.closest('[data-tracy-snapshot]'))) {
+			if ((el = target.closest('[data-tracy-snapshot]'))) {
 				let snapshot = JSON.parse(el.getAttribute('data-tracy-snapshot'));
 				el.removeAttribute('data-tracy-snapshot');
 				el.querySelectorAll('[data-tracy-dump]').forEach((toggler) => {
@@ -80,7 +80,8 @@ class Dumper {
 		});
 
 		document.documentElement.addEventListener('tracy-toggle', (e) => {
-			if (!e.target.matches('.tracy-dump *')) {
+			let target = Tracy.retarget(e);
+			if (!target.matches('.tracy-dump *')) {
 				return;
 			}
 
@@ -93,12 +94,12 @@ class Dumper {
 
 			} else if (origE && origE.altKey && cont.querySelector('.tracy-toggle')) { // triggered by alt key
 				if (e.detail.collapsed) { // reopen
-					e.target.classList.toggle('tracy-collapsed', false);
+					target.classList.toggle('tracy-collapsed', false);
 					cont.classList.toggle('tracy-collapsed', false);
 					e.detail.collapsed = false;
 				}
 
-				let expand = e.target.tracyAltExpand = !e.target.tracyAltExpand;
+				let expand = target.tracyAltExpand = !target.tracyAltExpand;
 				toggleChildren(cont, expand ? {} : false);
 			}
 
@@ -106,34 +107,37 @@ class Dumper {
 		});
 
 		document.documentElement.addEventListener('animationend', (e) => {
+			let target = Tracy.retarget(e);
 			if (e.animationName === 'tracy-dump-flash') {
-				e.target.classList.toggle('tracy-dump-flash', false);
+				target.classList.toggle('tracy-dump-flash', false);
 			}
 		});
 
 		document.addEventListener('mouseover', (e) => {
-			if (!e.target.matches('.tracy-dump *')) {
+			let target = Tracy.retarget(e);
+			if (!target.matches('.tracy-dump *')) {
 				return;
 			}
 
 			let el;
 
-			if (e.target.matches('.tracy-dump-hash') && (el = e.target.closest('tracy-div'))) {
+			if (target.matches('.tracy-dump-hash') && (el = target.closest('tracy-div'))) {
 				el.querySelectorAll('.tracy-dump-hash').forEach((el) => {
-					if (el.textContent === e.target.textContent) {
+					if (el.textContent === target.textContent) {
 						el.classList.add('tracy-dump-highlight');
 					}
 				});
 				return;
 			}
 
-			if ((el = e.target.closest('.tracy-toggle')) && !el.title) {
+			if ((el = target.closest('.tracy-toggle')) && !el.title) {
 				el.title = HINT_ALT;
 			}
 		});
 
 		document.addEventListener('mouseout', (e) => {
-			if (e.target.matches('.tracy-dump-hash')) {
+			let target = Tracy.retarget(e);
+			if (target.matches('.tracy-dump-hash')) {
 				document.querySelectorAll('.tracy-dump-hash.tracy-dump-highlight').forEach((el) => {
 					el.classList.remove('tracy-dump-highlight');
 				});
diff --git a/src/Tracy/assets/helpers.js b/src/Tracy/assets/helpers.js
index 83c71b8e7..8d54f07bd 100644
--- a/src/Tracy/assets/helpers.js
+++ b/src/Tracy/assets/helpers.js
@@ -5,7 +5,7 @@ function init() {
 		document.addEventListener(
 			'click',
 			(e) => {
-				e.target.closest('a[href^="editor:"]')?.setAttribute('target', '_blank');
+				Tracy.retarget(e).closest('a[href^="editor:"]')?.setAttribute('target', '_blank');
 			},
 			true,
 		);
@@ -14,6 +14,10 @@ function init() {
 
 
 let Tracy = window.Tracy = window.Tracy || {};
+
+// returns the real event target even when the event crossed a shadow DOM boundary
+Tracy.retarget = (e) => e.composedPath()[0] ?? e.target;
+
 if (!Tracy.helpers) {
 	init();
 	Tracy.helpers = true;
diff --git a/src/Tracy/assets/table-sort.js b/src/Tracy/assets/table-sort.js
index 39bae80f6..a222a0f05 100644
--- a/src/Tracy/assets/table-sort.js
+++ b/src/Tracy/assets/table-sort.js
@@ -6,10 +6,11 @@
 class TableSort {
 	static init() {
 		document.documentElement.addEventListener('click', (e) => {
+			let target = Tracy.retarget(e);
 			if ((window.getSelection().type !== 'Range')
-				&& e.target.matches('.tracy-sortable > :first-child > tr:first-child *')
+				&& target.matches('.tracy-sortable > :first-child > tr:first-child *')
 			) {
-				TableSort.sort(e.target.closest('td,th'));
+				TableSort.sort(target.closest('td,th'));
 			}
 		});
 
diff --git a/src/Tracy/assets/tabs.js b/src/Tracy/assets/tabs.js
index 8ec2fe7b2..ced8f2974 100644
--- a/src/Tracy/assets/tabs.js
+++ b/src/Tracy/assets/tabs.js
@@ -6,11 +6,11 @@
 class Tabs {
 	static init() {
 		document.documentElement.addEventListener('click', (e) => {
-			let label, context;
+			let label, context, target = Tracy.retarget(e);
 			if (
 				!e.shiftKey && !e.ctrlKey && !e.metaKey
-				&& (label = e.target.closest('.tracy-tab-label'))
-				&& (context = e.target.closest('.tracy-tabs'))
+				&& (label = target.closest('.tracy-tab-label'))
+				&& (context = target.closest('.tracy-tabs'))
 			) {
 				Tabs.toggle(context, label);
 				e.preventDefault();
diff --git a/src/Tracy/assets/toggle.js b/src/Tracy/assets/toggle.js
index bcc82a9f5..9e4ac33d3 100644
--- a/src/Tracy/assets/toggle.js
+++ b/src/Tracy/assets/toggle.js
@@ -16,7 +16,7 @@ class Toggle {
 			let el;
 			if (
 				!e.shiftKey && !e.ctrlKey && !e.metaKey
-				&& (el = e.target.closest('.tracy-toggle'))
+				&& (el = Tracy.retarget(e).closest('.tracy-toggle'))
 				&& (!start || Math.pow(start[0] - e.clientX, 2) + Math.pow(start[1] - e.clientY, 2) < MOVE_THRESHOLD) // no start = keyboard activation
 			) {
 				Toggle.toggle(el, undefined, e);

From aae400e40dbc723e5a0f350c82384cadc973d003 Mon Sep 17 00:00:00 2001
From: David Grudl 
Date: Sat, 18 Jul 2026 04:00:19 +0200
Subject: [PATCH 03/21] assets: added Tracy.css registry and adoptedStyleSheets
 helpers

Infrastructure for distributing styles as constructed stylesheets;
nothing consumes it yet.
---
 src/Tracy/assets/helpers.js | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/src/Tracy/assets/helpers.js b/src/Tracy/assets/helpers.js
index 8d54f07bd..fdb161f2e 100644
--- a/src/Tracy/assets/helpers.js
+++ b/src/Tracy/assets/helpers.js
@@ -18,6 +18,25 @@ let Tracy = window.Tracy = window.Tracy || {};
 // returns the real event target even when the event crossed a shadow DOM boundary
 Tracy.retarget = (e) => e.composedPath()[0] ?? e.target;
 
+// registry of CSS sources filled by the loader (DeferredContent, Dumper::renderAssets)
+Tracy.css = Tracy.css || {};
+
+// memoized constructed stylesheets, one instance per document
+Tracy.styleSheet = (name) => {
+	let sheets = Tracy.styleSheets = Tracy.styleSheets || {};
+	if (!sheets[name]) {
+		sheets[name] = (new CSSStyleSheet);
+		sheets[name].replaceSync(Tracy.css[name] || '');
+	}
+	return sheets[name];
+};
+
+// adopts named stylesheets into a Document or ShadowRoot, without duplicates
+Tracy.adoptStyleSheets = (root, names) => {
+	let sheets = names.map(Tracy.styleSheet).filter((sheet) => !root.adoptedStyleSheets.includes(sheet));
+	root.adoptedStyleSheets = [...root.adoptedStyleSheets, ...sheets];
+};
+
 if (!Tracy.helpers) {
 	init();
 	Tracy.helpers = true;

From 69bf77208995f7e296b233086d909a1d699e3934 Mon Sep 17 00:00:00 2001
From: David Grudl 
Date: Sat, 18 Jul 2026 04:03:07 +0200
Subject: [PATCH 04/21] added Shadow DOM for CSS isolation of Bar, BlueScreen
 and Dumper

CSS is no longer injected into the host page's document.head. Styles are
distributed as constructed stylesheets (adoptedStyleSheets) built from
the Tracy.css registry, so host-page styling cannot be affected by Tracy
and vice versa. This also makes Tracy styles work under CSP without
'unsafe-inline' style-src.

- Bar:  shadow host, panels live inside a 
  wrapper kept for backward compatibility of third-party panel CSS; the
  panel popup window adopts the stylesheets explicitly in toWindow()
- BlueScreen:  shadow host; the standalone error page keeps a
  document-level \n";
+		echo "\n";
 
-		if (!Debugger::isEnabled() || !Debugger::$showBar) {
-			$s = '(function(){' . file_get_contents(__DIR__ . '/../assets/toggle.js') . '})();'
-				. '(function(){' . file_get_contents(__DIR__ . '/../assets/helpers.js') . '})();'
+		if (!Debugger::isEnabled() || !Debugger::$showBar) { // otherwise the deferred loader provides the registry & scripts
+			$css = file_get_contents(__DIR__ . '/../assets/reset.css') . $s;
+			$s = '(function(){' . file_get_contents(__DIR__ . '/../assets/helpers.js') . '})();'
+				. '(function(){' . file_get_contents(__DIR__ . '/../assets/toggle.js') . '})();'
 				. '(function(){' . file_get_contents(__DIR__ . '/../Dumper/assets/dumper.js') . '})();';
-			echo "", str_replace(['

@@ -20,26 +21,26 @@ echo ' '; -echo Tracy\Helpers::escapeHtml($title) /* pos 22:9 */; +echo Tracy\Helpers::escapeHtml($title) /* pos 23:9 */; echo ': '; -echo Tracy\Helpers::escapeHtml($exception->getMessage()) /* pos 22:19 */; -echo Tracy\Helpers::escapeHtml($code) /* pos 22:45 */; +echo Tracy\Helpers::escapeHtml($exception->getMessage()) /* pos 23:19 */; +echo Tracy\Helpers::escapeHtml($code) /* pos 23:45 */; echo ' '; -if (count($chain) > 1) /* pos 24:2 */ { +if (count($chain) > 1) /* pos 25:2 */ { echo ' @@ -63,21 +64,21 @@ echo ' require __DIR__ . '/content.phtml'; echo ' \'use strict\'; '; -echo str_replace(['