feat: refine branded operator experience

Add the MBN-branded startup intro, semantic sidebar icons, and refined card-based operator visuals. Strengthen packaged UI and input smoke coverage for the updated experience.
This commit is contained in:
2026-07-24 01:26:13 +09:00
parent f43483533a
commit b050f2f06c
25 changed files with 2316 additions and 164 deletions

View File

@@ -65,6 +65,7 @@ function pendingMenuFocusHarness() {
return tabId === "theme" ? pendingButton : null;
}
function refreshCurrentMenu() {}
function syncHeadingIcon() {}
function marketLabel() { return "테마"; }
${source}
return { syncMarket, hasPendingKeyboardMarketFocus };
@@ -262,6 +263,46 @@ test("package input physically resizes the splitter, verifies persistence, and r
/ArrowRight: \{ key: "ArrowRight", code: "ArrowRight", virtualKey: 39 \}/);
});
test("read-only package smoke seals the real visual-card screen and nested title selection", () => {
const start = packageInputHarness.indexOf(
"async function verifyVisualCardPresentation()");
const end = packageInputHarness.indexOf(
"async function playlistDropGeometry", start);
assert.ok(start >= 0 && end > start,
"visual-card package verification must be a bounded helper");
const visual = packageInputHarness.slice(start, end);
for (const contract of [
/selectMainTab\(\s*"overseas"/,
/button\[data-action-id="fixed-001"\]\.visual-card/,
/getComputedStyle\(list\)/,
/columnCount:\s*tracks\.length/,
/cardHeight:\s*button\.getBoundingClientRect\(\)\.height/,
/ariaLabel:\s*button\.getAttribute\("aria-label"\)/,
/hasPreview:\s*Boolean\(preview\)/,
/wordmarkSource:\s*wordmark\.getAttribute\("src"\)/,
/wordmarkNaturalWidth:\s*wordmark\.naturalWidth/,
/metrics\.wordmarkNaturalWidth !== 176/,
/metrics\.brandProduct !== "V-STOCK"/,
/metrics\.hasPreview \|\|\s*metrics\.hasState \|\| metrics\.hasKicker/,
/select fixed graphic from its visual-card title/,
/outboundMessageCount !== outboundBaseline/,
/checkpoint\("visual-card-presentation"/
]) assert.match(visual, contract);
assert.match(packageInputHarness,
/if \(readOnlyProfile\) \{\s*current = await verifyVisualCardPresentation\(\);/);
});
test("UI-only package smoke verifies the local customer wordmark is loaded and visible", () => {
assert.match(uiOnlySmokeHarness,
/const brandWordmark = document\.querySelector\("img\.brand-wordmark"\)/);
assert.match(uiOnlySmokeHarness,
/source:\s*brandWordmark\?\.getAttribute\("src"\)/);
assert.match(uiOnlySmokeHarness, /naturalWidth:\s*brandWordmark\?\.naturalWidth/);
assert.match(uiOnlySmokeHarness, /shell\.brand\.naturalWidth !== 176/);
assert.match(uiOnlySmokeHarness, /shell\.brand\.naturalHeight !== 43/);
assert.match(uiOnlySmokeHarness, /shell\.brand\.product !== "V-STOCK"/);
});
test("unified operator theme keeps clear semantic states and concise feedback", () => {
assert.match(styles,
/Unified operator theme: visual-only rules; geometry and native contracts stay intact/);
@@ -327,6 +368,116 @@ test("the complete sidebar is one labelled and collapsible navigation region", (
assert.match(markup, /id="workspace-nav-footer" class="workspace-nav-footer"/);
});
test("sidebar icons form one semantic SVG family and the heading follows the selected menu", () => {
const mappings = [
["id=\"workspace-stock-tab\"", "stock-cut"],
["data-tab-id=\"overseas\"", "overseas"],
["data-tab-id=\"exchange\"", "exchange"],
["data-tab-id=\"index\"", "index"],
["data-tab-id=\"kospiIndustry\"", "kospi"],
["data-tab-id=\"kosdaqIndustry\"", "kosdaq"],
["data-tab-id=\"comparison\"", "comparison"],
["data-tab-id=\"theme\"", "theme"],
["data-tab-id=\"overseasStocks\"", "overseas-stocks"],
["data-tab-id=\"expert\"", "expert"],
["data-tab-id=\"tradingHalt\"", "trading-halt"],
["id=\"workspace-settings-tab\"", "settings"]
];
for (const [anchor, icon] of mappings) {
const start = markup.indexOf(anchor);
const end = markup.indexOf("</button>", start);
assert.ok(start >= 0 && end > start, `missing menu button: ${anchor}`);
const button = markup.slice(start, end);
assert.match(button, new RegExp(`data-icon="${icon}"`));
assert.match(button, new RegExp(`href="#workspace-icon-${icon}"`));
}
const symbolIds = Array.from(
markup.matchAll(/<symbol id="workspace-icon-([^"]+)" viewBox="0 0 24 24">/g),
match => match[1]);
assert.deepEqual(symbolIds, [
"menu",
"stock-cut",
"overseas",
"exchange",
"index",
"kospi",
"kosdaq",
"comparison",
"theme",
"overseas-stocks",
"expert",
"trading-halt",
"settings"
]);
const kospi = markup.slice(
markup.indexOf("<symbol id=\"workspace-icon-kospi\""),
markup.indexOf("</symbol>", markup.indexOf("<symbol id=\"workspace-icon-kospi\"")));
const kosdaq = markup.slice(
markup.indexOf("<symbol id=\"workspace-icon-kosdaq\""),
markup.indexOf("</symbol>", markup.indexOf("<symbol id=\"workspace-icon-kosdaq\"")));
assert.match(kospi, /M4\.5 18V6h4a4 4 0 0 1 0 8h-4/);
assert.match(kospi, /M15 6h4M17 6v12M15 18h4/);
assert.match(kosdaq, /M2\.3 18V6h1\.6c2\.8 0 4\.3 2\.2 4\.3 6/);
assert.match(kosdaq, /M9 18l2\.5-12H13l2\.5 12M10 14h4\.5/);
assert.match(kosdaq, /<ellipse cx="19\.2" cy="12" rx="3\.1" ry="6" stroke-width="1\.65">/);
assert.match(kosdaq, /m19\.5 14\.5 3\.2 4/);
assert.doesNotMatch(kospi + kosdaq, /<text\b/);
assert.notEqual(kospi, kosdaq);
assert.match(styles, /\.workspace-icon-sprite\s*\{[\s\S]*?width:\s*0;[\s\S]*?height:\s*0;/);
assert.match(styles, /\.workspace-nav-icon\s*\{\s*shape-rendering:\s*geometricPrecision;\s*\}/);
assert.match(navigation,
/const titleIcon = document\.querySelector\("\.workspace-heading-icon"\)/);
assert.match(navigation,
/function syncHeadingIcon\(button\)[\s\S]*?source\.cloneNode\(true\)[\s\S]*?titleIcon\.replaceChildren\(next\)/);
assert.match(navigation,
/activeWorkspace = name;[\s\S]*?refreshCurrentMenu\(\);[\s\S]*?syncHeadingIcon\(selectedButton\);[\s\S]*?title\.textContent/);
assert.match(navigation,
/if \(activeWorkspace === "catalog"\)[\s\S]*?syncHeadingIcon\(button\);[\s\S]*?title\.textContent = marketLabel\(button\)/);
});
test("package smokes verify semantic icon painting, heading identity, and collapsed centering", () => {
for (const harness of [packageInputHarness, uiOnlySmokeHarness]) {
for (const [tabId, icon] of [
["overseas", "overseas"],
["exchange", "exchange"],
["index", "index"],
["kospiIndustry", "kospi"],
["kosdaqIndustry", "kosdaq"],
["comparison", "comparison"],
["theme", "theme"],
["overseasStocks", "overseas-stocks"],
["expert", "expert"],
["tradingHalt", "trading-halt"]
]) {
assert.match(harness, new RegExp(`${tabId}: "${icon}"`));
}
assert.match(harness, /svg\?\.getBBox\?\.\(\)/);
assert.match(harness, /reference:\s*svg\?\.querySelector\("use"\)\?\.getAttribute\("href"\)/);
assert.match(harness, /Number\.parseFloat\(style\.opacity\) > 0/);
assert.match(harness, /style\.stroke !== "none" \|\| style\.fill !== "none"/);
assert.match(harness, /rendered:\s*Boolean\(/);
assert.match(harness, /expectedHeadingIcon/);
assert.match(harness, /#workspace-icon-\$\{expectedHeadingIcon\}/);
}
assert.match(packageInputHarness, /navigationItems:\s*\[/);
assert.match(packageInputHarness, /iconCenterOffset:/);
assert.match(packageInputHarness, /iconCenterOffsetY:/);
assert.match(packageInputHarness, /item\.iconCenterOffset > 1\.5/);
assert.match(packageInputHarness, /item\.iconCenterOffsetY > 1\.5/);
assert.match(packageInputHarness, /item\.labelVisible !== false/);
assert.match(packageInputHarness, /collapsedIconCenterOffsets:/);
assert.match(packageInputHarness,
/tab\.icon\.key === baseline\.marketTabs\[index\]\.icon\.key/);
assert.match(uiOnlySmokeHarness, /layout\.stockTabIcon\.rendered === true/);
assert.match(uiOnlySmokeHarness, /layout\.settingsTabIcon\.rendered === true/);
assert.match(uiOnlySmokeHarness, /tab\.icon\.rendered === true/);
});
test("workspace navigation stores only the approved local presentation preferences", () => {
assert.match(navigation, /entry\.view\.hidden = !isCurrent/);
assert.match(navigation, /entry\.view\.toggleAttribute\("inert", !isCurrent\)/);