feat: expose graphic screens in sidebar
This commit is contained in:
@@ -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; }));
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"; }
|
||||
});
|
||||
}());
|
||||
|
||||
Reference in New Issue
Block a user