feat: refresh workspace header
This commit is contained in:
@@ -132,13 +132,39 @@
|
||||
|
||||
<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 class="brand" role="img" aria-label="매일경제TV VRi">
|
||||
<span class="brand-symbol" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" focusable="false">
|
||||
<path d="M5 17V9M10 17V6M15 17v-4M20 17V8"></path>
|
||||
<path d="M4 19h17"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="brand-lockup">
|
||||
<span>매일경제TV</span>
|
||||
<strong>VRi</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div class="workspace-heading">
|
||||
<span class="workspace-heading-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" focusable="false">
|
||||
<rect x="3.5" y="4" width="17" height="16" rx="3"></rect>
|
||||
<path d="M8.5 4v16M12 9h5M12 13h5"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<div class="workspace-heading-copy">
|
||||
<small>현재 작업</small>
|
||||
<h1 id="workspace-title">종목·컷</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="playout-connection-summary">
|
||||
<span class="playout-connection-copy" aria-hidden="true">
|
||||
<small>송출 연결</small>
|
||||
<strong>Tornado2</strong>
|
||||
</span>
|
||||
<div class="connection-state" id="playout-connection-state" role="status"
|
||||
aria-label="Tornado2 연결 상태" aria-live="polite"
|
||||
aria-atomic="true">미연결</div>
|
||||
</div>
|
||||
<div class="connection-state" id="playout-connection-state"
|
||||
aria-label="Tornado2 연결 상태">미연결</div>
|
||||
</header>
|
||||
|
||||
<div class="workspace-content">
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]*?<svg/);
|
||||
assert.match(markup,
|
||||
/class="workspace-heading"[\s\S]*?class="workspace-heading-icon"[\s\S]*?<div class="workspace-heading-copy">[\s\S]*?<small>현재 작업<\/small>[\s\S]*?id="workspace-title"/);
|
||||
assert.match(markup,
|
||||
/class="playout-connection-summary"[\s\S]*?<small>송출 연결<\/small>[\s\S]*?<strong>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/);
|
||||
});
|
||||
|
||||
@@ -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]*?<svg",
|
||||
Markup);
|
||||
Assert.Matches(
|
||||
@"class=""workspace-heading""[\s\S]*?class=""workspace-heading-icon""" +
|
||||
@"[\s\S]*?<div class=""workspace-heading-copy"">[\s\S]*?" +
|
||||
@"<small>현재 작업</small>[\s\S]*?id=""workspace-title""",
|
||||
Markup);
|
||||
Assert.Matches(
|
||||
@"class=""playout-connection-summary""[\s\S]*?<small>송출 연결</small>" +
|
||||
@"[\s\S]*?<strong>Tornado2</strong>[\s\S]*?id=""playout-connection-state""",
|
||||
Markup);
|
||||
Assert.Matches(
|
||||
@"id=""playout-connection-state""[^>]*role=""status""" +
|
||||
@"[^>]*aria-label=""Tornado2 연결 상태""[^>]*aria-live=""polite""" +
|
||||
@"[^>]*aria-atomic=""true"">미연결</div>",
|
||||
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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user