@@ -151,9 +162,12 @@
-
-
-
+
+
+
송출 상태를 확인하고 있습니다.
diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/playout-ui.js b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/playout-ui.js
index 1127fe8..336349b 100644
--- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/playout-ui.js
+++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/playout-ui.js
@@ -85,6 +85,7 @@
if (!playout) {
connection.textContent = "미연결";
status.textContent = "송출 엔진 상태를 사용할 수 없습니다.";
+ status.title = status.textContent;
setAllDisabled(true);
return;
}
@@ -102,6 +103,7 @@
connection.textContent = connectionLabel(playout);
connection.classList.toggle("connected", playout.isConnected === true);
connection.classList.toggle("outcome-unknown", playout.outcomeUnknown === true);
+ connection.classList.toggle("dry-run", playout.mode === "dryRun");
prepare.classList.toggle("active", playout.phase !== "idle");
prepare.disabled = busy || !commandAvailable || !canPrepareToggle ||
playout.isPlayCompletionPending === true;
@@ -132,6 +134,7 @@
status.textContent = phaseLabel(playout.phase) + sceneLabel +
pageLabel(playout) + nextLabel(playout) + refresh +
(playout.message ? " · " + playout.message : "");
+ status.title = status.textContent;
}
prepare.addEventListener("click", function () {
@@ -188,5 +191,6 @@
} else {
setAllDisabled(true);
status.textContent = "WebView2 송출 브리지를 사용할 수 없습니다.";
+ status.title = status.textContent;
}
}());
diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/styles.css b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/styles.css
index 3c4c097..b711855 100644
--- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/styles.css
+++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/styles.css
@@ -518,3 +518,733 @@ summary[data-tree-root][aria-disabled="true"] { color: #777; }
.legacy-dialog > strong { display: block; margin-bottom: 18px; }
.legacy-dialog-actions { display: flex; justify-content: center; gap: 8px; margin-top: 24px; }
.legacy-dialog button { min-width: 80px; padding: 6px 15px; }
+
+/* Modern operator theme: visual-only overrides; geometry and native contracts stay intact. */
+:root {
+ --workspace-accent: #2563eb;
+ --workspace-border: #d9e0ea;
+ --workspace-nav-surface: #f7f9fc;
+ --ui-canvas: #eef2f6;
+ --ui-surface: #ffffff;
+ --ui-surface-subtle: #f8fafc;
+ --ui-surface-muted: #f1f4f8;
+ --ui-surface-hover: #edf3fb;
+ --ui-border: #d8e0ea;
+ --ui-border-strong: #778397;
+ --ui-text: #172033;
+ --ui-text-muted: #667085;
+ --ui-text-subtle: #8490a3;
+ --ui-accent: #2563eb;
+ --ui-accent-hover: #1d4ed8;
+ --ui-accent-soft: #eaf1ff;
+ --ui-success: #16834a;
+ --ui-success-soft: #e9f8ef;
+ --ui-danger: #c9364f;
+ --ui-danger-soft: #fff0f3;
+ --ui-warning: #a96300;
+ --ui-warning-soft: #fff5df;
+ --ui-radius-sm: 7px;
+ --ui-radius: 10px;
+ --ui-radius-lg: 14px;
+ --ui-shadow-sm: 0 1px 2px rgba(15, 23, 42, .06);
+ --ui-shadow-card: 0 1px 3px rgba(15, 23, 42, .08), 0 8px 24px rgba(15, 23, 42, .04);
+ --ui-shadow-dialog: 0 24px 70px rgba(15, 23, 42, .24), 0 4px 18px rgba(15, 23, 42, .12);
+ font-family: "Segoe UI Variable", "Segoe UI", "Noto Sans KR", "맑은 고딕", sans-serif;
+ background: var(--ui-canvas);
+ color: var(--ui-text);
+}
+
+body {
+ background: var(--ui-canvas);
+ color: var(--ui-text);
+ line-height: 1.35;
+ letter-spacing: -.01em;
+ text-rendering: optimizeLegibility;
+}
+
+* {
+ scrollbar-color: #9eabbc transparent;
+ scrollbar-width: auto;
+}
+*::-webkit-scrollbar { width: 12px; height: 12px; }
+*::-webkit-scrollbar-track { background: transparent; }
+*::-webkit-scrollbar-thumb {
+ border: 3px solid transparent;
+ border-radius: 999px;
+ background: #b8c3d1;
+ background-clip: padding-box;
+}
+*::-webkit-scrollbar-thumb:hover { background-color: #96a4b6; }
+::selection { background: #cfe0ff; color: var(--ui-text); }
+
+button {
+ border: 1px solid var(--ui-border-strong);
+ border-radius: var(--ui-radius-sm);
+ background: var(--ui-surface);
+ background-image: none;
+ box-shadow: var(--ui-shadow-sm);
+ color: var(--ui-text);
+ font-weight: 600;
+ transition: background-color 120ms ease, border-color 120ms ease,
+ box-shadow 120ms ease, color 120ms ease;
+}
+button:not(:disabled):hover {
+ border-color: #9daabc;
+ background: var(--ui-surface-hover);
+}
+button:not(:disabled):active {
+ border-color: var(--ui-accent);
+ background: var(--ui-accent-soft);
+}
+button:focus-visible,
+input:focus-visible,
+select:focus-visible {
+ outline: 2px solid #fff;
+ outline-offset: 0;
+ box-shadow: 0 0 0 4px var(--ui-accent);
+}
+button:disabled {
+ border-color: #dce2ea;
+ background: #f2f4f7;
+ box-shadow: none;
+ color: #98a2b3;
+ cursor: not-allowed;
+ opacity: 1;
+}
+input:not([type="checkbox"]):not([type="radio"]),
+select {
+ border: 1px solid var(--ui-border-strong);
+ border-radius: var(--ui-radius-sm);
+ background: var(--ui-surface);
+ box-shadow: inset 0 1px 2px rgba(15, 23, 42, .04);
+ color: var(--ui-text);
+ transition: border-color 120ms ease, box-shadow 120ms ease;
+}
+input:not([type="checkbox"]):not([type="radio"]):hover,
+select:hover { border-color: #9daabc; }
+input:not([type="checkbox"]):not([type="radio"]):focus,
+select:focus {
+ border-color: var(--ui-accent);
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, .13);
+}
+input[readonly] { background: var(--ui-surface-muted); color: var(--ui-text-muted); }
+input[type="checkbox"], input[type="radio"] { accent-color: var(--ui-accent); }
+
+.operator-shell { background: var(--ui-canvas); }
+.workspace-region {
+ border-right-color: var(--ui-border);
+ background: var(--ui-canvas);
+}
+.workspace-navigation {
+ border-right-color: var(--ui-border);
+ background: var(--workspace-nav-surface);
+ box-shadow: inset -1px 0 rgba(255, 255, 255, .8);
+}
+.workspace-nav-header { border-bottom: 1px solid rgba(216, 224, 234, .7); }
+.workspace-nav-toggle,
+.workspace-nav-item {
+ border: 0;
+ background: transparent;
+ box-shadow: none;
+ color: #445066;
+ font-weight: 600;
+}
+.workspace-nav-toggle:hover,
+.workspace-nav-item:hover { background: #edf1f6; color: var(--ui-text); }
+.workspace-nav-toggle:focus-visible,
+.workspace-nav-item:focus-visible {
+ outline: 2px solid var(--ui-accent);
+ outline-offset: -2px;
+ background: var(--ui-accent-soft);
+}
+.workspace-nav-item.workspace-current {
+ background: var(--ui-accent-soft);
+ color: #174ea6;
+ font-weight: 700;
+}
+.workspace-nav-item.workspace-current::before {
+ top: 9px;
+ bottom: 9px;
+ width: 4px;
+ border-radius: 0 4px 4px 0;
+ background: var(--ui-accent);
+}
+.workspace-nav-toggle svg,
+.workspace-nav-item svg { stroke-width: 1.9; }
+
+.workspace-surface,
+.workspace-content,
+.left-panel,
+.catalog-panel { background: var(--ui-canvas); }
+.workspace-header {
+ border-bottom-color: var(--ui-border);
+ background: rgba(255, 255, 255, .97);
+ box-shadow: 0 1px 0 rgba(15, 23, 42, .03);
+}
+.brand { gap: 10px; }
+.brand::after {
+ width: 1px;
+ height: 28px;
+ margin-left: 4px;
+ background: var(--ui-border);
+ content: "";
+}
+.brand span { color: #ed641f; font-size: 19px; letter-spacing: -1px; }
+.brand strong { color: #101828; font-size: 27px; letter-spacing: -3px; }
+.workspace-heading small,
+.playlist-heading small {
+ color: var(--ui-text-muted);
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: .08em;
+ text-transform: uppercase;
+}
+.workspace-heading h1 { color: var(--ui-text); font-size: 20px; font-weight: 750; }
+.connection-state {
+ min-width: 66px;
+ padding: 7px 11px;
+ border: 1px solid rgba(255, 255, 255, .35);
+ border-radius: 999px;
+ background: var(--ui-danger);
+ box-shadow: 0 3px 10px rgba(201, 54, 79, .2);
+ font: 700 12px "Segoe UI", sans-serif;
+ letter-spacing: .02em;
+ text-align: center;
+}
+.connection-state.connected {
+ background: var(--ui-success);
+ box-shadow: 0 3px 10px rgba(22, 131, 74, .2);
+}
+.connection-state.outcome-unknown {
+ background: var(--ui-warning);
+ box-shadow: 0 3px 10px rgba(169, 99, 0, .2);
+}
+.connection-state.dry-run:not(.outcome-unknown) {
+ background: var(--ui-accent);
+ box-shadow: 0 3px 10px rgba(37, 99, 235, .2);
+}
+
+.search-line { min-height: 46px; }
+.search-line label { color: #344054; font-weight: 700; }
+.search-line input { height: 34px; padding: 4px 10px; border-color: var(--ui-border-strong); }
+.search-line button {
+ display: inline-flex;
+ height: 34px;
+ align-items: center;
+ justify-content: center;
+ gap: 7px;
+ border-color: var(--ui-accent);
+ background: var(--ui-accent);
+ color: #fff;
+}
+.search-line button:not(:disabled):hover {
+ border-color: var(--ui-accent-hover);
+ background: var(--ui-accent-hover);
+}
+.search-line button svg {
+ width: 17px;
+ height: 17px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 2;
+ stroke-linecap: round;
+}
+
+.stock-results,
+.cut-list,
+.category-tree,
+.playlist-grid,
+.industry-list,
+.overseas-fixed-list,
+.overseas-results,
+.comparison-results,
+.comparison-saved,
+.comparison-fixed-list,
+.theme-results-panel,
+.theme-preview-panel,
+.expert-results,
+.expert-preview,
+.halt-results {
+ border-color: var(--ui-border);
+ border-radius: var(--ui-radius);
+ background: var(--ui-surface);
+ box-shadow: var(--ui-shadow-card);
+}
+.stock-results { margin-top: 3px; }
+.stock-results { height: 242px; }
+.stock-result,
+.category-tree button,
+.industry-list button,
+.overseas-fixed-list button,
+.overseas-results button,
+.comparison-results button,
+.comparison-saved button,
+.comparison-fixed-list button,
+.theme-results button,
+.expert-results button,
+.halt-results button,
+.named-playlist-definitions button,
+.manual-financial-list button,
+.operator-catalog-results button,
+.operator-catalog-stock-results button {
+ min-height: 24px;
+ border-radius: 0;
+ box-shadow: none;
+}
+.stock-result {
+ height: 24px;
+ padding: 0 8px;
+ color: var(--ui-text);
+ font-size: 14px;
+ line-height: 24px;
+}
+.stock-result:hover { background: var(--ui-surface-hover); }
+.stock-result.selected {
+ background: #344054;
+ color: #fff;
+ box-shadow: inset 3px 0 var(--ui-accent);
+}
+.manual-actions,
+.manual-list-actions { gap: 7px; }
+.manual-actions button,
+.manual-list-actions button {
+ border-color: #c8d7ef;
+ background: #f5f8fd;
+ color: #29466f;
+}
+.manual-actions button:not(:disabled):hover,
+.manual-list-actions button:not(:disabled):hover {
+ border-color: #9db8e4;
+ background: var(--ui-accent-soft);
+}
+
+.cut-list { overflow: auto; }
+.cut-row {
+ min-height: 24px;
+ border-bottom-color: #edf0f4;
+ background: var(--ui-surface);
+ color: var(--ui-text);
+}
+.cut-row:hover { background: #f7f9fc; }
+.cut-row .cut-number { border-right-color: #e6eaf0; color: var(--ui-accent); font-variant-numeric: tabular-nums; }
+.cut-row.selected {
+ background: #e8eef9;
+ box-shadow: inset 3px 0 var(--ui-accent);
+}
+.cut-row:focus {
+ outline: 2px solid var(--ui-accent);
+ outline-offset: -2px;
+}
+.cut-row.separator { background: var(--ui-surface-subtle); }
+.playlist-grid.cut-drag-over {
+ border-color: var(--ui-success);
+ box-shadow: inset 0 0 0 3px rgba(22, 131, 74, .85), var(--ui-shadow-card);
+}
+.cut-drag-feedback {
+ border-color: var(--ui-border-strong);
+ border-radius: 999px;
+ background: rgba(255, 255, 255, .98);
+ box-shadow: 0 12px 32px rgba(15, 23, 42, .18);
+ color: var(--ui-text-muted);
+}
+.cut-drag-feedback.allowed { border-color: var(--ui-success); color: #11613a; }
+.cut-drag-feedback.allowed .cut-drag-feedback-icon { background: var(--ui-success); }
+.operator-status { color: var(--ui-danger); font-weight: 600; }
+
+.market-tabs {
+ min-height: 38px;
+ gap: 2px;
+ padding: 3px;
+ border: 1px solid var(--ui-border);
+ border-radius: var(--ui-radius);
+ background: #e9eef5;
+}
+.market-tabs button {
+ min-width: 54px;
+ padding: 4px 10px;
+ border: 0;
+ border-radius: var(--ui-radius-sm);
+ background: transparent;
+ box-shadow: none;
+ color: #526076;
+}
+.market-tabs button:not(:disabled):hover { background: rgba(255, 255, 255, .65); }
+.market-tabs button.active {
+ border: 0;
+ background: var(--ui-surface);
+ box-shadow: 0 1px 3px rgba(15, 23, 42, .14);
+ color: #174ea6;
+ font-weight: 700;
+}
+.expand-line { color: var(--ui-text-muted); font-weight: 600; }
+.category-title {
+ border-bottom-color: var(--ui-border);
+ color: var(--ui-text-muted);
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: .08em;
+ text-transform: uppercase;
+}
+.category-tree { margin-top: 0; border-top: 1px solid var(--ui-border); }
+.tree-root,
+.catalog-section > summary,
+.industry-action-tree summary,
+.overseas-action-panel summary { color: var(--ui-success); font-weight: 700; }
+summary[data-tree-root].tree-node-selected,
+.tree-item.tree-node-selected,
+.tree-item:hover,
+.industry-list button.selected,
+.overseas-fixed-list button.selected,
+.overseas-results button.selected,
+.comparison-results button:hover,
+.comparison-results button.selected,
+.comparison-saved button.selected,
+.comparison-fixed-list button.selected,
+.theme-results button.selected,
+.expert-results button.selected,
+.halt-results button.selected {
+ background: #e8eef9;
+ color: var(--ui-text);
+}
+summary[data-tree-root]:focus-visible,
+.tree-item:focus-visible {
+ outline: 2px solid var(--ui-accent);
+ outline-offset: -2px;
+ background: var(--ui-accent-soft);
+}
+.overseas-group {
+ border-color: var(--ui-border);
+ border-radius: var(--ui-radius);
+ background: var(--ui-surface-subtle);
+ box-shadow: var(--ui-shadow-sm);
+}
+.theme-column-title,
+.operator-catalog-panel-header,
+.manual-financial-list button.manual-financial-list-header {
+ border-bottom-color: var(--ui-border);
+ background: var(--ui-surface-muted);
+ color: #475467;
+}
+
+.playlist-panel {
+ padding: 0 8px;
+ border-left-color: var(--ui-border);
+ background: var(--ui-canvas);
+ box-shadow: -4px 0 18px rgba(15, 23, 42, .035);
+}
+.playlist-heading {
+ margin: 0 -8px;
+ padding: 8px 14px;
+ border-bottom-color: var(--ui-border);
+ background: rgba(255, 255, 255, .97);
+ box-shadow: 0 1px 0 rgba(15, 23, 42, .03);
+}
+.playlist-heading-icon {
+ width: 38px;
+ height: 38px;
+ border-radius: 11px;
+ background: var(--ui-accent-soft);
+ color: var(--ui-accent);
+}
+.playlist-heading-icon svg {
+ width: 21px;
+ height: 21px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.8;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+.playlist-heading h2 { color: var(--ui-text); font-size: 19px; font-weight: 750; }
+.playlist-toolbar { gap: 5px; padding: 7px 0; overflow: auto; }
+.playlist-toolbar label {
+ display: inline-flex;
+ min-height: 25px;
+ align-items: center;
+ gap: 3px;
+ padding: 1px 5px;
+ border: 1px solid var(--ui-border);
+ border-radius: 6px;
+ background: rgba(255, 255, 255, .72);
+ color: #475467;
+ font-size: 12px;
+ font-weight: 600;
+}
+.playlist-toolbar button { height: 25px; border-radius: 6px; }
+.background-name { border-color: var(--ui-border-strong); border-radius: 6px; }
+.playlist-grid {
+ border-color: var(--ui-border);
+ border-radius: var(--ui-radius);
+ background: #e8edf3;
+ box-shadow: var(--ui-shadow-card);
+ scroll-padding-top: 29px;
+}
+.stock-results,
+.cut-list,
+.category-tree,
+.playlist-grid { scrollbar-gutter: stable; }
+.playlist-header {
+ border-bottom-color: var(--ui-border-strong);
+ background: #f4f6f9;
+ color: #475467;
+ font-size: 12px;
+ font-weight: 700;
+}
+.playlist-header > div { border-right-color: var(--ui-border); }
+.playlist-data-row {
+ border-bottom-color: #edf0f4;
+ background: var(--ui-surface);
+}
+.playlist-data-row > div { border-right-color: #edf0f4; }
+.playlist-data-row:not([aria-selected="true"]):not([data-active="true"]):not(.on-air):hover {
+ background: #f7f9fc;
+}
+.playlist-data-row .playlist-row-header {
+ border-color: var(--ui-border);
+ background: #f1f4f7;
+ background-image: none;
+ color: #475467;
+ font-weight: 700;
+}
+.playlist-data-row.on-air {
+ background: #fdc8d2;
+ box-shadow: inset 4px 0 #d92d4b;
+}
+.playlist-data-row.selected {
+ background: var(--ui-surface);
+ box-shadow: none;
+}
+.playlist-data-row.on-air.selected {
+ background: #fdc8d2;
+ box-shadow: inset 4px 0 #d92d4b;
+}
+.playlist-data-row[data-active="true"]:not(.on-air) {
+ background: var(--ui-warning-soft);
+ box-shadow: inset 0 3px var(--ui-warning);
+}
+.playlist-data-row[aria-selected="true"]:not(.on-air) {
+ background: var(--ui-accent-soft);
+ box-shadow: inset 4px 0 var(--ui-accent);
+}
+.playlist-data-row[data-active="true"][aria-selected="true"]:not(.on-air) {
+ background: var(--ui-warning-soft);
+ box-shadow: inset 4px 0 var(--ui-accent), inset 0 3px var(--ui-warning);
+}
+.playlist-data-row.on-air[data-active="true"] {
+ box-shadow: inset 4px 0 #d92d4b, inset 0 3px var(--ui-warning);
+}
+.playlist-data-row.on-air[aria-selected="true"] {
+ box-shadow: inset 4px 0 #d92d4b, inset 0 0 0 2px rgba(37, 99, 235, .7);
+}
+.playlist-data-row.on-air[data-active="true"][aria-selected="true"] {
+ box-shadow: inset 4px 0 #d92d4b, inset 0 3px var(--ui-warning),
+ inset 0 0 0 2px rgba(37, 99, 235, .7);
+}
+.playlist-data-row.playout-current:not(.on-air):not([data-active="true"]) {
+ box-shadow: inset 0 3px var(--ui-warning);
+}
+.playlist-data-row.drag-before { box-shadow: inset 0 3px var(--ui-success); }
+.playlist-data-row.drag-after { box-shadow: inset 0 -3px var(--ui-success); }
+
+.playout-actions { gap: 8px; }
+.playout-actions button {
+ height: 51px;
+ border-radius: var(--ui-radius);
+ background: var(--ui-surface);
+ box-shadow: var(--ui-shadow-sm);
+ font-size: 18px;
+ font-weight: 750;
+ letter-spacing: -.02em;
+}
+.playout-actions .take-in:not(:disabled) {
+ border-color: #83c9a1;
+ background: var(--ui-success-soft);
+ color: #11613a;
+}
+.playout-actions .take-in:not(:disabled):hover { border-color: var(--ui-success); background: #dff4e7; }
+.playout-actions .next:not(:disabled) {
+ border-color: #a9c1ec;
+ background: var(--ui-accent-soft);
+ color: #174ea6;
+}
+.playout-actions .next:not(:disabled):hover { border-color: var(--ui-accent); background: #dce8ff; }
+.playout-actions .take-out:not(:disabled) {
+ border-color: #e5a1ad;
+ background: var(--ui-danger-soft);
+ color: #a9263e;
+}
+.playout-actions .take-out:not(:disabled):hover { border-color: var(--ui-danger); background: #ffe2e8; }
+.playout-actions button:disabled { border-color: #dce2ea; background: #f2f4f7; color: #98a2b3; }
+.playout-actions .prepare.active {
+ border-color: var(--ui-success);
+ background: var(--ui-success);
+ color: #fff;
+}
+.playout-status {
+ border-top-color: var(--ui-border);
+ color: var(--ui-text-muted);
+ font-size: 12px;
+}
+
+.named-playlist-backdrop,
+.named-playlist-confirmation-backdrop,
+.manual-financial-backdrop,
+.manual-list-backdrop,
+.operator-catalog-backdrop,
+.dialog-backdrop {
+ background: rgba(15, 23, 42, .42);
+ backdrop-filter: blur(2px);
+}
+.named-playlist-dialog,
+.named-playlist-confirmation-dialog,
+.manual-financial-dialog,
+.manual-list-dialog,
+.operator-catalog-dialog,
+.legacy-dialog {
+ overflow: hidden;
+ border: 1px solid rgba(255, 255, 255, .72);
+ border-radius: var(--ui-radius-lg);
+ background: var(--ui-canvas);
+ box-shadow: var(--ui-shadow-dialog);
+}
+.named-playlist-dialog > header,
+.manual-financial-dialog > header,
+.manual-list-dialog > header,
+.operator-catalog-dialog > header {
+ border-bottom-color: var(--ui-border);
+ background: var(--ui-surface);
+}
+.named-playlist-dialog > header button,
+.manual-financial-dialog > header button,
+.manual-list-dialog > header button,
+.operator-catalog-dialog > header button {
+ border: 0;
+ background: transparent;
+ box-shadow: none;
+ color: var(--ui-text-muted);
+}
+.named-playlist-dialog > header button:hover,
+.manual-financial-dialog > header button:hover,
+.manual-list-dialog > header button:hover,
+.operator-catalog-dialog > header button:hover {
+ background: var(--ui-surface-muted);
+ color: var(--ui-text);
+}
+.named-playlist-status,
+.operator-catalog-status {
+ border-bottom-color: var(--ui-border);
+ background: var(--ui-surface-subtle);
+ color: var(--ui-text-muted);
+}
+.named-playlist-status.error,
+.operator-catalog-status.error,
+.manual-list-status.error,
+.manual-financial-warning {
+ border-color: #f1b4bf;
+ background: var(--ui-danger-soft);
+ color: #9d2339;
+}
+.named-playlist-definitions,
+.named-playlist-create,
+.manual-financial-list,
+.manual-financial-editor,
+.manual-financial-stock-results,
+.manual-list-table-wrap,
+.manual-list-search-results,
+.manual-list-status,
+.operator-catalog-list-panel,
+.operator-catalog-editor,
+.operator-catalog-stock-results,
+.operator-catalog-draft-rows {
+ border-color: var(--ui-border);
+ border-radius: var(--ui-radius);
+ background: var(--ui-surface);
+ box-shadow: var(--ui-shadow-sm);
+}
+.named-playlist-definitions button.selected,
+.manual-financial-list button.selected,
+.manual-financial-stock-results button.selected,
+.manual-list-search-results button.selected,
+.operator-catalog-results button.selected,
+.operator-catalog-stock-results button.selected,
+.operator-catalog-draft-row.selected {
+ border-color: #adc5ef;
+ background: var(--ui-accent-soft);
+ color: #174ea6;
+}
+.named-playlist-dialog > footer,
+.operator-catalog-toolbar {
+ border-color: var(--ui-border);
+ background: var(--ui-surface-subtle);
+}
+.named-playlist-dialog > footer #named-playlist-confirm-button,
+.named-playlist-confirmation-dialog #named-playlist-confirmation-yes,
+.manual-financial-actions .manual-primary,
+.manual-list-actions-row .primary,
+.operator-catalog-editor-actions button:last-child {
+ border-color: var(--ui-accent);
+ background: var(--ui-accent);
+ background-image: none;
+ color: #fff;
+ font-weight: 700;
+}
+.operator-catalog-toolbar button.selected {
+ border-color: var(--ui-accent);
+ background: var(--ui-accent);
+ color: #fff;
+}
+.operator-catalog-code { color: var(--ui-text-muted); }
+.operator-catalog-draft-row { border-bottom-color: #edf0f4; }
+.operator-catalog-draft-row:focus {
+ outline-color: var(--ui-accent);
+ outline-width: 2px;
+}
+.legacy-dialog { background: var(--ui-surface); }
+
+#playlist-delete-selected:not(:disabled),
+#playlist-delete-all:not(:disabled),
+#named-playlist-delete-button:not(:disabled) {
+ border-color: #df8796;
+ background: var(--ui-danger-soft);
+ color: #9d2339;
+}
+#playlist-delete-selected:not(:disabled):hover,
+#playlist-delete-all:not(:disabled):hover,
+#named-playlist-delete-button:not(:disabled):hover {
+ border-color: var(--ui-danger);
+ background: #ffe2e8;
+}
+
+.stock-result:focus-visible,
+.cut-row:focus-visible,
+.playlist-data-row:focus-visible,
+.tree-item:focus-visible,
+summary[data-tree-root]:focus-visible {
+ outline: 3px solid var(--ui-accent);
+ outline-offset: -3px;
+}
+
+@media (forced-colors: active) {
+ * { scrollbar-color: auto; }
+ button:focus-visible,
+ input:focus-visible,
+ select:focus-visible,
+ .stock-result:focus-visible,
+ .cut-row:focus-visible,
+ .playlist-data-row:focus-visible,
+ .tree-item:focus-visible,
+ summary[data-tree-root]:focus-visible {
+ outline: 3px solid Highlight;
+ outline-offset: -3px;
+ box-shadow: none;
+ }
+ .playlist-data-row[data-active="true"] { border-top: 3px solid Highlight; }
+ .playlist-data-row[aria-selected="true"] { border-right: 3px solid Highlight; }
+ .playlist-data-row.on-air { border-left: 4px double Highlight; }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ button,
+ input,
+ select { transition: none !important; }
+}
diff --git a/tests/LegacyParityWeb/operator-visual-accessibility.test.cjs b/tests/LegacyParityWeb/operator-visual-accessibility.test.cjs
new file mode 100644
index 0000000..d5643ed
--- /dev/null
+++ b/tests/LegacyParityWeb/operator-visual-accessibility.test.cjs
@@ -0,0 +1,89 @@
+"use strict";
+
+const assert = require("node:assert/strict");
+const fs = require("node:fs");
+const path = require("node:path");
+const test = require("node:test");
+
+const repositoryRoot = path.resolve(__dirname, "..", "..");
+const webRoot = path.join(
+ repositoryRoot,
+ "src",
+ "MBN_STOCK_WEBVIEW.LegacyParityApp",
+ "Web");
+const markup = fs.readFileSync(path.join(webRoot, "index.html"), "utf8");
+const styles = fs.readFileSync(path.join(webRoot, "styles.css"), "utf8");
+const app = fs.readFileSync(path.join(webRoot, "app.js"), "utf8");
+const playout = fs.readFileSync(path.join(webRoot, "playout-ui.js"), "utf8");
+
+function luminance(hex) {
+ const channels = [1, 3, 5].map(index =>
+ Number.parseInt(hex.slice(index, index + 2), 16) / 255);
+ const linear = channels.map(channel => channel <= 0.04045
+ ? channel / 12.92
+ : ((channel + 0.055) / 1.055) ** 2.4);
+ return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];
+}
+
+function contrast(left, right) {
+ const bright = Math.max(luminance(left), luminance(right));
+ const dark = Math.min(luminance(left), luminance(right));
+ return (bright + 0.05) / (dark + 0.05);
+}
+
+test("operator inputs retain a visible three-to-one boundary", () => {
+ const match = styles.match(/--ui-border-strong:\s*(#[0-9a-f]{6})/i);
+ assert.ok(match, "strong control border token must exist");
+ assert.ok(contrast(match[1], "#ffffff") >= 3,
+ `control border contrast was ${contrast(match[1], "#ffffff").toFixed(2)}:1`);
+});
+
+test("playlist exposes independent candidate, selection, and PGM states", () => {
+ assert.match(app, /element\.dataset\.active = isCandidate \? "true" : "false"/);
+ assert.match(app, /"aria-selected",\s*presentation\.isSelected \? "true" : "false"/s);
+ assert.match(app, /if \(presentation\.isOnAir\) accessibleStates\.push\("현재 PGM"\)/);
+ assert.match(app, /if \(isCandidate\) accessibleStates\.push\("송출 후보"\)/);
+ assert.match(app, /if \(presentation\.isSelected\) accessibleStates\.push\("삭제 선택됨"\)/);
+
+ for (const selector of [
+ '.playlist-data-row[data-active="true"]:not(.on-air)',
+ '.playlist-data-row[aria-selected="true"]:not(.on-air)',
+ '.playlist-data-row[data-active="true"][aria-selected="true"]:not(.on-air)',
+ '.playlist-data-row.on-air',
+ '.playlist-data-row.on-air[data-active="true"]',
+ '.playlist-data-row.on-air[aria-selected="true"]',
+ '.playlist-data-row.on-air[data-active="true"][aria-selected="true"]'
+ ]) {
+ assert.ok(styles.includes(selector), `${selector} must have an explicit visual state`);
+ }
+ assert.match(styles, /\.playlist-data-row\.on-air\s*\{[^}]*background:\s*#fdc8d2/i);
+});
+
+test("keyboard focus, high contrast, and scroll affordances remain visible", () => {
+ assert.match(styles,
+ /\.playlist-data-row:focus-visible[\s\S]*?outline:\s*3px solid var\(--ui-accent\)/);
+ assert.match(styles, /@media \(forced-colors:\s*active\)/);
+ assert.match(styles,
+ /\.playlist-data-row\[data-active="true"\]\s*\{\s*border-top:\s*3px solid Highlight/);
+ assert.match(styles,
+ /\.playlist-data-row\[aria-selected="true"\]\s*\{\s*border-right:\s*3px solid Highlight/);
+ assert.match(styles, /\.playlist-data-row\.on-air\s*\{\s*border-left:\s*4px double Highlight/);
+ assert.match(styles, /scrollbar-width:\s*auto/);
+ assert.match(styles, /scrollbar-gutter:\s*stable/);
+ assert.match(styles, /scroll-padding-top:\s*29px/);
+});
+
+test("playout controls publish their real keyboard shortcuts", () => {
+ assert.match(markup,
+ /id="playout-take-in"[^>]*aria-label="TAKE IN"[^>]*aria-keyshortcuts="F8"/s);
+ assert.match(markup,
+ /id="playout-take-out"[^>]*aria-label="TAKE OUT"[^>]*aria-keyshortcuts="Escape"/s);
+ assert.match(markup, /id="playout-next"[^>]*aria-label="NEXT"/s);
+});
+
+test("dry-run mode reads as information and clipped status keeps its full tooltip", () => {
+ assert.match(playout,
+ /connection\.classList\.toggle\("dry-run", playout\.mode === "dryRun"\)/);
+ assert.match(styles, /\.connection-state\.dry-run:not\(\.outcome-unknown\)\s*\{/);
+ assert.match(playout, /status\.title = status\.textContent/);
+});
diff --git a/tests/LegacyParityWeb/stock-result-keyboard.test.cjs b/tests/LegacyParityWeb/stock-result-keyboard.test.cjs
index b9ccd1b..870b280 100644
--- a/tests/LegacyParityWeb/stock-result-keyboard.test.cjs
+++ b/tests/LegacyParityWeb/stock-result-keyboard.test.cjs
@@ -37,7 +37,7 @@ function option(index, selected) {
const classes = new Set(selected ? ["selected"] : []);
return {
dataset: { resultIndex: String(index) },
- offsetHeight: 17,
+ offsetHeight: 24,
tabIndex: selected ? 0 : -1,
focused: false,
scrolled: false,
@@ -60,7 +60,7 @@ function keyboardHarness(selectedIndex = -1, count = 30) {
{ length: count },
(_, index) => option(index, index === selectedIndex)));
const stockResults = {
- clientHeight: 170,
+ clientHeight: 240,
querySelectorAll() { return options; }
};
const source = sliceBetween(
@@ -140,7 +140,8 @@ test("stock option keydown consumes list navigation but lets Home and End bubble
assert.doesNotMatch(factory, /stopPropagation|stopImmediatePropagation/);
assert.doesNotMatch(factory, /dblclick|add-playlist|append-playlist|cut-pointer-down/);
assert.match(styles,
- /\.stock-result \{[^}]*height: 17px;[^}]*line-height: 17px;/s);
+ /\.stock-result \{[^}]*height: 24px;[^}]*line-height: 24px;/s);
+ assert.match(styles, /\.stock-results \{ height: 242px; \}/);
});
test("rendering keeps exactly one tab stop, falling back to the first unselected row", () => {
diff --git a/tests/LegacyParityWeb/thin-client.test.cjs b/tests/LegacyParityWeb/thin-client.test.cjs
index a95b767..752ede2 100644
--- a/tests/LegacyParityWeb/thin-client.test.cjs
+++ b/tests/LegacyParityWeb/thin-client.test.cjs
@@ -170,7 +170,7 @@ test("state rendering preserves stock and playlist focus like native controls",
assert.doesNotMatch(app, /playlistRows\.replaceChildren/);
});
-test("playlist keeps the gray candidate separate from the pink on-air row and page", () => {
+test("playlist keeps candidate, selection, and pink on-air states independent", () => {
const sourceStart = app.indexOf("function playlistPlayoutPresentation");
const sourceEnd = app.indexOf("function presentCurrentPlaylistRow", sourceStart);
assert.ok(sourceStart >= 0 && sourceEnd > sourceStart);
diff --git a/tests/LegacyParityWeb/workspace-navigation.test.cjs b/tests/LegacyParityWeb/workspace-navigation.test.cjs
index a85c7e3..f3efd33 100644
--- a/tests/LegacyParityWeb/workspace-navigation.test.cjs
+++ b/tests/LegacyParityWeb/workspace-navigation.test.cjs
@@ -52,6 +52,30 @@ test("layout uses a two-to-one work and schedule split with compact schedule col
assert.match(styles, /\.playlist-row\s*\{[^}]*min-width:\s*600px/);
});
+test("modern operator theme keeps clear semantic states and accessible focus", () => {
+ assert.match(styles,
+ /Modern operator theme: visual-only overrides; geometry and native contracts stay intact/);
+ for (const token of [
+ "--ui-canvas",
+ "--ui-accent",
+ "--ui-success",
+ "--ui-danger"
+ ]) {
+ assert.match(styles, new RegExp(`${token}:`));
+ }
+
+ assert.match(styles,
+ /font-family:\s*"Segoe UI Variable",\s*"Segoe UI",\s*"Noto Sans KR"/);
+ assert.match(styles, /button:focus-visible,[\s\S]*?outline:\s*2px solid var\(--ui-accent\)/);
+ assert.match(styles, /\.playlist-data-row\.on-air\.selected\s*\{/);
+ assert.match(styles, /\.playout-actions \.take-in:not\(:disabled\)\s*\{/);
+ assert.match(styles, /\.playout-actions \.next:not\(:disabled\)\s*\{/);
+ assert.match(styles, /\.playout-actions \.take-out:not\(:disabled\)\s*\{/);
+ assert.match(styles, /@media \(prefers-reduced-motion:\s*reduce\)/);
+ assert.match(markup, /id="stock-search-button"[\s\S]*?