From d9b477e543b04a6050b3bfe241dc966171335730 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Tue, 21 Jul 2026 10:07:58 +0900 Subject: [PATCH] feat: refresh workspace header --- .../Web/index.html | 38 ++- .../Web/playout-ui.js | 5 +- .../Web/styles.css | 220 +++++++++++++++--- .../operator-visual-accessibility.test.cjs | 25 ++ .../LegacyWorkspaceLayoutContractTests.cs | 38 +++ 5 files changed, 292 insertions(+), 34 deletions(-) diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/index.html b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/index.html index 9e03bfd..0c4ed4f 100644 --- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/index.html +++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/index.html @@ -132,13 +132,39 @@
-
매일경제TVVRi
-
- 작업 영역 -

종목·컷

+ +
+ +
+ 현재 작업 +

종목·컷

+
+
+
+ +
미연결
-
미연결
diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/playout-ui.js b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/playout-ui.js index 336349b..326c394 100644 --- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/playout-ui.js +++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/playout-ui.js @@ -100,7 +100,10 @@ const canPrepareToggle = playout.phase === "idle" ? hasPlaylist : playout.phase === "prepared" || playout.phase === "program"; - connection.textContent = connectionLabel(playout); + const nextConnectionText = connectionLabel(playout); + if (connection.textContent !== nextConnectionText) { + connection.textContent = nextConnectionText; + } connection.classList.toggle("connected", playout.isConnected === true); connection.classList.toggle("outcome-unknown", playout.outcomeUnknown === true); connection.classList.toggle("dry-run", playout.mode === "dryRun"); diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/styles.css b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/styles.css index 7460cba..746b5e3 100644 --- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/styles.css +++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/styles.css @@ -663,7 +663,10 @@ input[type="checkbox"], input[type="radio"] { accent-color: var(--ui-accent); } 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-header { + border-bottom: 1px solid rgba(216, 224, 234, .7); + background: linear-gradient(180deg, rgba(255, 255, 255, .78), rgba(246, 249, 253, .6)); +} .workspace-nav-toggle, .workspace-nav-item { border: 0; @@ -700,20 +703,93 @@ input[type="checkbox"], input[type="radio"] { accent-color: var(--ui-accent); } .left-panel, .catalog-panel { background: var(--ui-canvas); } .workspace-header { + position: relative; border-bottom-color: var(--ui-border); - background: rgba(255, 255, 255, .97); - box-shadow: 0 1px 0 rgba(15, 23, 42, .03); + background: + radial-gradient(circle at 78% -90%, rgba(37, 99, 235, .12), transparent 48%), + linear-gradient(110deg, #fff 0%, #fbfcff 55%, #f5f8ff 100%); + box-shadow: 0 1px 0 rgba(15, 23, 42, .04), 0 8px 24px rgba(15, 23, 42, .025); } -.brand { gap: 10px; } -.brand::after { - width: 1px; - height: 28px; - margin-left: 4px; - background: var(--ui-border); +.workspace-header::before { + position: absolute; + right: 0; + bottom: -1px; + left: 0; + height: 2px; + background: linear-gradient(90deg, #f26b38 0 13%, var(--ui-accent) 13% 52%, transparent 82%); content: ""; + opacity: .72; + pointer-events: none; +} +.brand { + min-width: 154px; + gap: 10px; +} +.brand .brand-symbol { + display: grid; + width: 36px; + height: 36px; + flex: 0 0 36px; + place-items: center; + border: 1px solid rgba(255, 255, 255, .72); + border-radius: 11px; + background: linear-gradient(145deg, #ff8a4c, #e65320); + box-shadow: 0 7px 16px rgba(218, 78, 29, .2), inset 0 1px rgba(255, 255, 255, .25); + color: #fff; +} +.brand-symbol svg, +.workspace-heading-icon svg { + width: 21px; + height: 21px; + fill: none; + stroke: currentColor; + stroke-width: 1.8; + stroke-linecap: round; + stroke-linejoin: round; +} +.brand .brand-lockup { + display: flex; + min-width: 0; + flex-direction: column; + line-height: 1.08; +} +.brand-lockup span { + color: var(--ui-text); + font-size: 14px; + font-weight: 800; + letter-spacing: -.045em; +} +.brand-lockup strong { + margin-top: 4px; + color: #dc5826; + font-family: inherit; + font-size: 10px; + font-style: normal; + font-weight: 800; + letter-spacing: .18em; +} +.workspace-heading { + display: flex; + min-width: 0; + align-items: center; + gap: 10px; +} +.workspace-heading-icon { + display: grid; + width: 36px; + height: 36px; + flex: 0 0 36px; + place-items: center; + border: 1px solid #cedcf6; + border-radius: 11px; + background: rgba(234, 241, 255, .86); + box-shadow: inset 0 1px rgba(255, 255, 255, .85); + color: var(--ui-accent); +} +.workspace-heading-copy { + display: block; + min-width: 0; } -.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); @@ -722,29 +798,92 @@ input[type="checkbox"], input[type="radio"] { accent-color: var(--ui-accent); } letter-spacing: .08em; text-transform: uppercase; } -.workspace-heading h1 { color: var(--ui-text); font-size: 20px; font-weight: 750; } +.workspace-heading h1 { + margin-top: 0; + color: var(--ui-text); + font-size: 20px; + font-weight: 760; + letter-spacing: -.04em; +} +.playout-connection-summary { + display: flex; + align-items: center; + gap: 11px; + padding-left: 16px; + border-left: 1px solid rgba(190, 202, 219, .8); +} +.playout-connection-copy { + display: flex; + flex-direction: column; + align-items: flex-end; + line-height: 1.1; + white-space: nowrap; +} +.playout-connection-copy small { + color: var(--ui-text-muted); + font-size: 10px; + font-weight: 750; + letter-spacing: .08em; +} +.playout-connection-copy strong { + margin-top: 3px; + color: #344054; + font-size: 11px; + font-weight: 750; +} .connection-state { - min-width: 66px; - padding: 7px 11px; - border: 1px solid rgba(255, 255, 255, .35); + display: inline-flex; + min-width: 86px; + min-height: 30px; + align-items: center; + justify-content: center; + gap: 7px; + padding: 6px 11px; + border: 1px solid #efbcc5; border-radius: 999px; - background: var(--ui-danger); - box-shadow: 0 3px 10px rgba(201, 54, 79, .2); - font: 700 12px "Segoe UI", sans-serif; + background: var(--ui-danger-soft); + box-shadow: inset 0 1px rgba(255, 255, 255, .8); + color: #a52940; + font: 750 11px "Segoe UI", sans-serif; letter-spacing: .02em; text-align: center; + white-space: nowrap; +} +.connection-state::before { + width: 7px; + height: 7px; + flex: 0 0 7px; + border-radius: 50%; + background: currentColor; + box-shadow: 0 0 0 3px rgba(201, 54, 79, .11); + content: ""; } .connection-state.connected { - background: var(--ui-success); - box-shadow: 0 3px 10px rgba(22, 131, 74, .2); + border-color: #a9ddbf; + background: var(--ui-success-soft); + box-shadow: inset 0 1px rgba(255, 255, 255, .8); + color: #126b3e; +} +.connection-state.connected::before { + box-shadow: 0 0 0 3px rgba(22, 131, 74, .12); } .connection-state.outcome-unknown { - background: var(--ui-warning); - box-shadow: 0 3px 10px rgba(169, 99, 0, .2); + border-color: #e9cb8f; + background: var(--ui-warning-soft); + box-shadow: inset 0 1px rgba(255, 255, 255, .8); + color: #8a5100; +} +.connection-state.outcome-unknown::before { + box-shadow: 0 0 0 3px rgba(169, 99, 0, .12); } .connection-state.dry-run:not(.outcome-unknown) { - background: var(--ui-accent); - box-shadow: 0 3px 10px rgba(37, 99, 235, .2); + border-color: #b7caf2; + background: var(--ui-accent-soft); + box-shadow: inset 0 1px rgba(255, 255, 255, .8); + color: #174ea6; +} +.connection-state.dry-run:not(.outcome-unknown)::before { + box-shadow: 0 0 0 3px rgba(37, 99, 235, .12); } .search-line { min-height: 46px; } @@ -956,17 +1095,33 @@ summary[data-tree-root]:focus-visible, box-shadow: -4px 0 18px rgba(15, 23, 42, .035); } .playlist-heading { + position: relative; 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); + background: + radial-gradient(circle at 72% -110%, rgba(37, 99, 235, .11), transparent 48%), + linear-gradient(110deg, #fff 0%, #fbfcff 60%, #f5f8ff 100%); + box-shadow: 0 1px 0 rgba(15, 23, 42, .04), 0 8px 24px rgba(15, 23, 42, .025); +} +.playlist-heading::before { + position: absolute; + right: 0; + bottom: -1px; + left: 0; + height: 2px; + background: linear-gradient(90deg, var(--ui-accent), #65a3ff 42%, transparent 88%); + content: ""; + opacity: .62; + pointer-events: none; } .playlist-heading-icon { width: 38px; height: 38px; + border: 1px solid #cedcf6; border-radius: 11px; - background: var(--ui-accent-soft); + background: rgba(234, 241, 255, .86); + box-shadow: inset 0 1px rgba(255, 255, 255, .85); color: var(--ui-accent); } .playlist-heading-icon svg { @@ -1409,6 +1564,17 @@ summary[data-tree-root]:focus-visible { @media (forced-colors: active) { * { scrollbar-color: auto; } + .brand .brand-symbol, + .workspace-heading-icon, + .playlist-heading-icon, + .connection-state { + border: 1px solid CanvasText; + background: Canvas; + box-shadow: none; + color: CanvasText; + } + .workspace-header::before, + .playlist-heading::before { background: Highlight; } button:focus-visible, input:focus-visible, select:focus-visible, diff --git a/tests/LegacyParityWeb/operator-visual-accessibility.test.cjs b/tests/LegacyParityWeb/operator-visual-accessibility.test.cjs index d5643ed..9d233b6 100644 --- a/tests/LegacyParityWeb/operator-visual-accessibility.test.cjs +++ b/tests/LegacyParityWeb/operator-visual-accessibility.test.cjs @@ -87,3 +87,28 @@ test("dry-run mode reads as information and clipped status keeps its full toolti assert.match(styles, /\.connection-state\.dry-run:not\(\.outcome-unknown\)\s*\{/); assert.match(playout, /status\.title = status\.textContent/); }); + +test("workspace header uses a modern semantic lockup without changing native status text", () => { + assert.match(markup, + /class="brand"[^>]*role="img"[^>]*aria-label="매일경제TV VRi"[\s\S]*?class="brand-symbol"[\s\S]*?[\s\S]*?현재 작업<\/small>[\s\S]*?id="workspace-title"/); + assert.match(markup, + /class="playout-connection-summary"[\s\S]*?송출 연결<\/small>[\s\S]*?Tornado2<\/strong>[\s\S]*?id="playout-connection-state"/); + assert.match(markup, + /id="playout-connection-state"[^>]*role="status"[^>]*aria-label="Tornado2 연결 상태"[^>]*aria-live="polite"[^>]*aria-atomic="true">미연결<\/div>/); + assert.match(styles, + /\.workspace-header\s*\{[^}]*radial-gradient[\s\S]*?linear-gradient/); + assert.match(styles, /\.workspace-header::before\s*\{/); + assert.match(styles, /\.playlist-heading::before\s*\{/); + assert.match(styles, /\.connection-state::before\s*\{/); + assert.match(styles, + /\.playout-connection-copy small\s*\{[^}]*color:\s*var\(--ui-text-muted\)/); + assert.match(styles, /\.connection-state\.connected::before\s*\{/); + assert.match(styles, + /\.connection-state\.dry-run:not\(\.outcome-unknown\)::before\s*\{/); + assert.match(styles, + /\.workspace-surface\s*\{[^}]*grid-template-rows:\s*66px minmax\(0, 1fr\)/); + assert.match(playout, + /const nextConnectionText = connectionLabel\(playout\);[\s\S]*?if \(connection\.textContent !== nextConnectionText\)[\s\S]*?connection\.textContent = nextConnectionText/); +}); diff --git a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyWorkspaceLayoutContractTests.cs b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyWorkspaceLayoutContractTests.cs index 988e4f9..2b74f67 100644 --- a/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyWorkspaceLayoutContractTests.cs +++ b/tests/MBN_STOCK_WEBVIEW.LegacyWeb.Tests/LegacyWorkspaceLayoutContractTests.cs @@ -171,6 +171,44 @@ public sealed class LegacyWorkspaceLayoutContractTests Markup); } + [Fact] + public void WorkspaceHeaderUsesModernSemanticLockupAndStableNativeStatusTarget() + { + Assert.Matches( + @"class=""brand""[^>]*role=""img""[^>]*aria-label=""매일경제TV VRi""[\s\S]*?" + + @"class=""brand-symbol""[\s\S]*?[\s\S]*?" + + @"현재 작업[\s\S]*?id=""workspace-title""", + Markup); + Assert.Matches( + @"class=""playout-connection-summary""[\s\S]*?송출 연결" + + @"[\s\S]*?Tornado2[\s\S]*?id=""playout-connection-state""", + Markup); + Assert.Matches( + @"id=""playout-connection-state""[^>]*role=""status""" + + @"[^>]*aria-label=""Tornado2 연결 상태""[^>]*aria-live=""polite""" + + @"[^>]*aria-atomic=""true"">미연결
", + Markup); + Assert.Contains(".workspace-header::before", Styles, StringComparison.Ordinal); + Assert.Contains(".playlist-heading::before", Styles, StringComparison.Ordinal); + Assert.Contains(".connection-state::before", Styles, StringComparison.Ordinal); + Assert.Matches( + @"\.playout-connection-copy small\s*\{[^}]*color:\s*var\(--ui-text-muted\)", + Styles); + Assert.Contains(".connection-state.connected::before", Styles, StringComparison.Ordinal); + Assert.Contains( + ".connection-state.dry-run:not(.outcome-unknown)::before", + Styles, + StringComparison.Ordinal); + Assert.Contains( + "if (connection.textContent !== nextConnectionText)", + File.ReadAllText(Path.Combine(WebRoot, "playout-ui.js")), + StringComparison.Ordinal); + } + [Fact] public void NavigationChangesPresentationWithoutNativeOrStoredState() {