feat: expose graphic screens in sidebar

This commit is contained in:
2026-07-21 09:45:12 +09:00
parent 44a7d90413
commit 15ad9934a9
11 changed files with 1044 additions and 117 deletions

View File

@@ -7,22 +7,63 @@
const toggle = document.getElementById("workspace-nav-toggle");
const toggleLabel = toggle && toggle.querySelector(".workspace-nav-toggle-label");
const stockTab = document.getElementById("workspace-stock-tab");
const catalogTab = document.getElementById("workspace-catalog-tab");
const settingsTab = document.getElementById("workspace-settings-tab");
const stockWorkspace = document.getElementById("stock-workspace");
const catalogWorkspace = document.getElementById("catalog-workspace");
const settingsWorkspace = document.getElementById("settings-workspace");
const marketTabs = document.getElementById("market-tabs");
const title = document.getElementById("workspace-title");
if (!region || !navigation || !navigationItems || !toggle || !toggleLabel ||
!stockTab || !catalogTab || !stockWorkspace || !catalogWorkspace ||
!marketTabs || !title) {
!stockTab || !settingsTab || !stockWorkspace || !catalogWorkspace ||
!settingsWorkspace || !marketTabs || !title) {
return;
}
const views = Object.freeze({
stock: Object.freeze({ tab: stockTab, view: stockWorkspace, title: "종목·컷" }),
catalog: Object.freeze({ tab: catalogTab, view: catalogWorkspace, title: "기타 그래픽" })
stock: Object.freeze({ view: stockWorkspace, title: "종목·컷" }),
catalog: Object.freeze({ view: catalogWorkspace, title: "기타 그래픽" }),
settings: Object.freeze({ view: settingsWorkspace, title: "설정" })
});
let activeWorkspace = "stock";
let activeMarketTabId = marketTabs.querySelector("button.active[data-tab-id]")
?.dataset.tabId || marketTabs.querySelector("button[data-tab-id]")?.dataset.tabId || null;
let pendingKeyboardMarketTabId = null;
function marketButtons() {
return Array.from(marketTabs.querySelectorAll("button[data-tab-id]"));
}
function marketButton(tabId) {
return marketButtons().find(function (button) {
return button.dataset.tabId === tabId;
}) || null;
}
function marketLabel(button) {
const label = button && button.querySelector(".workspace-nav-label");
return (label && label.textContent.trim()) || button?.textContent.trim() || views.catalog.title;
}
function selectedMenuButton(name) {
if (name === "stock") return stockTab;
if (name === "settings") return settingsTab;
if (name === "catalog") {
return marketButton(activeMarketTabId) || marketButtons()[0] || null;
}
return null;
}
function refreshCurrentMenu() {
const selected = selectedMenuButton(activeWorkspace);
Array.from(navigation.querySelectorAll(".workspace-nav-item")).forEach(function (button) {
const isCurrent = button === selected;
button.classList.toggle("workspace-current", isCurrent);
button.setAttribute("aria-current", isCurrent ? "page" : "false");
});
region.dataset.activeWorkspace = activeWorkspace;
if (activeMarketTabId) region.dataset.activeMarketTab = activeMarketTabId;
}
function setNavigationExpanded(expanded) {
const isExpanded = expanded === true;
@@ -37,31 +78,62 @@
const selected = views[name];
if (!selected) return false;
const selectedButton = selectedMenuButton(name);
const currentFocus = document.activeElement;
const focusWillBeHidden = Object.keys(views).some(function (key) {
return key !== name && views[key].view.contains(currentFocus);
});
if (focusWillBeHidden) selected.tab.focus();
if (focusWillBeHidden && selectedButton) selectedButton.focus();
Object.keys(views).forEach(function (key) {
const entry = views[key];
const isCurrent = key === name;
entry.tab.classList.toggle("workspace-current", isCurrent);
entry.tab.setAttribute("aria-current", isCurrent ? "page" : "false");
entry.view.hidden = !isCurrent;
entry.view.toggleAttribute("inert", !isCurrent);
entry.view.setAttribute("aria-hidden", isCurrent ? "false" : "true");
});
region.dataset.activeWorkspace = name;
title.textContent = selected.title;
activeWorkspace = name;
refreshCurrentMenu();
title.textContent = name === "catalog"
? marketLabel(selectedMenuButton(name))
: selected.title;
return true;
}
function syncMarket(tabId) {
const button = marketButton(tabId);
if (!button) return false;
activeMarketTabId = tabId;
region.dataset.activeMarketTab = tabId;
if (activeWorkspace === "catalog") {
refreshCurrentMenu();
title.textContent = marketLabel(button);
}
if (pendingKeyboardMarketTabId && !button.disabled) {
const pendingButton = marketButton(pendingKeyboardMarketTabId);
const focusWasLost = !document.activeElement ||
document.activeElement === document.body ||
document.activeElement === document.documentElement;
if (activeWorkspace === "catalog" && pendingButton &&
!pendingButton.disabled && focusWasLost) {
pendingButton.focus();
}
pendingKeyboardMarketTabId = null;
}
return true;
}
function select(name) {
if (views[name]) return setWorkspace(name);
if (!syncMarket(name)) return false;
return setWorkspace("catalog");
}
function focusAdjacentItem(direction) {
const items = [stockTab, catalogTab].filter(function (button) {
return button.disabled !== true;
});
const items = Array.from(navigation.querySelectorAll(".workspace-nav-item"))
.filter(function (button) { return button.disabled !== true; });
if (items.length === 0) return;
const currentIndex = items.indexOf(document.activeElement);
const nextIndex = currentIndex < 0
? 0
@@ -73,14 +145,17 @@
setNavigationExpanded(toggle.getAttribute("aria-expanded") !== "true");
});
stockTab.addEventListener("click", function () { setWorkspace("stock"); });
catalogTab.addEventListener("click", function () { setWorkspace("catalog"); });
settingsTab.addEventListener("click", function () { setWorkspace("settings"); });
navigationItems.addEventListener("keydown", function (event) {
navigation.addEventListener("keydown", function (event) {
const menuButton = event.target.closest && event.target.closest(".workspace-nav-item");
if ((event.key === "Enter" || event.key === " ") && menuButton) {
if (!menuButton) return;
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
event.stopPropagation();
setWorkspace(menuButton === stockTab ? "stock" : "catalog");
const keyboardMarketTabId = menuButton.dataset.tabId || null;
menuButton.click();
if (keyboardMarketTabId) pendingKeyboardMarketTabId = keyboardMarketTabId;
return;
}
if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
@@ -89,19 +164,25 @@
focusAdjacentItem(event.key === "ArrowUp" ? -1 : 1);
});
// A market tab can also be activated by automation or accessibility tools.
// Reveal its owning workspace before app.js dispatches the native select-tab intent.
// Reveal the selected catalog screen before app.js dispatches its native
// select-tab intent. The native state response remains authoritative and
// is reconciled through syncMarket after every render.
marketTabs.addEventListener("click", function (event) {
if (event.target.closest("button[data-tab-id]")) setWorkspace("catalog");
const button = event.target.closest("button[data-tab-id]");
if (!button || !marketTabs.contains(button)) return;
syncMarket(button.dataset.tabId);
setWorkspace("catalog");
}, true);
setNavigationExpanded(true);
setWorkspace("stock");
window.LegacyWorkspaceNavigation = Object.freeze({
select: setWorkspace,
select: select,
syncMarket: syncMarket,
setExpanded: setNavigationExpanded,
active: function () { return region.dataset.activeWorkspace; },
active: function () { return activeWorkspace; },
activeMarket: function () { return activeMarketTabId; },
expanded: function () { return toggle.getAttribute("aria-expanded") === "true"; }
});
}());