From 898fa7ac0dd3e09cbb9594f84174d659bcba9865 Mon Sep 17 00:00:00 2001 From: hided62 Date: Sun, 16 Aug 2026 05:45:48 +0000 Subject: [PATCH] =?UTF-8?q?fix(game-ui):=20=EB=B2=84=ED=8A=BC=20=EB=88=8C?= =?UTF-8?q?=EB=A6=BC=20=ED=9A=A8=EA=B3=BC=EB=A5=BC=20=EA=B3=B5=ED=86=B5?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lumen 버튼의 테두리와 hover/active 이동을 공통 레이어가 소유하도록 정리하고 색상 변형은 CSS 변수만 설정하게 합니다.\n\n모바일 하단의 외부·국가·빠른 이동·갱신·로비 버튼을 같은 계약에 연결하고 Chromium 회귀 검증을 확장합니다. --- app/game-frontend/e2e/mainNavigation.spec.ts | 135 +++++++++++++++++- .../src/assets/styles/legacy-controls.css | 49 +++++-- .../src/assets/styles/tokens.css | 2 + .../components/main/MainMobileBottomBar.vue | 109 ++++---------- docs/frontend-css-architecture.md | 28 ++-- 5 files changed, 218 insertions(+), 105 deletions(-) diff --git a/app/game-frontend/e2e/mainNavigation.spec.ts b/app/game-frontend/e2e/mainNavigation.spec.ts index ff5750a3..b0f489e6 100644 --- a/app/game-frontend/e2e/mainNavigation.spec.ts +++ b/app/game-frontend/e2e/mainNavigation.spec.ts @@ -1564,7 +1564,7 @@ test('the 939/940 boundary switches to the Ref-style 500px single document', asy await persistArtifact(page, `${basePath.slice(1)}-mobile-500`); }); -test('mobile bottom controls preserve Ref navigation color, contrast, and pressed depth', async ({ page }) => { +test('mobile bottom controls share Ref pressed geometry while their color bases vary', async ({ page }, testInfo) => { const state: NavigationFixture = { officerLevel: 5, permission: 2, @@ -1574,6 +1574,7 @@ test('mobile bottom controls preserve Ref navigation color, contrast, and presse nationColor: '#FFFF00', generalMeCalls: 0, operations: [], + refreshDelayMs: 300, }; await installFixture(page, state); await page.setViewportSize({ width: 500, height: 900 }); @@ -1586,19 +1587,26 @@ test('mobile bottom controls preserve Ref navigation color, contrast, and presse return { backgroundColor: style.backgroundColor, color: style.color, + borderTopWidth: style.borderTopWidth, + borderLeftWidth: style.borderLeftWidth, borderBottomWidth: style.borderBottomWidth, + borderBottomColor: style.borderBottomColor, marginTop: style.marginTop, fontWeight: style.fontWeight, height: rect.height, + classNames: [...element.classList], }; }); - const pointerDownStyle = async (selector: string) => { + const pointerDownStyle = async (selector: string, screenshotName?: string) => { const target = page.locator(selector); const box = await target.boundingBox(); if (!box) throw new Error(`mobile bottom control is not measurable: ${selector}`); await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); await page.mouse.down(); const activeStyle = await buttonStyle(selector); + if (screenshotName) { + await page.screenshot({ path: testInfo.outputPath(screenshotName), fullPage: true }); + } await page.mouse.move(1, 1); await page.mouse.up(); return activeStyle; @@ -1609,10 +1617,14 @@ test('mobile bottom controls preserve Ref navigation color, contrast, and presse expect(await buttonStyle(globalSelector)).toMatchObject({ backgroundColor: 'rgb(0, 88, 44)', color: 'rgb(255, 255, 255)', + borderTopWidth: '0px', + borderLeftWidth: '1px', borderBottomWidth: '4px', + borderBottomColor: 'rgb(0, 79, 40)', marginTop: '0px', fontWeight: '700', height: 45, + classNames: expect.arrayContaining(['legacy-button', 'legacy-button--navigation']), }); await page.locator(globalSelector).hover(); expect(await buttonStyle(globalSelector)).toMatchObject({ @@ -1629,19 +1641,78 @@ test('mobile bottom controls preserve Ref navigation color, contrast, and presse }); const nationTrigger = page.locator('[data-bottom-menu="nation"]'); - await expect(nationTrigger).toHaveCSS('background-color', 'rgb(255, 255, 0)'); - await expect(nationTrigger).toHaveCSS('color', 'rgb(0, 0, 0)'); + expect(await buttonStyle('[data-bottom-menu="nation"]')).toMatchObject({ + backgroundColor: 'rgb(255, 255, 0)', + color: 'rgb(0, 0, 0)', + borderTopWidth: '0px', + borderLeftWidth: '1px', + borderBottomWidth: '4px', + borderBottomColor: 'color(srgb 0.9 0.9 0)', + marginTop: '0px', + fontWeight: '700', + height: 45, + classNames: expect.arrayContaining(['legacy-button', 'legacy-button--lumen']), + }); + await nationTrigger.hover(); + expect(await buttonStyle('[data-bottom-menu="nation"]')).toMatchObject({ + backgroundColor: 'rgb(255, 255, 0)', + borderBottomWidth: '3px', + marginTop: '1px', + height: 44, + }); + expect(await pointerDownStyle('[data-bottom-menu="nation"]', 'mobile-bottom-nation-active.png')).toMatchObject({ + backgroundColor: 'rgb(255, 255, 0)', + borderBottomWidth: '2px', + marginTop: '2px', + height: 43, + }); - await page.locator('[data-bottom-menu="quick"]').click(); + const quickSelector = '[data-bottom-menu="quick"]'; + expect(await buttonStyle(quickSelector)).toMatchObject({ + backgroundColor: 'rgb(33, 37, 41)', + color: 'rgb(255, 255, 255)', + borderTopWidth: '0px', + borderLeftWidth: '1px', + borderBottomWidth: '4px', + borderBottomColor: 'rgb(30, 33, 37)', + marginTop: '0px', + fontWeight: '700', + height: 45, + classNames: expect.arrayContaining(['legacy-button', 'legacy-button--dark']), + }); + await page.locator(quickSelector).hover(); + expect(await buttonStyle(quickSelector)).toMatchObject({ + backgroundColor: 'rgb(33, 37, 41)', + borderBottomWidth: '3px', + marginTop: '1px', + height: 44, + }); + expect(await pointerDownStyle(quickSelector)).toMatchObject({ + backgroundColor: 'rgb(33, 37, 41)', + borderBottomWidth: '2px', + marginTop: '2px', + height: 43, + }); + + await page.locator(quickSelector).click(); const lobbySelector = '#mobile-quick-menu .lobby-link'; await expect(page.locator(lobbySelector)).toBeVisible(); expect(await buttonStyle(lobbySelector)).toMatchObject({ backgroundColor: 'rgb(0, 88, 44)', color: 'rgb(255, 255, 255)', borderBottomWidth: '4px', + borderBottomColor: 'rgb(0, 79, 40)', marginTop: '0px', fontWeight: '700', height: 40, + classNames: expect.arrayContaining(['legacy-button', 'legacy-button--navigation']), + }); + await page.locator(lobbySelector).hover(); + expect(await buttonStyle(lobbySelector)).toMatchObject({ + backgroundColor: 'rgb(0, 88, 44)', + borderBottomWidth: '3px', + marginTop: '1px', + height: 39, }); expect(await pointerDownStyle(lobbySelector)).toMatchObject({ backgroundColor: 'rgb(0, 88, 44)', @@ -1655,9 +1726,18 @@ test('mobile bottom controls preserve Ref navigation color, contrast, and presse backgroundColor: 'rgb(0, 88, 44)', color: 'rgb(255, 255, 255)', borderBottomWidth: '4px', + borderBottomColor: 'rgb(0, 79, 40)', marginTop: '0px', fontWeight: '700', height: 45, + classNames: expect.arrayContaining(['legacy-button', 'legacy-button--navigation']), + }); + await page.locator(autoRefreshSelector).hover(); + expect(await buttonStyle(autoRefreshSelector)).toMatchObject({ + backgroundColor: 'rgb(0, 88, 44)', + borderBottomWidth: '3px', + marginTop: '1px', + height: 44, }); expect(await pointerDownStyle(autoRefreshSelector)).toMatchObject({ backgroundColor: 'rgb(0, 88, 44)', @@ -1666,6 +1746,51 @@ test('mobile bottom controls preserve Ref navigation color, contrast, and presse height: 43, }); + const manualRefreshSelector = '[data-bottom-menu="manual-refresh"]'; + expect(await buttonStyle(manualRefreshSelector)).toMatchObject({ + backgroundColor: 'rgb(33, 37, 41)', + color: 'rgb(255, 255, 255)', + borderTopWidth: '0px', + borderLeftWidth: '1px', + borderBottomWidth: '4px', + borderBottomColor: 'rgb(30, 33, 37)', + marginTop: '0px', + fontWeight: '700', + height: 45, + classNames: expect.arrayContaining(['legacy-button', 'legacy-button--dark']), + }); + await page.locator(manualRefreshSelector).hover(); + expect(await buttonStyle(manualRefreshSelector)).toMatchObject({ + backgroundColor: 'rgb(33, 37, 41)', + borderBottomWidth: '3px', + marginTop: '1px', + height: 44, + }); + expect(await pointerDownStyle(manualRefreshSelector)).toMatchObject({ + backgroundColor: 'rgb(33, 37, 41)', + borderBottomWidth: '2px', + marginTop: '2px', + height: 43, + }); + await page.keyboard.press('Tab'); + await page.locator(manualRefreshSelector).focus(); + await expect + .poll(() => page.locator(manualRefreshSelector).evaluate((element) => element.matches(':focus-visible'))) + .toBe(true); + await expect + .poll(() => page.locator(manualRefreshSelector).evaluate((element) => getComputedStyle(element).boxShadow)) + .not.toBe('none'); + await page.locator(manualRefreshSelector).click(); + await expect(page.locator(manualRefreshSelector)).toBeDisabled(); + expect(await buttonStyle(manualRefreshSelector)).toMatchObject({ + backgroundColor: 'rgb(33, 37, 41)', + borderBottomWidth: '4px', + marginTop: '0px', + height: 45, + }); + await expect(page.locator(manualRefreshSelector)).toHaveCSS('opacity', '0.55'); + await expect(page.locator(manualRefreshSelector)).toBeEnabled(); + await persistArtifact(page, `${basePath.slice(1)}-mobile-bottom-ref-buttons`); }); diff --git a/app/game-frontend/src/assets/styles/legacy-controls.css b/app/game-frontend/src/assets/styles/legacy-controls.css index 3112e8b0..47a9d79a 100644 --- a/app/game-frontend/src/assets/styles/legacy-controls.css +++ b/app/game-frontend/src/assets/styles/legacy-controls.css @@ -39,18 +39,23 @@ } /* - * Ref Bootstrap 5.2 + Lumen button family. The modifier describes the legacy - * semantic role; width and placement remain in the owning scoped component. + * Ref Bootstrap 5.2 + Lumen button family. This class owns the common raised + * edge and pressed movement. Semantic modifiers below only select face, edge, + * and text colors; width and fixed-height compensation stay with the owner. + * Existing semantic modifiers also opt in for backward compatibility. */ .legacy-button:is( + .legacy-button--lumen, .legacy-button--primary, .legacy-button--secondary, .legacy-button--danger, .legacy-button--info, - .legacy-button--navigation + .legacy-button--navigation, + .legacy-button--dark ) { --legacy-button-bg: var(--sammo-button-primary-bg); --legacy-button-border: var(--sammo-button-primary-border); + --legacy-button-color: #fff; min-height: 35.5px; margin-top: 0; border-color: var(--legacy-button-border); @@ -59,7 +64,7 @@ border-radius: 5.25px; padding: 5.25px 10.5px; background: var(--legacy-button-bg); - color: #fff; + color: var(--legacy-button-color); line-height: 21px; /* Ref's framework baseline for these controls. */ vertical-align: middle; @@ -85,12 +90,19 @@ --legacy-button-border: var(--sammo-button-navigation-border); } +.legacy-button.legacy-button--dark { + --legacy-button-bg: var(--sammo-button-dark-bg); + --legacy-button-border: var(--sammo-button-dark-border); +} + .legacy-button:is( + .legacy-button--lumen, .legacy-button--primary, .legacy-button--secondary, .legacy-button--danger, .legacy-button--info, - .legacy-button--navigation + .legacy-button--navigation, + .legacy-button--dark ):not(:disabled, [aria-disabled='true']):hover { margin-top: 1px; border-color: var(--legacy-button-border); @@ -99,11 +111,28 @@ } .legacy-button:is( + .legacy-button--lumen, .legacy-button--primary, .legacy-button--secondary, .legacy-button--danger, .legacy-button--info, - .legacy-button--navigation + .legacy-button--navigation, + .legacy-button--dark + ):not(:disabled, [aria-disabled='true'])[aria-expanded='true'] { + margin-top: 1px; + border-color: var(--legacy-button-border); + border-bottom-width: 3px; + background: var(--legacy-button-bg); +} + +.legacy-button:is( + .legacy-button--lumen, + .legacy-button--primary, + .legacy-button--secondary, + .legacy-button--danger, + .legacy-button--info, + .legacy-button--navigation, + .legacy-button--dark ):not(:disabled, [aria-disabled='true']):active { margin-top: 2px; border-color: var(--legacy-button-border); @@ -112,16 +141,14 @@ box-shadow: none; } -.legacy-button.legacy-button--primary:not(:disabled):active { - background: #2d4967; -} - .legacy-button:is( + .legacy-button--lumen, .legacy-button--primary, .legacy-button--secondary, .legacy-button--danger, .legacy-button--info, - .legacy-button--navigation + .legacy-button--navigation, + .legacy-button--dark ):focus { border-color: var(--legacy-button-border); background: var(--legacy-button-bg); diff --git a/app/game-frontend/src/assets/styles/tokens.css b/app/game-frontend/src/assets/styles/tokens.css index 82b1f1cc..894ccb92 100644 --- a/app/game-frontend/src/assets/styles/tokens.css +++ b/app/game-frontend/src/assets/styles/tokens.css @@ -21,6 +21,8 @@ --sammo-button-info-border: #2f89c5; --sammo-button-navigation-bg: #00582c; --sammo-button-navigation-border: #004f28; + --sammo-button-dark-bg: #212529; + --sammo-button-dark-border: #1e2125; --sammo-texture-walnut: url('https://sam-image.hided.net/game/back_walnut.jpg'); --sammo-texture-green: url('https://sam-image.hided.net/game/back_green.jpg'); --sammo-texture-blue: url('https://sam-image.hided.net/game/back_blue.jpg'); diff --git a/app/game-frontend/src/components/main/MainMobileBottomBar.vue b/app/game-frontend/src/components/main/MainMobileBottomBar.vue index 70bf3108..11deec41 100644 --- a/app/game-frontend/src/components/main/MainMobileBottomBar.vue +++ b/app/game-frontend/src/components/main/MainMobileBottomBar.vue @@ -54,7 +54,7 @@ const onQuick = (item: QuickNavigationItem) => { >