diff --git a/app/game-frontend/e2e/diplomacy.spec.ts b/app/game-frontend/e2e/diplomacy.spec.ts index 4c9b6627..07a6da19 100644 --- a/app/game-frontend/e2e/diplomacy.spec.ts +++ b/app/game-frontend/e2e/diplomacy.spec.ts @@ -2,6 +2,7 @@ import { mkdir, writeFile } from 'node:fs/promises'; import { resolve } from 'node:path'; import { expect, test, type Page, type Route } from '@playwright/test'; +import { expectLumenButtonStates } from './lumenButton.js'; const basePath = `/${(process.env.PLAYWRIGHT_GAME_BASE_PATH ?? 'che').replace(/^\/+|\/+$/g, '')}`; const artifactRoot = process.env.DIPLOMACY_ARTIFACT_DIR ? resolve(process.env.DIPLOMACY_ARTIFACT_DIR) : null; @@ -103,7 +104,9 @@ for (const viewport of [ await expect(card.getByRole('link', { name: '자료' })).toHaveAttribute('href', 'https://example.com'); await expect(card.getByRole('link', { name: '자료' })).toHaveAttribute('rel', 'noopener noreferrer nofollow'); await expect(card.locator('.letter-text script, .letter-text svg, .letter-text math')).toHaveCount(0); - await expect(card.locator('.letter-text [onerror], .letter-text [onclick], .letter-text [style]')).toHaveCount(0); + await expect(card.locator('.letter-text [onerror], .letter-text [onclick], .letter-text [style]')).toHaveCount( + 0 + ); expect(await page.evaluate(() => (globalThis as Record).__diplomacyXss)).toBeUndefined(); const geometry = await card.evaluate((element) => { @@ -142,9 +145,7 @@ for (const viewport of [ } const send = page.getByRole('button', { name: '전송' }); - await send.focus(); - await expect(send).toBeFocused(); - await send.hover(); + await expectLumenButtonStates(page, send, 'rgb(55, 90, 127)'); await screenshot(page, `diplomacy-html-${basePath.slice(1)}-${viewport.name}.png`); }); } diff --git a/app/game-frontend/e2e/inGameInfo.spec.ts b/app/game-frontend/e2e/inGameInfo.spec.ts index 2270d1ce..e32182a9 100644 --- a/app/game-frontend/e2e/inGameInfo.spec.ts +++ b/app/game-frontend/e2e/inGameInfo.spec.ts @@ -2,6 +2,7 @@ import { expect, test, type Page, type Route } from '@playwright/test'; import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +import { expectLumenButtonStates } from './lumenButton.js'; const response = (data: unknown) => ({ result: { data } }); const artifactRoot = process.env.CITY_PARITY_ARTIFACT_DIR; @@ -493,6 +494,13 @@ test('four legacy menu pages keep the 1000px desktop table contract', async ({ p .first() .evaluate((el) => getComputedStyle(el).borderCollapse) ).toBe(borderCollapse); + if (path === 'nation/info' || path === 'current-city') { + await expectLumenButtonStates( + page, + page.getByRole('button', { name: '돌아가기' }).first(), + 'rgb(0, 88, 44)' + ); + } if (path === 'nation/info') { await expect(page.locator(selector)).toContainText('작 위호족'); await expect(page.locator(selector)).not.toContainText('작 위1'); diff --git a/app/game-frontend/e2e/inGameMenus.spec.ts b/app/game-frontend/e2e/inGameMenus.spec.ts index 5e0c0e8c..e38c78f8 100644 --- a/app/game-frontend/e2e/inGameMenus.spec.ts +++ b/app/game-frontend/e2e/inGameMenus.spec.ts @@ -2,6 +2,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { basename, resolve } from 'node:path'; import { expect, test, type Locator, type Page, type Route } from '@playwright/test'; import { gameBasePath, gameProfile, gameTrpcRoute } from './gameTestPaths.js'; +import { expectLumenButtonStates } from './lumenButton.js'; import { touchDrag } from './touchDrag.js'; const response = (data: unknown) => ({ result: { data } }); @@ -1320,6 +1321,7 @@ test('내 정보&설정 keeps desktop density and becomes a 390px horizontal-ide expect(desktop.customCssHeight).toBe(150); expect(desktop.backgroundImage).toContain('back_walnut.jpg'); expect(desktop.sectionBackgroundImage).toContain('back_green.jpg'); + await expectLumenButtonStates(page, page.locator('#set_my_setting'), 'rgb(34, 85, 0)'); await persistParityArtifact(page, 'core-my-page-desktop', desktop); const defenceSelect = page.locator('select').filter({ has: page.locator('option[value="999"]') }); diff --git a/app/game-frontend/e2e/lumenButton.ts b/app/game-frontend/e2e/lumenButton.ts new file mode 100644 index 00000000..191bbb64 --- /dev/null +++ b/app/game-frontend/e2e/lumenButton.ts @@ -0,0 +1,66 @@ +import { expect, type Locator, type Page } from '@playwright/test'; + +type ButtonGeometry = { + top: number; + bottom: number; + height: number; + marginTop: string; + borderBottomWidth: string; + borderRadius: string; + backgroundColor: string; +}; + +const measure = (control: Locator): Promise => + control.evaluate((element) => { + const rect = element.getBoundingClientRect(); + const style = getComputedStyle(element); + return { + top: rect.top, + bottom: rect.bottom, + height: rect.height, + marginTop: style.marginTop, + borderBottomWidth: style.borderBottomWidth, + borderRadius: style.borderRadius, + backgroundColor: style.backgroundColor, + }; + }); + +export const expectLumenButtonStates = async ( + page: Page, + control: Locator, + expectedBackground: string +): Promise<{ base: ButtonGeometry; hover: ButtonGeometry; active: ButtonGeometry }> => { + await expect(control).toBeVisible(); + await page.mouse.move(0, 0); + const base = await measure(control); + expect(base).toMatchObject({ + marginTop: '0px', + borderBottomWidth: '4px', + borderRadius: '5.25px', + backgroundColor: expectedBackground, + }); + + await control.hover(); + const hover = await measure(control); + expect(hover).toMatchObject({ marginTop: '1px', borderBottomWidth: '3px' }); + expect(hover.top).toBeCloseTo(base.top + 1, 1); + expect(hover.bottom).toBeCloseTo(base.bottom, 1); + + const box = await control.boundingBox(); + if (!box) throw new Error('Lumen button is not measurable'); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + const active = await measure(control); + expect(active).toMatchObject({ marginTop: '2px', borderBottomWidth: '2px' }); + expect(active.top).toBeCloseTo(base.top + 2, 1); + expect(active.bottom).toBeCloseTo(base.bottom, 1); + await page.mouse.move(0, 0); + await page.mouse.up(); + + await page.keyboard.press('Tab'); + await control.focus(); + await expect(control).toBeFocused(); + await expect.poll(() => control.evaluate((element) => getComputedStyle(element).boxShadow)).not.toBe('none'); + + return { base, hover, active }; +}; diff --git a/app/game-frontend/e2e/nationCityOfficeIntegration.spec.ts b/app/game-frontend/e2e/nationCityOfficeIntegration.spec.ts index 04aff5ae..08d20ab1 100644 --- a/app/game-frontend/e2e/nationCityOfficeIntegration.spec.ts +++ b/app/game-frontend/e2e/nationCityOfficeIntegration.spec.ts @@ -1,6 +1,7 @@ import { expect, test, type Page, type Route } from '@playwright/test'; import { gameProfile, gameTrpcRoute } from './gameTestPaths.js'; +import { expectLumenButtonStates } from './lumenButton.js'; type Role = 'head' | 'member'; type AppointmentInput = { destGeneralId: number; destCityId: number; officerLevel: number }; @@ -352,6 +353,7 @@ test('암행부 행을 도시별로 나누고 수뇌의 인사부 즉시 임명 await expect(page.locator('.nation-cities-page')).toBeVisible(); await expect(page.locator('.city-user-table')).toHaveCount(0); await expect(page.getByRole('button', { name: '인사부 연동' })).toHaveCount(0); + await expectLumenButtonStates(page, page.getByRole('button', { name: '암행부 연동' }), 'rgb(55, 90, 127)'); const citySort = page.locator('#nation-city-sort'); await expect(citySort).toHaveCSS('background-color', 'rgb(24, 35, 29)'); diff --git a/app/game-frontend/e2e/nationOffices.spec.ts b/app/game-frontend/e2e/nationOffices.spec.ts index 12716a2c..1c626da9 100644 --- a/app/game-frontend/e2e/nationOffices.spec.ts +++ b/app/game-frontend/e2e/nationOffices.spec.ts @@ -3,6 +3,7 @@ import { mkdir, readFile } from 'node:fs/promises'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { gameProfile, gameTrpcRoute } from './gameTestPaths.js'; +import { expectLumenButtonStates } from './lumenButton.js'; type Role = 'leader' | 'head' | 'member'; type FixtureState = { @@ -290,6 +291,7 @@ test('personnel keeps the desktop frame while exposing row-level appointment con await page.setViewportSize({ width: 1000, height: 900 }); await gotoOffice(page, 'nation/personnel'); await expect(page.getByText('작위검증국')).toBeVisible(); + await expectLumenButtonStates(page, page.locator('.personnel-change-button').first(), 'rgb(49, 91, 61)'); const computed = await page.locator('#personnel-container').evaluate((container) => { const box = (selector?: string) => { @@ -339,7 +341,7 @@ test('personnel keeps the desktop frame while exposing row-level appointment con await changeButton.hover(); expect(await changeButton.evaluate((button) => getComputedStyle(button).cursor)).toBe('pointer'); await changeButton.focus(); - expect(await changeButton.evaluate((button) => getComputedStyle(button).outlineStyle)).not.toBe('none'); + expect(await changeButton.evaluate((button) => getComputedStyle(button).boxShadow)).not.toBe('none'); await expect(page.getByRole('button', { name: '허창 태수 변경하기', exact: true })).toBeVisible(); await expect(page.getByRole('button', { name: '허창 군사 변경하기', exact: true })).toHaveCount(0); await screenshot(page, 'core-personnel-desktop-leader.png'); diff --git a/app/game-frontend/src/views/CurrentCityView.vue b/app/game-frontend/src/views/CurrentCityView.vue index 01ea4136..beddbb04 100644 --- a/app/game-frontend/src/views/CurrentCityView.vue +++ b/app/game-frontend/src/views/CurrentCityView.vue @@ -92,7 +92,11 @@ const commandBrief = (command: ReservedCommand): string => - 도 시 정 보
@@ -129,7 +133,15 @@ const commandBrief = (command: ReservedCommand): string => - +
+ +
@@ -310,7 +322,15 @@ const commandBrief = (command: ReservedCommand): string => - + @@ -390,7 +394,7 @@ onBeforeUnmount(() => {
동작
- +
@@ -577,22 +581,41 @@ onBeforeUnmount(() => {
동작
- - - + + 돌아가기

- 삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 : - HideD(hided62@gmail.com) / + 삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 : HideD(hided62@gmail.com) + / Credit
@@ -659,17 +683,6 @@ onBeforeUnmount(() => { text-align: left; } -.legacy-button { - min-height: 34px; - padding: 5px 10px; - border: 1px solid #2d5d7f; - border-radius: 4px; - background: #315f86; - color: #fff; - font-weight: 700; - text-decoration: none; -} - .panel { width: 1000px; margin: 10px auto; @@ -749,7 +762,7 @@ onBeforeUnmount(() => { } .editor-toolbar button, -.diplomacy-view button { +.diplomacy-view button:not(.legacy-button) { border: 1px solid #aaa; border-radius: 0; background: #666; @@ -758,16 +771,6 @@ onBeforeUnmount(() => { cursor: pointer; } -.diplomacy-view .legacy-button { - border: 0; - border-radius: 5.25px; - padding: 5.25px 10.5px; - background-color: rgb(55 90 127); - color: #fff; - font-weight: 700; - line-height: 21px; -} - .editor-toolbar button.active { background: #315f86; } diff --git a/app/game-frontend/src/views/MyPageView.vue b/app/game-frontend/src/views/MyPageView.vue index a69960aa..bb647408 100644 --- a/app/game-frontend/src/views/MyPageView.vue +++ b/app/game-frontend/src/views/MyPageView.vue @@ -399,10 +399,14 @@ onMounted(() => { 내 정 보
- 지난 플레이 + + 지난 플레이 +
@@ -491,7 +495,7 @@ onMounted(() => { +
가오픈 기간 내 장수 삭제 ({{ formatDieOnPrestartAvailableAt }} 부터)
- +
서버 개시 이전 거병(2턴부터 건국 가능)
@@ -618,6 +640,7 @@ onMounted(() => { +

항목을 끌어 놓거나 위·아래 버튼으로 상대 순서를 바꿉니다.

- +
- -
- + +
+ + +
@@ -772,8 +805,7 @@ onMounted(() => { display: flex; gap: 4px; } -.legacy-button, -button, +button:not(.legacy-button), select, textarea { border: 1px solid #777; @@ -782,15 +814,8 @@ textarea { background: #6b6b6b; font: inherit; } -.legacy-button { - min-height: 34px; - padding: 5px 10px; - border-color: #2d5d7f; - border-radius: 4px; - background: #315f86; - color: #fff; - font-weight: 700; - text-decoration: none; +.legacy-page .legacy-button, +.mobile-layout-dialog .legacy-button { letter-spacing: 0; } button { @@ -871,10 +896,17 @@ button:disabled { color: orange; } .action-button { + --legacy-button-height: 30px; + --legacy-button-bg: #225500; + --legacy-button-border: #1f4d00; + position: relative; + top: 4px; width: 160px; - height: 30px; - margin: 4px 0; - background: #225500; + margin-bottom: 8px; +} +.action-button.legacy-button--danger { + --legacy-button-bg: var(--sammo-button-danger-bg); + --legacy-button-border: var(--sammo-button-danger-border); } .select-general-link { display: inline-flex; @@ -909,7 +941,6 @@ button:disabled { } .mobile-layout-open { min-height: 34px; - background: #315f86; font-weight: 700; } .mobile-layout-dialog { @@ -1000,7 +1031,6 @@ button:disabled { .mobile-layout-move-buttons button { width: 36px; min-height: 34px; - background: #315f86; font-weight: 700; } .mobile-layout-dialog__actions { @@ -1013,7 +1043,6 @@ button:disabled { padding: 4px 10px; } .mobile-layout-dialog__actions .mobile-layout-apply { - background: #225500; font-weight: 700; } .button-group { diff --git a/app/game-frontend/src/views/NationCitiesView.vue b/app/game-frontend/src/views/NationCitiesView.vue index 2648e12d..cd3197ca 100644 --- a/app/game-frontend/src/views/NationCitiesView.vue +++ b/app/game-frontend/src/views/NationCitiesView.vue @@ -294,7 +294,11 @@ onMounted(async () => {
@@ -309,12 +313,18 @@ onMounted(async () => { @update:model-value="updateSelectedSort" @submit="applySelectedSort" /> - @@ -583,7 +647,7 @@ onMounted(async () => { v-for="level in [4, 3, 2] as const" :key="level" type="button" - class="appointment-button for-duty" + class="legacy-button legacy-button--primary appointment-button for-duty" :class="[`mode-${level}`, { 'chief-target': isChief(general.id) }]" :disabled=" !canAppoint(city.id, general.id, level) || pendingAppointment !== '' @@ -642,7 +706,15 @@ onMounted(async () => { - +