Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- refactor: simplify strict value hydration, collector cleanup reporting, sensitive-key lookup, and toolbar message validation without changing public contracts.
- test: enforce complete PHP line, method, and mutation coverage with exact HTML rendering assertions.
- perf: return the committed manifest from snapshot writes and reuse its raw rollback payload to avoid duplicate index reads and hydration.
- fix(ui): polish Request/Timeline layout, typography, focus/motion behavior, keyboard switching, and EXPLAIN status announcements.
2 changes: 1 addition & 1 deletion resources/assets/dist/css/debug.min.css

Large diffs are not rendered by default.

Binary file not shown.
2 changes: 1 addition & 1 deletion resources/assets/dist/js/debug.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion resources/assets/dist/js/toolbar.min.js

Large diffs are not rendered by default.

50 changes: 14 additions & 36 deletions resources/src/core/debug.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "../panels/db.js";
import "../panels/phpinfo-search.js";
import "../panels/userswitch.js";
import {
bindThemeToggle,
normalizeTheme,
preserveThemeInLinks,
readStoredTheme,
Expand Down Expand Up @@ -78,41 +79,18 @@ import { requestParentToolbarDrawerClose } from "../toolbar/focus.js";
return theme;
}

function bindThemeToggle() {
var button = document.querySelector("[data-yii-debug-theme-toggle]");

if (!button) {
return;
}

var icon = button.querySelector(".yii-debug-brand-icon");

button.addEventListener("click", function () {
var current =
normalizeTheme(
document.documentElement.getAttribute("data-yii-debug-theme"),
) || "light";
var next = current === "dark" ? "light" : "dark";

document.documentElement.setAttribute("data-yii-debug-theme", next);
button.setAttribute("data-current-theme", next);
writeTheme(next);
preserveThemeInLinks(next);

if (icon) {
icon.innerHTML =
next === "dark"
? button.getAttribute("data-icon-sun")
: button.getAttribute("data-icon-moon");
}

if (window.parent && window.parent !== window) {
window.parent.postMessage(
{ source: "yii-debug-toolbar", type: "theme", theme: next },
window.location.origin,
);
}
});
function bindThemeToggleButton() {
bindThemeToggle(
document.querySelector("[data-yii-debug-theme-toggle]"),
function (next) {
if (window.parent && window.parent !== window) {
window.parent.postMessage(
{ source: "yii-debug-toolbar", type: "theme", theme: next },
window.location.origin,
);
}
},
);
}

function closest(element, selector) {
Expand Down Expand Up @@ -194,7 +172,7 @@ import { requestParentToolbarDrawerClose } from "../toolbar/focus.js";
}

preserveThemeInLinks(applyTheme());
bindThemeToggle();
bindThemeToggleButton();

document.addEventListener("click", function (event) {
var tab = findToggle(event.target, "tab");
Expand Down
54 changes: 54 additions & 0 deletions resources/src/core/theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,60 @@ export function normalizeTheme(value) {
return hasDark ? "dark" : "light";
}

export function themeToggleLabel(theme) {
return normalizeTheme(theme) === "dark"
? "Switch to light theme"
: "Switch to dark theme";
}

export function bindThemeToggle(button, onThemeChange) {
if (!button) {
return;
}

const icon = button.querySelector(".yii-debug-brand-icon");
const updateToggle = (theme) => {
const normalized = normalizeTheme(theme) || "light";
const label = themeToggleLabel(normalized);

button.setAttribute("data-current-theme", normalized);
button.setAttribute("aria-label", label);
button.setAttribute(
"aria-pressed",
normalized === "dark" ? "true" : "false",
);
button.setAttribute("title", label);

if (icon) {
icon.innerHTML =
normalized === "dark"
? button.getAttribute("data-icon-sun")
: button.getAttribute("data-icon-moon");
}
};
const initial = normalizeTheme(
document.documentElement.getAttribute("data-yii-debug-theme"),
);

updateToggle(initial);

button.addEventListener("click", () => {
const current = normalizeTheme(
document.documentElement.getAttribute("data-yii-debug-theme"),
);
const next = current === "dark" ? "light" : "dark";

document.documentElement.setAttribute("data-yii-debug-theme", next);
updateToggle(next);
writeTheme(next);
preserveThemeInLinks(next);

if (onThemeChange) {
onThemeChange(next);
}
});
}

export function readStoredTheme(key = THEME_STORAGE_KEY) {
try {
return window.localStorage
Expand Down
Binary file not shown.
107 changes: 80 additions & 27 deletions resources/src/panels/db.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,31 @@
import { ajax, on } from "../core/dom.js";

const EXPLAIN_ALL_SELECTOR =
".yii-debug-db-explain-all-toggle, .yii-debug-db-explain-all a";
const EXPLAIN_ERROR_MESSAGE = "Unable to load the EXPLAIN output. Try again.";

export function updateExplainAllControl(control, expanded) {
control.textContent = expanded ? "Collapse all" : "Explain all";
control.setAttribute("aria-expanded", expanded ? "true" : "false");
}

(function () {
"use strict";

function syncExplainAllControls(expanded) {
var controls = document.querySelectorAll(EXPLAIN_ALL_SELECTOR);
var isExpanded =
typeof expanded === "boolean"
? expanded
: document.querySelectorAll(
".yii-debug-db-explain.is-open, .yii-debug-db-explain.is-loading",
).length > 0;

for (var i = 0; i < controls.length; i++) {
updateExplainAllControl(controls[i], isExpanded);
}
}

on(
document.querySelectorAll(".yii-debug-db-explain-toggle"),
"click",
Expand All @@ -14,60 +37,90 @@ import { ajax, on } from "../core/dom.js";
isOpen = container.classList.contains("is-open"),
self = this;

if (container.classList.contains("is-loading")) {
return;
}

if (isOpen) {
container.classList.remove("is-open");
self.setAttribute("aria-expanded", "false");
syncExplainAllControls();
return;
}

// Lazy-load on first open; cached afterwards.
if (target.dataset.loaded === "1") {
container.classList.add("is-open");
self.setAttribute("aria-expanded", "true");
syncExplainAllControls();
return;
}

container.classList.add("is-loading");
target.classList.remove("is-error");
target.removeAttribute("role");
target.setAttribute("aria-busy", "true");
target.textContent = "";
syncExplainAllControls();

ajax(this.href, {
success: function (xhr) {
target.innerHTML = xhr.responseText;
target.dataset.loaded = "1";
target.removeAttribute("aria-busy");
container.classList.remove("is-loading");
container.classList.add("is-open");
self.setAttribute("aria-expanded", "true");

if (container.dataset.collapseAfterLoad === "1") {
delete container.dataset.collapseAfterLoad;
self.setAttribute("aria-expanded", "false");
} else {
container.classList.add("is-open");
self.setAttribute("aria-expanded", "true");
}

syncExplainAllControls();
},
error: function () {
container.classList.remove("is-loading");
delete container.dataset.collapseAfterLoad;
target.removeAttribute("aria-busy");
target.classList.add("is-error");
target.setAttribute("role", "alert");
target.textContent = EXPLAIN_ERROR_MESSAGE;
self.setAttribute("aria-expanded", "false");
syncExplainAllControls();
},
});
},
);

on(
document.querySelectorAll(".yii-debug-db-explain-all a"),
"click",
function () {
var event = new MouseEvent("click", {
cancelable: true,
bubbles: true,
});
var toggles = document.querySelectorAll(".yii-debug-db-explain-toggle");
var anyOpen =
document.querySelectorAll(".yii-debug-db-explain.is-open").length > 0;

for (var i = 0, len = toggles.length; i < len; i++) {
var open = toggles[i]
.closest(".yii-debug-db-explain")
.classList.contains("is-open");
// When at least one is open, close every open row; when all are closed,
// open every row. Skip toggles that are already in the desired state so
// a half-open list collapses (or expands) to a uniform end state.
if (anyOpen === open) {
toggles[i].dispatchEvent(event);
}
on(document.querySelectorAll(EXPLAIN_ALL_SELECTOR), "click", function (e) {
e.preventDefault();

var event = new MouseEvent("click", {
cancelable: true,
bubbles: true,
});
var toggles = document.querySelectorAll(".yii-debug-db-explain-toggle");
var anyOpen =
document.querySelectorAll(
".yii-debug-db-explain.is-open, .yii-debug-db-explain.is-loading",
).length > 0;

for (var i = 0, len = toggles.length; i < len; i++) {
var container = toggles[i].closest(".yii-debug-db-explain");
var isOpen = container.classList.contains("is-open");
var isLoading = container.classList.contains("is-loading");

if (anyOpen && isLoading) {
container.dataset.collapseAfterLoad = "1";
} else if ((anyOpen && isOpen) || (!anyOpen && !isOpen)) {
toggles[i].dispatchEvent(event);
}
}

this.textContent = anyOpen ? "[+] Explain all" : "[-] Explain all";
},
);
syncExplainAllControls(!anyOpen);
});

syncExplainAllControls();
})();
92 changes: 69 additions & 23 deletions resources/src/panels/userswitch.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,33 +58,79 @@ import { ajax, on } from "../core/dom.js";
renderError(form, xhr);
},
});
};
},
closestElement = function (target, selector) {
if (target && target.nodeType !== 1) {
target = target.parentElement;
}

on(
document.getElementById("debug-userswitch__filter"),
"click",
function (e) {
var el;
if (
e.target.tagName.toLowerCase() === "td" &&
e.target.parentElement.parentElement.tagName.toLowerCase() === "tbody"
) {
el = e.target.parentElement;
} else if (
e.target.tagName.toLowerCase() === "tr" &&
e.target.parentElement.tagName.toLowerCase() === "tbody"
) {
el = e.target;
} else {
return;
return target && typeof target.closest === "function"
? target.closest(selector)
: null;
},
findSwitchRow = function (filter, target) {
var row = closestElement(target, "tbody tr[data-key]");

return row && filter.contains(row) ? row : null;
},
isInteractiveTarget = function (row, target) {
var interactive = closestElement(
target,
'a, button, input, select, textarea, label, summary, [role="button"], [role="link"], [contenteditable="true"]',
);

return interactive !== null && row.contains(interactive);
},
switchToRow = function (filter, target) {
var row = findSwitchRow(filter, target);
var form;
var userId;

if (!row || isInteractiveTarget(row, target)) {
return false;
}

var form = document.getElementById("debug-userswitch__set-identity");
document.getElementById("user_id").value = el.dataset.key;
form = document.getElementById("debug-userswitch__set-identity");
userId = document.getElementById("user_id");

if (!form || !userId) {
return false;
}

userId.value = row.dataset.key;
sendSetIdentity(form);
e.stopPropagation();
},
);

return true;
};

var filter = document.getElementById("debug-userswitch__filter");

on(filter, "click", function (e) {
if (!switchToRow(filter, e.target)) {
return;
}

e.stopPropagation();
});
on(filter, "keydown", function (e) {
if (e.key !== "Enter" && e.key !== " ") {
return;
}

var row = findSwitchRow(filter, e.target);

if (!row || e.target !== row || isInteractiveTarget(row, e.target)) {
return;
}

e.preventDefault();

if (!switchToRow(filter, row)) {
return;
}

e.stopPropagation();
});
on(
document.getElementById("debug-userswitch__reset-identity-button"),
"click",
Expand Down
Loading
Loading