From 6bc35f78f1e868365367e5a21574ed22a672fdda Mon Sep 17 00:00:00 2001 From: hided62 Date: Mon, 17 Aug 2026 15:35:50 +0000 Subject: [PATCH] =?UTF-8?q?fix(game-ui):=20=EB=B6=84=ED=95=A0=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EC=9D=B4=EC=9D=8C=EC=83=88=EB=A5=BC=20=ED=95=98?= =?UTF-8?q?=EB=82=98=EB=A1=9C=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 공용 split primitive가 내부 모서리와 구분선을 소유하게 해 Lumen 규칙의 cascade 덮어쓰기를 막습니다. 공통·국가 메뉴의 desktop/mobile 및 상호작용 상태를 Chromium 회귀로 고정합니다. --- app/game-frontend/e2e/mainNavigation.spec.ts | 117 ++++++++++++++++++ .../src/assets/styles/legacy-controls.css | 15 +++ .../src/components/main/MainGlobalMenu.vue | 10 +- .../src/components/main/MainNationMenu.vue | 10 +- 4 files changed, 136 insertions(+), 16 deletions(-) diff --git a/app/game-frontend/e2e/mainNavigation.spec.ts b/app/game-frontend/e2e/mainNavigation.spec.ts index 57383be0..e8075a48 100644 --- a/app/game-frontend/e2e/mainNavigation.spec.ts +++ b/app/game-frontend/e2e/mainNavigation.spec.ts @@ -995,6 +995,123 @@ test('desktop menus preserve ref columns, prefix-safe routes, and controlled dro await persistArtifact(page, `${basePath.slice(1)}-desktop-1200`); }); +test('split buttons keep square inner corners and a single divider in every interaction state', async ({ + page, +}, testInfo) => { + const state: NavigationFixture = { + officerLevel: 5, + permission: 2, + nationLevel: 3, + stage: 1, + npcMode: 1, + scenarioTitle: '분할 버튼 이음새 검증 시나리오', + generalMeCalls: 0, + operations: [], + nationColor: '#663399', + }; + await installFixture(page, state); + if (artifactRoot) await mkdir(resolve(artifactRoot), { recursive: true }); + + const measure = (main: Locator, toggle: Locator) => + Promise.all([ + main.evaluate((element) => { + const rect = element.getBoundingClientRect(); + const style = getComputedStyle(element); + return { + rect: rect.toJSON(), + borderRightWidth: style.borderRightWidth, + borderTopLeftRadius: style.borderTopLeftRadius, + borderTopRightRadius: style.borderTopRightRadius, + borderBottomRightRadius: style.borderBottomRightRadius, + borderBottomLeftRadius: style.borderBottomLeftRadius, + }; + }), + toggle.evaluate((element) => { + const rect = element.getBoundingClientRect(); + const style = getComputedStyle(element); + return { + rect: rect.toJSON(), + borderLeftWidth: style.borderLeftWidth, + borderTopLeftRadius: style.borderTopLeftRadius, + borderTopRightRadius: style.borderTopRightRadius, + borderBottomRightRadius: style.borderBottomRightRadius, + borderBottomLeftRadius: style.borderBottomLeftRadius, + }; + }), + ]); + + const expectAttached = async (main: Locator, toggle: Locator) => { + const [mainStyle, toggleStyle] = await measure(main, toggle); + expect(mainStyle).toMatchObject({ + borderRightWidth: '1px', + borderTopLeftRadius: '5.25px', + borderTopRightRadius: '0px', + borderBottomRightRadius: '0px', + borderBottomLeftRadius: '5.25px', + }); + expect(toggleStyle).toMatchObject({ + borderLeftWidth: '0px', + borderTopLeftRadius: '0px', + borderTopRightRadius: '5.25px', + borderBottomRightRadius: '5.25px', + borderBottomLeftRadius: '0px', + }); + expect(toggleStyle.rect.left).toBeCloseTo(mainStyle.rect.right, 2); + }; + + for (const width of [1200, 500]) { + await page.setViewportSize({ width, height: 900 }); + await waitForMain(page); + const globalSplit = page.locator('.main-global-menu:visible .main-menu-split').first(); + const nationSplit = page.locator('.main-nation-menu:visible .nation-menu-split').first(); + const pairs: Array<[string, Locator, Locator]> = [ + [ + 'global', + globalSplit.locator('[data-navigation-id="board-community"]'), + globalSplit.locator('[data-menu-id="boards"]'), + ], + [ + 'nation', + nationSplit.locator('[data-navigation-id="auction-resource"]'), + nationSplit.locator('[data-menu-id="auction"]'), + ], + ]; + + for (const [label, main, toggle] of pairs) { + await expect(main).toBeVisible(); + await expect(toggle).toBeVisible(); + await page.mouse.move(width - 1, 899); + await expectAttached(main, toggle); + + await toggle.focus(); + await expect(toggle).toBeFocused(); + await expectAttached(main, toggle); + + await toggle.hover(); + await expectAttached(main, toggle); + + const box = await toggle.boundingBox(); + if (!box) throw new Error(`${label} split toggle is not measurable`); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await expectAttached(main, toggle); + await page.mouse.move(width - 1, 899); + await page.mouse.up(); + + await toggle.click(); + await expect(toggle).toHaveAttribute('aria-expanded', 'true'); + await expectAttached(main, toggle); + await page.keyboard.press('Escape'); + + await toggle.locator('..').screenshot({ + path: artifactRoot + ? resolve(artifactRoot, `${basePath.slice(1)}-${width}-${label}-split-button.png`) + : testInfo.outputPath(`${width}-${label}-split-button.png`), + }); + } + } +}); + test('the repeated bottom global menu opens upward on the mobile document', async ({ page }, testInfo) => { const state: NavigationFixture = { officerLevel: 5, diff --git a/app/game-frontend/src/assets/styles/legacy-controls.css b/app/game-frontend/src/assets/styles/legacy-controls.css index be50525f..6a2f54fb 100644 --- a/app/game-frontend/src/assets/styles/legacy-controls.css +++ b/app/game-frontend/src/assets/styles/legacy-controls.css @@ -153,3 +153,18 @@ border-color: var(--legacy-button-border); background: var(--legacy-button-bg); } + +/* + * Split controls share one outer silhouette. Keep the main control's right + * border as the subtle divider while removing every inner corner and the + * toggle's overlapping left border. These rules intentionally follow the + * Lumen family so its border shorthand cannot restore the inner rounding. + */ +.legacy-split-button > .main-menu-link { + border-radius: 5.25px 0 0 5.25px; +} + +.legacy-split-button > .legacy-split-button__toggle { + border-left-width: 0; + border-radius: 0 5.25px 5.25px 0; +} diff --git a/app/game-frontend/src/components/main/MainGlobalMenu.vue b/app/game-frontend/src/components/main/MainGlobalMenu.vue index 419fe7e5..5a331a20 100644 --- a/app/game-frontend/src/components/main/MainGlobalMenu.vue +++ b/app/game-frontend/src/components/main/MainGlobalMenu.vue @@ -59,7 +59,7 @@ const isActive = (link: MainNavigationLinkItem) => link.id === 'survey' && props -