feat: add task-manager workspace layout
This commit is contained in:
@@ -8,57 +8,108 @@
|
||||
</head>
|
||||
<body>
|
||||
<main class="operator-shell" aria-label="V-Stock 증권정보송출시스템">
|
||||
<section class="left-panel" aria-label="종목 및 컷 선택">
|
||||
<header class="brand-line">
|
||||
<div class="brand"><span>매일경제TV</span><strong>VRi</strong></div>
|
||||
<div class="connection-state" id="playout-connection-state"
|
||||
aria-label="Tornado2 연결 상태">미연결</div>
|
||||
</header>
|
||||
<section id="workspace-region" class="workspace-region" aria-label="작업 영역">
|
||||
<aside id="workspace-navigation" class="workspace-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="메뉴 접기">
|
||||
<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="작업 화면">
|
||||
<button id="workspace-stock-tab" class="workspace-nav-item workspace-current"
|
||||
type="button" aria-label="종목·컷" title="종목·컷"
|
||||
aria-current="page" aria-controls="stock-workspace">
|
||||
<svg aria-hidden="true" viewBox="0 0 24 24" focusable="false">
|
||||
<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>
|
||||
</button>
|
||||
<button id="workspace-catalog-tab" class="workspace-nav-item" type="button"
|
||||
aria-label="기타 그래픽" title="기타 그래픽"
|
||||
aria-current="false" aria-controls="catalog-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>
|
||||
</svg>
|
||||
<span>기타 그래픽</span>
|
||||
</button>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div class="search-line">
|
||||
<label for="stock-search">종목</label>
|
||||
<input id="stock-search" maxlength="32767" autocomplete="off" spellcheck="false">
|
||||
<button id="stock-search-button" type="button"><span aria-hidden="true">⌕</span> 검색</button>
|
||||
</div>
|
||||
<section class="workspace-surface" aria-labelledby="workspace-title">
|
||||
<header class="workspace-header">
|
||||
<div class="brand"><span>매일경제TV</span><strong>VRi</strong></div>
|
||||
<div class="workspace-heading">
|
||||
<small>작업 영역</small>
|
||||
<h1 id="workspace-title">종목·컷</h1>
|
||||
</div>
|
||||
<div class="connection-state" id="playout-connection-state"
|
||||
aria-label="Tornado2 연결 상태">미연결</div>
|
||||
</header>
|
||||
|
||||
<div id="stock-results" class="stock-results" role="listbox" aria-label="종목명"></div>
|
||||
<div class="workspace-content">
|
||||
<section id="stock-workspace" class="left-panel workspace-view"
|
||||
aria-label="종목 및 컷 선택">
|
||||
<div class="search-line">
|
||||
<label for="stock-search">종목</label>
|
||||
<input id="stock-search" maxlength="32767" autocomplete="off" spellcheck="false">
|
||||
<button id="stock-search-button" type="button"><span aria-hidden="true">⌕</span> 검색</button>
|
||||
</div>
|
||||
|
||||
<div class="manual-actions" aria-label="수동 입력">
|
||||
<button type="button" data-manual-screen="revenue-composition">주요매출 구성</button>
|
||||
<button type="button" data-manual-screen="growth-metrics">성장성 지표</button>
|
||||
<button type="button" data-manual-screen="sales">매출액</button>
|
||||
<button type="button" data-manual-screen="operating-profit">영업이익</button>
|
||||
</div>
|
||||
<div class="manual-list-actions" aria-label="수동 송출 목록" hidden>
|
||||
<button type="button" data-manual-list-screen="individual">개인 순매도</button>
|
||||
<button type="button" data-manual-list-screen="foreign">외국인 순매도</button>
|
||||
<button type="button" data-manual-list-screen="institution">기관 순매도</button>
|
||||
<button type="button" data-manual-list-screen="vi">VI 발동</button>
|
||||
</div>
|
||||
<div id="stock-results" class="stock-results" role="listbox" aria-label="종목명"></div>
|
||||
|
||||
<div id="cut-list" class="cut-list" role="listbox" aria-label="종목 컷" aria-multiselectable="true"></div>
|
||||
<div id="operator-status" class="operator-status" aria-live="polite"></div>
|
||||
</section>
|
||||
<div class="manual-actions" aria-label="수동 입력">
|
||||
<button type="button" data-manual-screen="revenue-composition">주요매출 구성</button>
|
||||
<button type="button" data-manual-screen="growth-metrics">성장성 지표</button>
|
||||
<button type="button" data-manual-screen="sales">매출액</button>
|
||||
<button type="button" data-manual-screen="operating-profit">영업이익</button>
|
||||
</div>
|
||||
<div class="manual-list-actions" aria-label="수동 송출 목록" hidden>
|
||||
<button type="button" data-manual-list-screen="individual">개인 순매도</button>
|
||||
<button type="button" data-manual-list-screen="foreign">외국인 순매도</button>
|
||||
<button type="button" data-manual-list-screen="institution">기관 순매도</button>
|
||||
<button type="button" data-manual-list-screen="vi">VI 발동</button>
|
||||
</div>
|
||||
|
||||
<section class="catalog-panel" aria-label="분류별 그래픽">
|
||||
<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>
|
||||
<div id="cut-list" class="cut-list" role="listbox" aria-label="종목 컷"
|
||||
aria-multiselectable="true"></div>
|
||||
<div id="operator-status" class="operator-status" aria-live="polite"></div>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="playlist-panel" aria-label="플레이리스트">
|
||||
<header class="playlist-heading">
|
||||
<div class="playlist-heading-icon" aria-hidden="true">▤</div>
|
||||
<div><small>항상 표시</small><h2>송출 스케줄</h2></div>
|
||||
</header>
|
||||
<div class="playlist-toolbar">
|
||||
<label><input id="playlist-enable-all" type="checkbox" checked disabled> 전체</label>
|
||||
<label><input id="moving-average-5" type="checkbox" checked> 5일선</label>
|
||||
@@ -265,6 +316,7 @@
|
||||
<script src="legacy-cut-drag.js"></script>
|
||||
<script src="legacy-modal-focus.js"></script>
|
||||
<script src="legacy-named-playlist-selection.js"></script>
|
||||
<script src="workspace-navigation.js"></script>
|
||||
<script src="app.js"></script>
|
||||
<script src="playout-ui.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
color-scheme: light;
|
||||
font-family: "NanumGothic", "맑은 고딕", sans-serif;
|
||||
font-size: 13px;
|
||||
--workspace-nav-expanded: 210px;
|
||||
--workspace-nav-collapsed: 62px;
|
||||
--workspace-accent: #0f6cbd;
|
||||
--workspace-border: #d2d2d2;
|
||||
--workspace-nav-surface: #f2f2f2;
|
||||
background: #efefef;
|
||||
color: #111;
|
||||
}
|
||||
@@ -11,38 +16,154 @@
|
||||
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; }
|
||||
body.busy { cursor: wait; }
|
||||
body.busy .operator-shell { pointer-events: none; }
|
||||
button, input { font: inherit; }
|
||||
button, input, select { font: inherit; }
|
||||
button { border: 1px solid #a8a8a8; background: linear-gradient(#fff, #e9e9e9); color: #111; }
|
||||
button:not(:disabled) { cursor: pointer; }
|
||||
button:disabled { color: #555; opacity: 1; }
|
||||
|
||||
.operator-shell {
|
||||
display: grid;
|
||||
grid-template-columns: 430px 500px minmax(760px, 1fr);
|
||||
grid-template-columns: minmax(900px, 2fr) minmax(600px, 1fr);
|
||||
width: 100%;
|
||||
min-width: 1690px;
|
||||
min-width: 1500px;
|
||||
height: 100vh;
|
||||
background: #efefef;
|
||||
}
|
||||
|
||||
.left-panel, .catalog-panel, .playlist-panel { min-height: 0; border-right: 1px solid #bbb; }
|
||||
.left-panel { display: flex; flex-direction: column; padding: 6px 15px 4px 20px; background: #f5f5f5; }
|
||||
.workspace-region {
|
||||
--workspace-nav-width: var(--workspace-nav-expanded);
|
||||
display: grid;
|
||||
grid-template-columns: var(--workspace-nav-width) minmax(0, 1fr);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
border-right: 1px solid var(--workspace-border);
|
||||
background: #f7f7f7;
|
||||
}
|
||||
.workspace-region.nav-collapsed { --workspace-nav-width: var(--workspace-nav-collapsed); }
|
||||
.workspace-navigation {
|
||||
display: grid;
|
||||
grid-template-rows: 66px minmax(0, 1fr);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border-right: 1px solid var(--workspace-border);
|
||||
background: var(--workspace-nav-surface);
|
||||
}
|
||||
.workspace-nav-header { display: flex; align-items: center; padding: 10px; }
|
||||
.workspace-nav-toggle {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: 42px;
|
||||
height: 42px;
|
||||
align-items: center;
|
||||
gap: 13px;
|
||||
padding: 0 12px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.workspace-nav-toggle:hover { background: #e5e5e5; }
|
||||
.workspace-nav-toggle:focus-visible {
|
||||
outline: 2px solid var(--workspace-accent);
|
||||
outline-offset: -2px;
|
||||
background: #e5e5e5;
|
||||
}
|
||||
.workspace-nav-toggle svg, .workspace-nav-item svg {
|
||||
flex: 0 0 22px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.8;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.workspace-nav-toggle-label { overflow: hidden; text-overflow: ellipsis; }
|
||||
.workspace-nav-items {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
overflow: auto;
|
||||
padding: 4px 8px 12px;
|
||||
}
|
||||
.workspace-nav-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: 44px;
|
||||
height: 46px;
|
||||
flex: 0 0 46px;
|
||||
align-items: center;
|
||||
gap: 13px;
|
||||
padding: 0 12px;
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.workspace-nav-item:hover { background: #e7e7e7; }
|
||||
.workspace-nav-item:focus-visible {
|
||||
outline: 2px solid var(--workspace-accent);
|
||||
outline-offset: -2px;
|
||||
background: #e7e7e7;
|
||||
}
|
||||
.workspace-nav-item.workspace-current { background: #dedede; font-weight: 700; }
|
||||
.workspace-nav-item.workspace-current::before {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 10px;
|
||||
bottom: 10px;
|
||||
width: 3px;
|
||||
border-radius: 2px;
|
||||
background: var(--workspace-accent);
|
||||
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-item { justify-content: center; padding: 0; }
|
||||
|
||||
.brand-line { height: 68px; display: flex; align-items: center; justify-content: space-between; }
|
||||
.workspace-surface {
|
||||
display: grid;
|
||||
grid-template-rows: 66px minmax(0, 1fr);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
.workspace-header {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
padding: 8px 20px;
|
||||
border-bottom: 1px solid var(--workspace-border);
|
||||
background: #fff;
|
||||
}
|
||||
.brand { display: flex; align-items: center; gap: 12px; white-space: nowrap; }
|
||||
.brand span { color: #ed671f; font-size: 28px; font-weight: 800; letter-spacing: -2px; }
|
||||
.brand strong { font: italic 900 38px Arial, sans-serif; letter-spacing: -5px; }
|
||||
.connection-state { padding: 10px 12px; background: #c40000; color: white; font: 700 18px Consolas, monospace; }
|
||||
.brand span { color: #ed671f; font-size: 20px; font-weight: 800; letter-spacing: -1px; }
|
||||
.brand strong { font: italic 900 29px Arial, sans-serif; letter-spacing: -4px; }
|
||||
.workspace-heading { min-width: 0; }
|
||||
.workspace-heading small { display: block; color: #666; font-size: 11px; }
|
||||
.workspace-heading h1 { margin: 1px 0 0; overflow: hidden; font-size: 21px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.connection-state { padding: 8px 10px; border-radius: 4px; background: #c40000; color: white; font: 700 14px Consolas, monospace; }
|
||||
.connection-state.connected { background: #176c32; }
|
||||
.connection-state.outcome-unknown { background: #8a4d00; }
|
||||
.workspace-content { position: relative; min-width: 0; min-height: 0; overflow: hidden; }
|
||||
.workspace-view { width: 100%; height: 100%; min-width: 0; min-height: 0; }
|
||||
.left-panel, .catalog-panel, .playlist-panel { min-height: 0; }
|
||||
.left-panel { display: flex; flex-direction: column; padding: 16px 20px 12px; background: #f7f7f7; }
|
||||
|
||||
.search-line { display: grid; grid-template-columns: 40px 162px 1fr 117px; align-items: center; gap: 3px; height: 38px; }
|
||||
.search-line { display: grid; grid-template-columns: 44px minmax(220px, 460px) 112px; align-items: center; justify-content: start; gap: 7px; min-height: 42px; }
|
||||
.search-line label { font-weight: 700; }
|
||||
.search-line input { grid-column: 2; height: 27px; padding: 2px 7px; border: 1px solid #888; font-size: 16px; }
|
||||
.search-line button { grid-column: 4; height: 31px; font-size: 14px; }
|
||||
.search-line button span { font-size: 21px; margin-right: 20px; }
|
||||
.search-line input { grid-column: 2; height: 31px; padding: 2px 8px; border: 1px solid #888; font-size: 16px; }
|
||||
.search-line button { grid-column: 3; height: 31px; font-size: 14px; }
|
||||
.search-line button span { font-size: 19px; margin-right: 12px; }
|
||||
|
||||
.stock-results { height: 174px; border: 1px solid #999; background: #fff; overflow-y: auto; }
|
||||
.stock-results { height: 190px; border: 1px solid #999; background: #fff; overflow-y: auto; }
|
||||
.stock-result { display: block; width: 100%; height: 17px; padding: 0 5px; border: 0; background: #fff; text-align: left; font-size: 16px; line-height: 17px; }
|
||||
.stock-result.selected { background: #696969; color: #fff; }
|
||||
|
||||
@@ -153,15 +274,15 @@ button:disabled { color: #555; opacity: 1; }
|
||||
.cut-drag-feedback.allowed .cut-drag-feedback-icon::before { content: "+"; }
|
||||
.operator-status { min-height: 18px; padding-top: 2px; color: #a40000; font-size: 11px; }
|
||||
|
||||
.catalog-panel { display: flex; flex-direction: column; padding: 0 8px; background: #f3f3f3; }
|
||||
.market-tabs { display: flex; height: 29px; border-bottom: 1px solid #999; }
|
||||
.market-tabs button { min-width: 48px; padding: 3px 8px; border-width: 0 1px 0 0; background: #efefef; }
|
||||
.market-tabs button.active { background: #fff; border-top: 2px solid #e8781d; }
|
||||
.catalog-panel { display: flex; flex-direction: column; padding: 14px 16px 12px; background: #f7f7f7; }
|
||||
.market-tabs { display: flex; min-height: 34px; border-bottom: 1px solid #999; }
|
||||
.market-tabs button { min-width: 54px; padding: 4px 10px; border-width: 0 1px 0 0; background: #efefef; }
|
||||
.market-tabs button.active { background: #fff; border-top: 2px solid var(--workspace-accent); font-weight: 700; }
|
||||
.market-tabs button[draggable="true"] { cursor: grab; }
|
||||
.market-tabs button.dragging { opacity: .58; cursor: grabbing; }
|
||||
.expand-line { height: 31px; padding: 7px 8px 0; }
|
||||
.expand-line { height: 34px; padding: 8px 8px 0; }
|
||||
.category-title { height: 27px; border-bottom: 1px solid #777; font-size: 12px; }
|
||||
.category-tree { flex: 1; overflow: auto; padding: 7px 12px; background: #fff; border-left: 1px solid #aaa; border-right: 1px solid #aaa; }
|
||||
.category-tree { flex: 1; overflow: auto; padding: 10px 14px; background: #fff; border: 1px solid #aaa; border-top: 0; }
|
||||
.tree-root { margin: 5px 0 2px; color: #168000; font-size: 16px; }
|
||||
.tree-root.second { margin-top: 15px; }
|
||||
.catalog-section > summary { margin: 5px 0 2px; color: #168000; font-size: 16px; cursor: default; user-select: none; }
|
||||
@@ -317,16 +438,53 @@ summary[data-tree-root][aria-disabled="true"] { color: #777; }
|
||||
.operator-catalog-editor-actions { display: grid; grid-template-columns: auto auto 1fr auto auto; gap: 7px; padding: 2px 9px 0; }
|
||||
.operator-catalog-editor-actions button:last-child { min-width: 92px; color: #fff; background: #176b2d; font-weight: 700; }
|
||||
|
||||
.playlist-panel { display: grid; grid-template-rows: 58px minmax(0, 1fr) 76px 42px; padding: 0 8px; background: #efefef; }
|
||||
.playlist-toolbar { display: flex; align-items: center; gap: 5px; white-space: nowrap; }
|
||||
.playlist-toolbar button { height: 25px; padding: 2px 7px; }
|
||||
.playlist-toolbar .toolbar-spacer { flex: 1; }
|
||||
.background-name { width: 82px; height: 25px; border: 1px solid #888; background: #fff; }
|
||||
.playlist-panel {
|
||||
display: grid;
|
||||
grid-template-rows: 66px minmax(84px, auto) minmax(0, 1fr) 76px 42px;
|
||||
min-width: 0;
|
||||
padding: 0 8px;
|
||||
border-left: 1px solid var(--workspace-border);
|
||||
background: #efefef;
|
||||
}
|
||||
.playlist-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
margin: 0 -8px;
|
||||
padding: 8px 14px;
|
||||
border-bottom: 1px solid var(--workspace-border);
|
||||
background: #fff;
|
||||
}
|
||||
.playlist-heading-icon {
|
||||
display: grid;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
place-items: center;
|
||||
border-radius: 7px;
|
||||
background: #e6f1fb;
|
||||
color: var(--workspace-accent);
|
||||
font-size: 22px;
|
||||
}
|
||||
.playlist-heading small { display: block; color: #666; font-size: 11px; }
|
||||
.playlist-heading h2 { margin: 1px 0 0; font-size: 19px; }
|
||||
.playlist-toolbar {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
overflow: auto;
|
||||
flex-wrap: wrap;
|
||||
padding: 6px 0;
|
||||
}
|
||||
.playlist-toolbar label { white-space: nowrap; }
|
||||
.playlist-toolbar button { height: 25px; padding: 2px 6px; white-space: nowrap; }
|
||||
.playlist-toolbar .toolbar-spacer { display: none; }
|
||||
.background-name { width: 76px; height: 25px; border: 1px solid #888; background: #fff; }
|
||||
.fade-control { margin-left: 2px; }
|
||||
#playout-fade-duration { width: 48px; height: 25px; }
|
||||
|
||||
.playlist-grid { min-height: 0; border: 1px solid #888; background: #d5d5d5; overflow: auto; }
|
||||
.playlist-row { display: grid; grid-template-columns: 30px 40px 100px 260px 200px minmax(255px, 1fr) 52px; min-width: 937px; }
|
||||
.playlist-row { display: grid; grid-template-columns: 30px 40px 100px minmax(115px, 1.1fr) minmax(95px, .8fr) minmax(138px, 1.3fr) 52px; min-width: 600px; }
|
||||
.playlist-header { position: sticky; top: 0; z-index: 2; height: 29px; background: #efefef; border-bottom: 1px solid #999; }
|
||||
.playlist-header > div { display: flex; align-items: center; justify-content: center; border-right: 1px solid #aaa; }
|
||||
.playlist-data-row { min-height: 25px; background: #fff; border-bottom: 1px solid #ddd; }
|
||||
@@ -343,12 +501,16 @@ summary[data-tree-root][aria-disabled="true"] { color: #777; }
|
||||
.playlist-data-row.drag-before { box-shadow: inset 0 3px #176b2d; }
|
||||
.playlist-data-row.drag-after { box-shadow: inset 0 -3px #176b2d; }
|
||||
|
||||
.playout-actions { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: center; gap: 12px; }
|
||||
.playout-actions button { height: 51px; background: #fff; font-size: 25px; font-weight: 700; }
|
||||
.playout-actions { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); align-items: center; gap: 7px; }
|
||||
.playout-actions button { height: 51px; padding: 2px 4px; background: #fff; font-size: 20px; font-weight: 700; white-space: nowrap; }
|
||||
.playout-actions button:disabled { color: #888; background: #f7f7f7; }
|
||||
.playout-actions .prepare { font-size: 21px; }
|
||||
.playout-actions .prepare.active { color: #fff; background: #1a7700; }
|
||||
.playout-status { border-top: 1px solid #bbb; display: flex; align-items: center; justify-content: center; color: #666; }
|
||||
.playout-status { border-top: 1px solid #bbb; display: flex; align-items: center; justify-content: center; overflow: hidden; color: #666; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.workspace-region { scroll-behavior: auto; }
|
||||
}
|
||||
|
||||
.dialog-backdrop { position: fixed; inset: 0; z-index: 30; background: rgba(0, 0, 0, .25); }
|
||||
.dialog-backdrop[hidden] { display: none; }
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const region = document.getElementById("workspace-region");
|
||||
const navigation = document.getElementById("workspace-navigation");
|
||||
const navigationItems = document.getElementById("workspace-nav-items");
|
||||
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 stockWorkspace = document.getElementById("stock-workspace");
|
||||
const catalogWorkspace = document.getElementById("catalog-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) {
|
||||
return;
|
||||
}
|
||||
|
||||
const views = Object.freeze({
|
||||
stock: Object.freeze({ tab: stockTab, view: stockWorkspace, title: "종목·컷" }),
|
||||
catalog: Object.freeze({ tab: catalogTab, view: catalogWorkspace, title: "기타 그래픽" })
|
||||
});
|
||||
|
||||
function setNavigationExpanded(expanded) {
|
||||
const isExpanded = expanded === true;
|
||||
region.classList.toggle("nav-collapsed", !isExpanded);
|
||||
toggle.setAttribute("aria-expanded", isExpanded ? "true" : "false");
|
||||
toggle.setAttribute("aria-label", isExpanded ? "작업 메뉴 접기" : "작업 메뉴 펼치기");
|
||||
toggle.title = isExpanded ? "메뉴 접기" : "메뉴 펼치기";
|
||||
toggleLabel.textContent = isExpanded ? "메뉴 접기" : "메뉴 펼치기";
|
||||
}
|
||||
|
||||
function setWorkspace(name) {
|
||||
const selected = views[name];
|
||||
if (!selected) return false;
|
||||
|
||||
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();
|
||||
|
||||
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;
|
||||
return true;
|
||||
}
|
||||
|
||||
function focusAdjacentItem(direction) {
|
||||
const items = [stockTab, catalogTab].filter(function (button) {
|
||||
return button.disabled !== true;
|
||||
});
|
||||
const currentIndex = items.indexOf(document.activeElement);
|
||||
const nextIndex = currentIndex < 0
|
||||
? 0
|
||||
: (currentIndex + direction + items.length) % items.length;
|
||||
items[nextIndex].focus();
|
||||
}
|
||||
|
||||
toggle.addEventListener("click", function () {
|
||||
setNavigationExpanded(toggle.getAttribute("aria-expanded") !== "true");
|
||||
});
|
||||
stockTab.addEventListener("click", function () { setWorkspace("stock"); });
|
||||
catalogTab.addEventListener("click", function () { setWorkspace("catalog"); });
|
||||
|
||||
navigationItems.addEventListener("keydown", function (event) {
|
||||
const menuButton = event.target.closest && event.target.closest(".workspace-nav-item");
|
||||
if ((event.key === "Enter" || event.key === " ") && menuButton) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setWorkspace(menuButton === stockTab ? "stock" : "catalog");
|
||||
return;
|
||||
}
|
||||
if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
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.
|
||||
marketTabs.addEventListener("click", function (event) {
|
||||
if (event.target.closest("button[data-tab-id]")) setWorkspace("catalog");
|
||||
}, true);
|
||||
|
||||
setNavigationExpanded(true);
|
||||
setWorkspace("stock");
|
||||
|
||||
window.LegacyWorkspaceNavigation = Object.freeze({
|
||||
select: setWorkspace,
|
||||
setExpanded: setNavigationExpanded,
|
||||
active: function () { return region.dataset.activeWorkspace; },
|
||||
expanded: function () { return toggle.getAttribute("aria-expanded") === "true"; }
|
||||
});
|
||||
}());
|
||||
Reference in New Issue
Block a user