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

@@ -1374,12 +1374,23 @@
if (!button) return;
const buttonAtPosition = marketTabs.children.item(position);
if (buttonAtPosition !== button) marketTabs.insertBefore(button, buttonAtPosition);
button.textContent = tab.label;
const label = button.querySelector(".workspace-nav-label");
if (label) label.textContent = tab.label;
else button.textContent = tab.label;
button.setAttribute("aria-label", tab.label);
button.title = tab.label;
button.classList.toggle("active", tab.isActive === true);
button.setAttribute("aria-current", tab.isActive === true ? "page" : "false");
button.disabled = state.isBusy === true || state.fixedSectionBatch != null;
button.draggable = state.isBusy !== true && state.fixedSectionBatch == null;
});
const activeTab = state.tabs.find(function (tab) { return tab.isActive === true; });
// Keep the clicked menu highlighted while native tab loading is in flight.
// The busy snapshot still carries the previous active tab; only reconcile
// once the authoritative operation has completed.
if (activeTab && state.isBusy !== true &&
window.LegacyWorkspaceNavigation?.syncMarket) {
window.LegacyWorkspaceNavigation.syncMarket(activeTab.id);
}
acknowledgePendingTabHoverSwap(
state.tabs.map(function (tab) { return tab.id; }));
}

View File

@@ -9,17 +9,20 @@
<body>
<main class="operator-shell" aria-label="V-Stock 증권정보송출시스템">
<section id="workspace-region" class="workspace-region" aria-label="작업 영역">
<aside id="workspace-navigation" class="workspace-navigation" aria-label="작업 메뉴">
<aside id="workspace-navigation" class="workspace-navigation" role="navigation"
aria-label="작업 메뉴">
<div class="workspace-nav-header">
<button id="workspace-nav-toggle" class="workspace-nav-toggle" type="button"
aria-expanded="true" aria-controls="workspace-nav-items" title="메뉴 접기">
aria-expanded="true"
aria-controls="workspace-nav-items workspace-nav-footer" title="메뉴 접기">
<svg aria-hidden="true" viewBox="0 0 24 24" focusable="false">
<path d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
<span class="workspace-nav-toggle-label">메뉴 접기</span>
</button>
</div>
<nav id="workspace-nav-items" class="workspace-nav-items" aria-label="작업 화면">
<div id="workspace-nav-items" class="workspace-nav-items" role="group"
aria-label="작업 화면">
<button id="workspace-stock-tab" class="workspace-nav-item workspace-current"
type="button" aria-label="종목·컷" title="종목·컷"
aria-current="page" aria-controls="stock-workspace">
@@ -27,20 +30,104 @@
<circle cx="10" cy="10" r="5"></circle>
<path d="m14 14 5 5M4 20h16M6 17l3-3 3 2 5-6"></path>
</svg>
<span>종목·컷</span>
<span class="workspace-nav-label">종목·컷</span>
</button>
<button id="workspace-catalog-tab" class="workspace-nav-item" type="button"
aria-label="기타 그래픽" title="기타 그래픽"
aria-current="false" aria-controls="catalog-workspace">
<div class="workspace-nav-section-label"><span>기타 그래픽</span></div>
<div id="market-tabs" class="market-tabs workspace-market-nav" role="group"
aria-label="기타 그래픽">
<button class="workspace-nav-item workspace-market-item active" type="button"
draggable="true" data-tab-id="overseas" aria-label="해외" title="해외"
aria-current="false" aria-controls="catalog-workspace">
<svg aria-hidden="true" viewBox="0 0 24 24" focusable="false">
<circle cx="12" cy="12" r="8"></circle><path d="M4 12h16M12 4c2.5 2.2 3.8 4.9 3.8 8s-1.3 5.8-3.8 8c-2.5-2.2-3.8-4.9-3.8-8S9.5 6.2 12 4Z"></path>
</svg>
<span class="workspace-nav-label">해외</span>
</button>
<button class="workspace-nav-item workspace-market-item" type="button"
draggable="true" data-tab-id="exchange" aria-label="환율" title="환율"
aria-current="false" aria-controls="catalog-workspace">
<svg aria-hidden="true" viewBox="0 0 24 24" focusable="false">
<path d="M5 8h13l-3-3M19 16H6l3 3"></path>
</svg>
<span class="workspace-nav-label">환율</span>
</button>
<button class="workspace-nav-item workspace-market-item" type="button"
draggable="true" data-tab-id="index" aria-label="지수" title="지수"
aria-current="false" aria-controls="catalog-workspace">
<svg aria-hidden="true" viewBox="0 0 24 24" focusable="false">
<path d="M4 19h16M5 16l4-5 3 2 6-8"></path><path d="m14 5 4 0 0 4"></path>
</svg>
<span class="workspace-nav-label">지수</span>
</button>
<button class="workspace-nav-item workspace-market-item" type="button"
draggable="true" data-tab-id="kospiIndustry" aria-label="코스피" title="코스피"
aria-current="false" aria-controls="catalog-workspace">
<svg aria-hidden="true" viewBox="0 0 24 24" focusable="false">
<path d="M5 19V9h3v10M11 19V5h3v14M17 19v-7h3v7M4 19h17"></path>
</svg>
<span class="workspace-nav-label">코스피</span>
</button>
<button class="workspace-nav-item workspace-market-item" type="button"
draggable="true" data-tab-id="kosdaqIndustry" aria-label="코스닥" title="코스닥"
aria-current="false" aria-controls="catalog-workspace">
<svg aria-hidden="true" viewBox="0 0 24 24" focusable="false">
<path d="M6 5v14M4 9h4M12 4v16M10 14h4M18 6v13M16 10h4"></path>
</svg>
<span class="workspace-nav-label">코스닥</span>
</button>
<button class="workspace-nav-item workspace-market-item" type="button"
draggable="true" data-tab-id="comparison" aria-label="비교" title="비교"
aria-current="false" aria-controls="catalog-workspace">
<svg aria-hidden="true" viewBox="0 0 24 24" focusable="false">
<path d="M5 7h14M5 17h14M8 4 5 7l3 3M16 14l3 3-3 3"></path>
</svg>
<span class="workspace-nav-label">비교</span>
</button>
<button class="workspace-nav-item workspace-market-item" type="button"
draggable="true" data-tab-id="theme" aria-label="테마" title="테마"
aria-current="false" aria-controls="catalog-workspace">
<svg aria-hidden="true" viewBox="0 0 24 24" focusable="false">
<path d="m12 4 2.2 4.5 5 .7-3.6 3.5.9 5-4.5-2.4-4.5 2.4.9-5-3.6-3.5 5-.7L12 4Z"></path>
</svg>
<span class="workspace-nav-label">테마</span>
</button>
<button class="workspace-nav-item workspace-market-item" type="button"
draggable="true" data-tab-id="overseasStocks" aria-label="해외종목" title="해외종목"
aria-current="false" aria-controls="catalog-workspace">
<svg aria-hidden="true" viewBox="0 0 24 24" focusable="false">
<circle cx="8" cy="9" r="4"></circle><path d="M3 19c.7-3 2.3-5 5-5s4.3 2 5 5M15 7h6M18 4v6M15 14h6M18 11v6"></path>
</svg>
<span class="workspace-nav-label">해외종목</span>
</button>
<button class="workspace-nav-item workspace-market-item" type="button"
draggable="true" data-tab-id="expert" aria-label="전문가" title="전문가"
aria-current="false" aria-controls="catalog-workspace">
<svg aria-hidden="true" viewBox="0 0 24 24" focusable="false">
<circle cx="12" cy="8" r="4"></circle><path d="M5 20c.8-4 3.1-6 7-6s6.2 2 7 6M17 5l2-2M7 5 5-2"></path>
</svg>
<span class="workspace-nav-label">전문가</span>
</button>
<button class="workspace-nav-item workspace-market-item" type="button"
draggable="true" data-tab-id="tradingHalt" aria-label="정지" title="정지"
aria-current="false" aria-controls="catalog-workspace">
<svg aria-hidden="true" viewBox="0 0 24 24" focusable="false">
<circle cx="12" cy="12" r="8"></circle><path d="M9.5 8v8M14.5 8v8"></path>
</svg>
<span class="workspace-nav-label">정지</span>
</button>
</div>
</div>
<div id="workspace-nav-footer" class="workspace-nav-footer">
<button id="workspace-settings-tab" class="workspace-nav-item" type="button"
aria-label="설정" title="설정" aria-current="false"
aria-controls="settings-workspace">
<svg aria-hidden="true" viewBox="0 0 24 24" focusable="false">
<rect x="4" y="4" width="6" height="6" rx="1"></rect>
<rect x="14" y="4" width="6" height="6" rx="1"></rect>
<rect x="4" y="14" width="6" height="6" rx="1"></rect>
<rect x="14" y="14" width="6" height="6" rx="1"></rect>
<circle cx="12" cy="12" r="3"></circle>
<path d="M19 12a7 7 0 0 0-.1-1l2-1.5-2-3.4-2.4 1A8 8 0 0 0 15 6.2L14.7 4h-4l-.3 2.2a8 8 0 0 0-1.5.9l-2.4-1-2 3.4 2 1.5a7 7 0 0 0 0 2l-2 1.5 2 3.4 2.4-1a8 8 0 0 0 1.5.9l.3 2.2h4l.3-2.2a8 8 0 0 0 1.5-.9l2.4 1 2-3.4-2-1.5a7 7 0 0 0 .1-1Z"></path>
</svg>
<span>기타 그래픽</span>
<span class="workspace-nav-label">설정</span>
</button>
</nav>
</div>
</aside>
<section class="workspace-surface" aria-labelledby="workspace-title">
@@ -91,22 +178,49 @@
<section id="catalog-workspace" class="catalog-panel workspace-view"
aria-label="분류별 그래픽" hidden inert>
<nav id="market-tabs" class="market-tabs" aria-label="시장 분류">
<button class="active" type="button" draggable="true" data-tab-id="overseas">해외</button>
<button type="button" draggable="true" data-tab-id="exchange">환율</button>
<button type="button" draggable="true" data-tab-id="index">지수</button>
<button type="button" draggable="true" data-tab-id="kospiIndustry">코스피</button>
<button type="button" draggable="true" data-tab-id="kosdaqIndustry">코스닥</button>
<button type="button" draggable="true" data-tab-id="comparison">비교</button>
<button type="button" draggable="true" data-tab-id="theme">테마</button>
<button type="button" draggable="true" data-tab-id="overseasStocks">해외종목</button>
<button type="button" draggable="true" data-tab-id="expert">전문가</button>
<button type="button" draggable="true" data-tab-id="tradingHalt">정지</button>
</nav>
<label class="expand-line"><input id="catalog-expand-all" type="checkbox" checked> Expand</label>
<div class="category-title">Category</div>
<div id="category-tree" class="category-tree" aria-label="원본 카테고리"></div>
</section>
<section id="settings-workspace" class="settings-panel workspace-view"
aria-label="설정" hidden inert>
<div class="settings-intro">
<div class="settings-intro-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" focusable="false">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19 12a7 7 0 0 0-.1-1l2-1.5-2-3.4-2.4 1A8 8 0 0 0 15 6.2L14.7 4h-4l-.3 2.2a8 8 0 0 0-1.5.9l-2.4-1-2 3.4 2 1.5a7 7 0 0 0 0 2l-2 1.5 2 3.4 2.4-1a8 8 0 0 0 1.5.9l.3 2.2h4l.3-2.2a8 8 0 0 0 1.5-.9l2.4 1 2-3.4-2-1.5a7 7 0 0 0 .1-1Z"></path>
</svg>
</div>
<div>
<h2>운영 설정</h2>
<p>화면 구성과 운영 단축키를 확인합니다. 송출 대상과 데이터베이스 연결은 검증된 로컬 설정을 그대로 사용합니다.</p>
</div>
</div>
<div class="settings-grid">
<article class="settings-card">
<h3>화면 구성</h3>
<dl>
<div><dt>왼쪽 메뉴</dt><dd>상단 메뉴 버튼으로 접기·펼치기</dd></div>
<div><dt>작업 화면</dt><dd>선택한 종목 또는 그래픽 작업 표시</dd></div>
<div><dt>송출 스케줄</dt><dd>오른쪽에 항상 고정 표시</dd></div>
</dl>
</article>
<article class="settings-card">
<h3>운영 단축키</h3>
<dl class="settings-shortcuts">
<div><dt><kbd>F2</kbd></dt><dd>배경 파일</dd></div>
<div><dt><kbd>F3</kbd></dt><dd>배경 없음</dd></div>
<div><dt><kbd>F8</kbd></dt><dd>TAKE IN</dd></div>
<div><dt><kbd>Esc</kbd></dt><dd>TAKE OUT</dd></div>
</dl>
</article>
<article class="settings-card settings-card-wide">
<h3>보호되는 로컬 설정</h3>
<p>DB 자격 증명, Tornado2 송출 대상, 자산 경로와 Live/DryRun 권한은 이 화면에서 변경하지 않습니다. 현재 PC에 검증된 운영 프로필이 계속 적용됩니다.</p>
</article>
</div>
</section>
</div>
</section>
</section>

View File

@@ -42,7 +42,7 @@ button:disabled { color: #555; opacity: 1; }
.workspace-region.nav-collapsed { --workspace-nav-width: var(--workspace-nav-collapsed); }
.workspace-navigation {
display: grid;
grid-template-rows: 66px minmax(0, 1fr);
grid-template-rows: 66px minmax(0, 1fr) auto;
min-width: 0;
min-height: 0;
overflow: hidden;
@@ -87,8 +87,22 @@ button:disabled { color: #555; opacity: 1; }
flex-direction: column;
gap: 4px;
overflow: auto;
padding: 4px 8px 12px;
padding: 4px 8px 8px;
}
.workspace-nav-section-label {
display: flex;
height: 26px;
flex: 0 0 26px;
align-items: center;
padding: 4px 12px 2px;
overflow: hidden;
color: #666;
font-size: 10px;
font-weight: 700;
letter-spacing: .08em;
white-space: nowrap;
}
.workspace-nav-footer { padding: 8px; border-top: 1px solid var(--workspace-border); }
.workspace-nav-item {
position: relative;
display: flex;
@@ -124,8 +138,17 @@ button:disabled { color: #555; opacity: 1; }
content: "";
}
.nav-collapsed .workspace-nav-toggle { justify-content: center; padding: 0; }
.nav-collapsed .workspace-nav-toggle-label, .nav-collapsed .workspace-nav-item span { display: none; }
.nav-collapsed .workspace-nav-toggle-label,
.nav-collapsed .workspace-nav-item span,
.nav-collapsed .workspace-nav-section-label span { display: none; }
.nav-collapsed .workspace-nav-item { justify-content: center; padding: 0; }
.nav-collapsed .workspace-nav-section-label {
height: 12px;
flex-basis: 12px;
margin: 0 7px;
padding: 0;
border-top: 1px solid var(--workspace-border);
}
.workspace-surface {
display: grid;
@@ -1224,6 +1247,166 @@ summary[data-tree-root]:focus-visible {
outline-offset: -3px;
}
/* Sidebar catalog navigation and its read-only settings landing. */
.workspace-nav-section-label {
color: var(--ui-text-subtle);
text-transform: uppercase;
}
.workspace-nav-footer {
border-top-color: var(--ui-border);
background: rgba(255, 255, 255, .42);
}
.workspace-navigation .market-tabs {
display: flex;
min-height: 0;
flex: 0 0 auto;
flex-direction: column;
gap: 4px;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
}
.workspace-navigation .market-tabs button {
position: relative;
display: flex;
width: 100%;
min-width: 44px;
height: 46px;
flex: 0 0 46px;
align-items: center;
justify-content: flex-start;
gap: 13px;
padding: 0 12px;
overflow: hidden;
border: 0;
border-radius: 6px;
background: transparent;
box-shadow: none;
color: #445066;
font-weight: 600;
text-align: left;
white-space: nowrap;
}
.workspace-navigation .market-tabs button.active:not(.workspace-current) {
background: transparent;
box-shadow: none;
color: #445066;
font-weight: 600;
}
.workspace-navigation .market-tabs button:not(:disabled):hover {
background: #edf1f6;
color: var(--ui-text);
}
.workspace-navigation .market-tabs button:focus-visible {
outline: 2px solid var(--ui-accent);
outline-offset: -2px;
background: var(--ui-accent-soft);
}
.workspace-navigation .market-tabs button.workspace-current,
.workspace-navigation .market-tabs button.active.workspace-current {
background: var(--ui-accent-soft);
box-shadow: none;
color: #174ea6;
font-weight: 700;
}
.workspace-navigation .market-tabs button.workspace-current::before {
position: absolute;
left: 0;
top: 9px;
bottom: 9px;
width: 4px;
border-radius: 0 4px 4px 0;
background: var(--ui-accent);
content: "";
}
.workspace-navigation .market-tabs button[draggable="true"] { cursor: grab; }
.workspace-navigation .market-tabs button.dragging { opacity: .58; cursor: grabbing; }
.nav-collapsed .workspace-navigation .market-tabs button {
justify-content: center;
padding: 0;
}
.settings-panel {
overflow: auto;
padding: 24px;
background: var(--ui-canvas);
}
.settings-intro {
display: flex;
max-width: 900px;
align-items: center;
gap: 16px;
padding: 20px;
border: 1px solid #c9d8ee;
border-radius: var(--ui-radius-lg);
background: linear-gradient(135deg, #f8fbff, #eef4ff);
box-shadow: var(--ui-shadow-sm);
}
.settings-intro-icon {
display: grid;
width: 48px;
height: 48px;
flex: 0 0 48px;
place-items: center;
border-radius: 13px;
background: var(--ui-accent);
color: #fff;
box-shadow: 0 8px 20px rgba(37, 99, 235, .2);
}
.settings-intro-icon svg {
width: 25px;
height: 25px;
fill: none;
stroke: currentColor;
stroke-width: 1.7;
stroke-linecap: round;
stroke-linejoin: round;
}
.settings-intro h2 { margin: 0 0 5px; font-size: 20px; }
.settings-intro p { margin: 0; color: var(--ui-text-muted); line-height: 1.55; }
.settings-grid {
display: grid;
max-width: 900px;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
margin-top: 16px;
}
.settings-card {
padding: 18px;
border: 1px solid var(--ui-border);
border-radius: var(--ui-radius);
background: var(--ui-surface);
box-shadow: var(--ui-shadow-card);
}
.settings-card-wide { grid-column: 1 / -1; }
.settings-card h3 { margin: 0 0 13px; font-size: 15px; }
.settings-card p { margin: 0; color: var(--ui-text-muted); line-height: 1.6; }
.settings-card dl { margin: 0; }
.settings-card dl > div {
display: grid;
grid-template-columns: minmax(90px, .42fr) minmax(0, 1fr);
gap: 12px;
padding: 9px 0;
border-top: 1px solid #edf0f4;
}
.settings-card dl > div:first-child { padding-top: 0; border-top: 0; }
.settings-card dl > div:last-child { padding-bottom: 0; }
.settings-card dt { color: #344054; font-weight: 700; }
.settings-card dd { margin: 0; color: var(--ui-text-muted); }
.settings-shortcuts kbd {
display: inline-block;
min-width: 40px;
padding: 3px 7px;
border: 1px solid #b8c4d4;
border-bottom-width: 2px;
border-radius: 6px;
background: var(--ui-surface-subtle);
color: #344054;
font: 700 11px "Segoe UI", sans-serif;
text-align: center;
}
@media (forced-colors: active) {
* { scrollbar-color: auto; }
button:focus-visible,

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"; }
});
}());