Merge remote-tracking branch 'origin/main' into feature/mobile-auto-refresh-control-20260813
This commit is contained in:
@@ -578,7 +578,7 @@ test('enters general and nation command arguments and sends exact values', async
|
||||
expect(Number.parseFloat(geometry.fontSize)).toBeGreaterThanOrEqual(10);
|
||||
});
|
||||
|
||||
test('shows Ref recruitment details and preserves the 1000px desktop and 500px mobile information layouts', async ({
|
||||
test('uses a Ref-style full recruitment page without horizontal overflow on desktop or mobile', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
const requests = await install(page);
|
||||
@@ -589,7 +589,10 @@ test('shows Ref recruitment details and preserves the 1000px desktop and 500px m
|
||||
let picker = page.getByTestId('command-picker');
|
||||
await picker.getByRole('button', { name: '내정', exact: true }).click();
|
||||
await picker.getByRole('button', { name: '징병', exact: true }).click();
|
||||
let form = picker.getByTestId('recruitment-command-form');
|
||||
await expect(picker).toHaveAttribute('role', 'dialog');
|
||||
await expect(picker).toHaveAttribute('aria-modal', 'true');
|
||||
await expect(picker.getByRole('button', { name: '명령 입력 닫기', exact: true })).toBeFocused();
|
||||
const form = picker.getByTestId('recruitment-command-form');
|
||||
await expect(form).toContainText('현재 기술력 : 1등급');
|
||||
await expect(form).toContainText('공격');
|
||||
await expect(form).toContainText('방어');
|
||||
@@ -619,19 +622,23 @@ test('shows Ref recruitment details and preserves the 1000px desktop and 500px m
|
||||
})
|
||||
: null;
|
||||
return {
|
||||
overlay: element.closest('[data-testid="command-picker"]')?.getBoundingClientRect().toJSON(),
|
||||
formWidth: element.getBoundingClientRect().width,
|
||||
rowHeight: row?.getBoundingClientRect().height ?? 0,
|
||||
infoWidth: info?.getBoundingClientRect().width ?? 0,
|
||||
imageNaturalSize: naturalSize,
|
||||
scrollWidth: element.scrollWidth,
|
||||
clientWidth: element.clientWidth,
|
||||
bodyOverflow: getComputedStyle(document.body).overflow,
|
||||
};
|
||||
});
|
||||
expect(desktopGeometry.formWidth).toBeCloseTo(986, 0);
|
||||
expect(desktopGeometry.overlay).toMatchObject({ x: 0, y: 0, width: 1200, height: 900 });
|
||||
expect(desktopGeometry.formWidth).toBe(1000);
|
||||
expect(desktopGeometry.rowHeight).toBeGreaterThanOrEqual(64);
|
||||
expect(desktopGeometry.infoWidth).toBeCloseTo(250, 0);
|
||||
expect(desktopGeometry.imageNaturalSize).toEqual({ width: 128, height: 128 });
|
||||
expect(desktopGeometry.scrollWidth).toBe(desktopGeometry.clientWidth);
|
||||
expect(desktopGeometry.bodyOverflow).toBe('hidden');
|
||||
|
||||
const infantry = form.getByRole('button', { name: '보병 선택 가능', exact: true });
|
||||
await infantry.getByRole('button', { name: '절반', exact: true }).click();
|
||||
@@ -641,49 +648,116 @@ test('shows Ref recruitment details and preserves the 1000px desktop and 500px m
|
||||
expect(JSON.stringify(requests)).toContain('"crewType":1100');
|
||||
expect(JSON.stringify(requests)).toContain('"amount":3500');
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.setViewportSize({ width: 500, height: 844 });
|
||||
await page.getByRole('button', { name: '2턴 명령 입력', exact: true }).click();
|
||||
picker = page.getByTestId('command-picker');
|
||||
await picker.getByRole('button', { name: '내정', exact: true }).click();
|
||||
await picker.getByRole('button', { name: '징병', exact: true }).click();
|
||||
form = picker.getByTestId('recruitment-command-form');
|
||||
await page.evaluate(() => window.scrollTo(0, 0));
|
||||
const mobileGeometry = await form.evaluate((element) => {
|
||||
const row = element.querySelector('.crew-row');
|
||||
const image = row?.querySelector('.crew-image');
|
||||
const info = row?.querySelector('.crew-info');
|
||||
const selectedPanel = element.querySelector('.mobile-selected-panel');
|
||||
await expect(picker.getByRole('button', { name: '명령 입력 닫기', exact: true })).toBeFocused();
|
||||
const referenceWidthGeometry = await picker.evaluate((element) => {
|
||||
const formElement = element.querySelector<HTMLElement>('[data-testid="recruitment-command-form"]')!;
|
||||
const row = formElement.querySelector<HTMLElement>('.crew-row')!;
|
||||
const selectedPanel = formElement.querySelector<HTMLElement>('.mobile-selected-panel')!;
|
||||
return {
|
||||
formLeft: element.getBoundingClientRect().left,
|
||||
formWidth: element.getBoundingClientRect().width,
|
||||
rowWidth: row?.getBoundingClientRect().width ?? 0,
|
||||
rowHeight: row?.getBoundingClientRect().height ?? 0,
|
||||
imageWidth: image?.getBoundingClientRect().width ?? 0,
|
||||
infoWidth: info?.getBoundingClientRect().width ?? 0,
|
||||
selectedDisplay: selectedPanel ? getComputedStyle(selectedPanel).display : '',
|
||||
overlayWidth: element.getBoundingClientRect().width,
|
||||
rowWidth: row.getBoundingClientRect().width,
|
||||
rowGridColumns: getComputedStyle(row).gridTemplateColumns,
|
||||
selectedWidth: selectedPanel.getBoundingClientRect().width,
|
||||
selectedGridColumns: getComputedStyle(selectedPanel).gridTemplateColumns,
|
||||
scrollWidth: element.scrollWidth,
|
||||
clientWidth: element.clientWidth,
|
||||
documentScrollWidth: document.documentElement.scrollWidth,
|
||||
};
|
||||
});
|
||||
expect(mobileGeometry.formLeft).toBe(0);
|
||||
expect(mobileGeometry.formWidth).toBe(500);
|
||||
expect(mobileGeometry.rowWidth).toBe(500);
|
||||
expect(referenceWidthGeometry).toMatchObject({
|
||||
overlayWidth: 500,
|
||||
rowWidth: 500,
|
||||
rowGridColumns: '64px 76px 30px 30px 30px 270px',
|
||||
selectedWidth: 500,
|
||||
selectedGridColumns: '64px 76px 270px 90px',
|
||||
scrollWidth: 500,
|
||||
clientWidth: 500,
|
||||
});
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
const mobileGeometry = await picker.evaluate((element) => {
|
||||
const formElement = element.querySelector<HTMLElement>('[data-testid="recruitment-command-form"]')!;
|
||||
const row = formElement.querySelector('.crew-row');
|
||||
const image = row?.querySelector('.crew-image');
|
||||
const info = row?.querySelector('.crew-info');
|
||||
const selectedPanel = formElement.querySelector('.mobile-selected-panel');
|
||||
const rect = element.getBoundingClientRect();
|
||||
const bottomElement = document.elementFromPoint(window.innerWidth / 2, window.innerHeight - 1);
|
||||
return {
|
||||
overlay: rect.toJSON(),
|
||||
overlayCoversBottom: bottomElement instanceof Element && element.contains(bottomElement),
|
||||
formLeft: formElement.getBoundingClientRect().left,
|
||||
formWidth: formElement.getBoundingClientRect().width,
|
||||
rowWidth: row?.getBoundingClientRect().width ?? 0,
|
||||
rowHeight: row?.getBoundingClientRect().height ?? 0,
|
||||
rowGridColumns: row ? getComputedStyle(row).gridTemplateColumns : '',
|
||||
rowGridRows: row ? getComputedStyle(row).gridTemplateRows : '',
|
||||
imageWidth: image?.getBoundingClientRect().width ?? 0,
|
||||
infoWidth: info?.getBoundingClientRect().width ?? 0,
|
||||
selectedWidth: selectedPanel?.getBoundingClientRect().width ?? 0,
|
||||
selectedDisplay: selectedPanel ? getComputedStyle(selectedPanel).display : '',
|
||||
actionsBottom: element.querySelector<HTMLElement>('.picker-actions')?.getBoundingClientRect().bottom ?? 0,
|
||||
viewportHeight: window.innerHeight,
|
||||
scrollWidth: element.scrollWidth,
|
||||
clientWidth: element.clientWidth,
|
||||
bodyOverflow: getComputedStyle(document.body).overflow,
|
||||
};
|
||||
});
|
||||
expect(mobileGeometry.overlay).toMatchObject({ x: 0, y: 0, width: 390, height: 844 });
|
||||
expect(mobileGeometry.overlayCoversBottom).toBe(true);
|
||||
expect(mobileGeometry.rowGridColumns).toBe('64px 76px 30px 30px 30px 160px');
|
||||
expect(mobileGeometry).toMatchObject({
|
||||
formLeft: 0,
|
||||
formWidth: 390,
|
||||
rowWidth: 390,
|
||||
imageWidth: 64,
|
||||
selectedWidth: 390,
|
||||
selectedDisplay: 'grid',
|
||||
actionsBottom: 844,
|
||||
viewportHeight: 844,
|
||||
bodyOverflow: 'hidden',
|
||||
});
|
||||
expect(mobileGeometry.rowHeight).toBeGreaterThanOrEqual(64);
|
||||
expect(mobileGeometry.rowHeight).toBeLessThanOrEqual(66);
|
||||
expect(mobileGeometry.imageWidth).toBe(64);
|
||||
expect(mobileGeometry.infoWidth).toBe(270);
|
||||
expect(mobileGeometry.selectedDisplay).toBe('grid');
|
||||
expect(mobileGeometry.infoWidth).toBeGreaterThanOrEqual(150);
|
||||
expect(mobileGeometry.scrollWidth).toBe(mobileGeometry.clientWidth);
|
||||
expect(mobileGeometry.documentScrollWidth).toBeGreaterThanOrEqual(500);
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 360 });
|
||||
await picker.evaluate((element) => (element.scrollTop = 160));
|
||||
const stickyGeometry = await picker.evaluate((element) => {
|
||||
const header = element.querySelector<HTMLElement>(':scope > header')!;
|
||||
const listFront = element.querySelector<HTMLElement>('.recruitment-list-front')!;
|
||||
const actions = element.querySelector<HTMLElement>('.picker-actions')!;
|
||||
return {
|
||||
scrollTop: element.scrollTop,
|
||||
headerTop: header.getBoundingClientRect().top,
|
||||
listFrontTop: listFront.getBoundingClientRect().top,
|
||||
actionsBottom: actions.getBoundingClientRect().bottom,
|
||||
viewportHeight: window.innerHeight,
|
||||
};
|
||||
});
|
||||
expect(stickyGeometry.scrollTop).toBeGreaterThan(0);
|
||||
expect(stickyGeometry.headerTop).toBe(0);
|
||||
expect(stickyGeometry.listFrontTop).toBeGreaterThanOrEqual(44);
|
||||
expect(stickyGeometry.actionsBottom).toBe(stickyGeometry.viewportHeight);
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await picker.evaluate((element) => (element.scrollTop = 0));
|
||||
await page.screenshot({ path: testInfo.outputPath('recruitment-mobile.png') });
|
||||
|
||||
await picker.getByRole('button', { name: '명령 다시 선택', exact: true }).click();
|
||||
await expect.poll(() => page.evaluate(() => getComputedStyle(document.body).overflow)).not.toBe('hidden');
|
||||
await picker.getByRole('button', { name: '내정', exact: true }).click();
|
||||
await picker.getByRole('button', { name: '모병', exact: true }).click();
|
||||
const mercenaryForm = picker.getByTestId('recruitment-command-form');
|
||||
await expect(mercenaryForm).toContainText('모병은 가격 2배의 자금이 소요됩니다.');
|
||||
await expect(mercenaryForm.locator('.mobile-selected-panel output')).toHaveText('1,346금');
|
||||
await page.keyboard.press('Escape');
|
||||
await expect(picker).toHaveCount(0);
|
||||
await expect.poll(() => page.evaluate(() => getComputedStyle(document.body).overflow)).not.toBe('hidden');
|
||||
});
|
||||
|
||||
test('uses the map to choose a nation target in the chief command window', async ({ page }) => {
|
||||
|
||||
@@ -423,14 +423,16 @@ watch(
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
@media (max-width: 939px) {
|
||||
.recruitment-command-form {
|
||||
width: 500px;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow-x: clip;
|
||||
}
|
||||
.recruitment-list-front {
|
||||
position: sticky;
|
||||
z-index: 5;
|
||||
top: 0;
|
||||
top: 44px;
|
||||
background: #1d1d1d;
|
||||
}
|
||||
.recruitment-status {
|
||||
@@ -438,7 +440,7 @@ watch(
|
||||
}
|
||||
.mobile-selected-panel {
|
||||
display: grid;
|
||||
grid-template-columns: 64px 76px 270px 90px;
|
||||
grid-template-columns: 64px minmax(58px, 76px) minmax(0, 1fr) minmax(64px, 90px);
|
||||
min-height: 64px;
|
||||
align-items: stretch;
|
||||
}
|
||||
@@ -458,10 +460,11 @@ watch(
|
||||
text-align: right;
|
||||
}
|
||||
.crew-grid {
|
||||
grid-template:
|
||||
'image name attack defence speed info' 32px
|
||||
'image name avoid cost rice info' 32px /
|
||||
64px 76px 30px 30px 30px 270px;
|
||||
grid-template-areas:
|
||||
'image name attack defence speed info'
|
||||
'image name avoid cost rice info';
|
||||
grid-template-columns: 64px minmax(58px, 76px) repeat(3, minmax(26px, 30px)) minmax(0, 1fr);
|
||||
grid-template-rows: 32px 32px;
|
||||
}
|
||||
.crew-grid .crew-image {
|
||||
grid-area: image;
|
||||
@@ -518,7 +521,27 @@ watch(
|
||||
}
|
||||
.crew-info {
|
||||
overflow: hidden;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 440px) {
|
||||
.mobile-selected-panel {
|
||||
grid-template-columns: 64px minmax(58px, 76px) minmax(0, 1fr) 72px;
|
||||
min-height: 86px;
|
||||
}
|
||||
.mobile-selected-panel .amount-panel label {
|
||||
grid-template-columns: auto minmax(34px, 1fr) auto;
|
||||
}
|
||||
.mobile-selected-panel output {
|
||||
grid-column: 1 / -1;
|
||||
min-height: 22px;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
.mobile-selected-panel .quick-buttons button,
|
||||
.mobile-selected-panel .submit-recruit {
|
||||
min-height: 32px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, shallowRef, watch } from 'vue';
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
|
||||
import CommandArgumentForm from '../main/CommandArgumentForm.vue';
|
||||
import CommandSelectForm from '../main/CommandSelectForm.vue';
|
||||
import { commandArgumentPresentation } from './commandArgumentPresentation';
|
||||
@@ -70,6 +70,7 @@ const commandArgsValid = ref(false);
|
||||
const expanded = ref(false);
|
||||
const menuRevision = ref(0);
|
||||
const pendingReservation = ref<CommandPatternEntry | null>(null);
|
||||
const pickerElement = ref<HTMLElement | null>(null);
|
||||
const collapsedRowCount = 15;
|
||||
|
||||
const loadStorage = (key: string) => {
|
||||
@@ -136,6 +137,7 @@ const quickPickerTop = computed(() => `${70 + (quickTarget.value ?? 0) * 34.4}px
|
||||
const isRecruitmentCommand = computed(
|
||||
() => selectedCommand.value?.key === 'che_징병' || selectedCommand.value?.key === 'che_모병'
|
||||
);
|
||||
const isRecruitmentOverlayOpen = computed(() => pickerOpen.value && isRecruitmentCommand.value);
|
||||
const rowLabel = (row: ReservedCommandRow): string => row.label ?? labelMap.value.get(row.action) ?? row.action;
|
||||
const selectedIndices = () => normalizedSelection(selected.value, previousSelected.value, props.rows.length);
|
||||
const pattern = () => extractPattern(props.rows, selectedIndices());
|
||||
@@ -172,6 +174,46 @@ const closePicker = () => {
|
||||
quickTarget.value = null;
|
||||
selectedCommand.value = null;
|
||||
};
|
||||
|
||||
let previousBodyOverflow: string | null = null;
|
||||
const restoreBodyScroll = () => {
|
||||
if (previousBodyOverflow === null) return;
|
||||
document.body.style.overflow = previousBodyOverflow;
|
||||
previousBodyOverflow = null;
|
||||
};
|
||||
|
||||
watch(isRecruitmentOverlayOpen, async (open) => {
|
||||
if (!open) {
|
||||
restoreBodyScroll();
|
||||
return;
|
||||
}
|
||||
if (previousBodyOverflow === null) previousBodyOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = 'hidden';
|
||||
await nextTick();
|
||||
pickerElement.value?.querySelector<HTMLElement>('[data-picker-close]')?.focus();
|
||||
});
|
||||
|
||||
onBeforeUnmount(restoreBodyScroll);
|
||||
|
||||
const trapRecruitmentFocus = (event: KeyboardEvent) => {
|
||||
if (!isRecruitmentOverlayOpen.value || event.key !== 'Tab' || !pickerElement.value) return;
|
||||
const focusable = [
|
||||
...pickerElement.value.querySelectorAll<HTMLElement>(
|
||||
'button:not(:disabled), input:not(:disabled), select:not(:disabled), [tabindex="0"]'
|
||||
),
|
||||
].filter((element) => element.offsetParent !== null);
|
||||
if (!focusable.length) return;
|
||||
const first = focusable[0];
|
||||
const last = focusable.at(-1);
|
||||
if (!first || !last) return;
|
||||
if (event.shiftKey && document.activeElement === first) {
|
||||
event.preventDefault();
|
||||
last.focus();
|
||||
} else if (!event.shiftKey && document.activeElement === last) {
|
||||
event.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
};
|
||||
const togglePicker = (turnIndex?: number) => {
|
||||
const target = turnIndex ?? null;
|
||||
if (pickerOpen.value && quickTarget.value === target) {
|
||||
@@ -600,65 +642,79 @@ const clickOutsideMenu = (event: Event) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="pickerOpen"
|
||||
class="command-picker"
|
||||
:class="{ 'recruitment-picker': isRecruitmentCommand }"
|
||||
data-testid="command-picker"
|
||||
:style="quickTarget === null || props.compact ? undefined : { top: quickPickerTop }"
|
||||
>
|
||||
<header>
|
||||
<strong>{{ quickTarget === null ? '선택한 턴' : `${quickTarget + 1}턴` }} 명령 입력</strong
|
||||
><button type="button" aria-label="명령 입력 닫기" @click="closePicker">×</button>
|
||||
</header>
|
||||
<CommandSelectForm
|
||||
v-if="!selectedCommand"
|
||||
:command-table="scopedTable"
|
||||
:loading="props.loading"
|
||||
:scope="props.scope"
|
||||
:active-category="activeCategory"
|
||||
:allow-blocked="true"
|
||||
@update:active-category="activeCategory = $event"
|
||||
@select="selectCommand"
|
||||
/>
|
||||
<template v-else>
|
||||
<div class="selected-command">
|
||||
<strong>{{ selectedCommand.name }}</strong>
|
||||
<small v-if="selectedCommand.reason"
|
||||
>현재 상태: {{ selectedCommand.reason }} · 예약 입력은 가능합니다.</small
|
||||
>
|
||||
</div>
|
||||
<RecruitmentCommandForm
|
||||
v-if="
|
||||
isRecruitmentCommand &&
|
||||
props.commandTable?.inputOptions.recruitment &&
|
||||
(selectedCommand.key === 'che_징병' || selectedCommand.key === 'che_모병')
|
||||
"
|
||||
:command-key="selectedCommand.key"
|
||||
:info="props.commandTable.inputOptions.recruitment"
|
||||
@update:args="commandArgs = $event"
|
||||
@update:valid="commandArgsValid = $event"
|
||||
@submit="submitCommand"
|
||||
<Teleport to="body" :disabled="!isRecruitmentCommand">
|
||||
<div
|
||||
v-if="pickerOpen"
|
||||
ref="pickerElement"
|
||||
class="command-picker"
|
||||
:class="{ 'recruitment-picker': isRecruitmentCommand }"
|
||||
data-testid="command-picker"
|
||||
:style="isRecruitmentCommand || quickTarget === null || props.compact ? undefined : { top: quickPickerTop }"
|
||||
:role="isRecruitmentCommand ? 'dialog' : undefined"
|
||||
:aria-modal="isRecruitmentCommand ? 'true' : undefined"
|
||||
:aria-label="
|
||||
isRecruitmentCommand
|
||||
? `${selectedCommand?.name ?? ''} ${quickTarget === null ? '선택한 턴' : `${quickTarget + 1}턴`} 명령 입력`
|
||||
: undefined
|
||||
"
|
||||
@keydown.esc.stop.prevent="closePicker"
|
||||
@keydown="trapRecruitmentFocus"
|
||||
>
|
||||
<header>
|
||||
<strong
|
||||
><template v-if="isRecruitmentCommand">{{ selectedCommand?.name }} · </template
|
||||
>{{ quickTarget === null ? '선택한 턴' : `${quickTarget + 1}턴` }} 명령 입력</strong
|
||||
><button data-picker-close type="button" aria-label="명령 입력 닫기" @click="closePicker">×</button>
|
||||
</header>
|
||||
<CommandSelectForm
|
||||
v-if="!selectedCommand"
|
||||
:command-table="scopedTable"
|
||||
:loading="props.loading"
|
||||
:scope="props.scope"
|
||||
:active-category="activeCategory"
|
||||
:allow-blocked="true"
|
||||
@update:active-category="activeCategory = $event"
|
||||
@select="selectCommand"
|
||||
/>
|
||||
<CommandArgumentForm
|
||||
v-else-if="selectedCommand.reqArg && props.commandTable"
|
||||
:command-key="selectedCommand.key"
|
||||
:fields="selectedCommand.inputFields"
|
||||
:options="props.commandTable.inputOptions"
|
||||
:map-data="props.mapData"
|
||||
:map-layout="props.mapLayout"
|
||||
@update:args="commandArgs = $event"
|
||||
@update:valid="commandArgsValid = $event"
|
||||
/>
|
||||
<div class="picker-actions">
|
||||
<button :disabled="Boolean(pendingReservation)" @click="selectedCommand = null">
|
||||
명령 다시 선택</button
|
||||
><button :disabled="!commandArgsValid || Boolean(pendingReservation)" @click="submitCommand">
|
||||
{{ pendingReservation ? '저장 중' : '입력' }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="selected-command">
|
||||
<strong>{{ selectedCommand.name }}</strong>
|
||||
<small v-if="selectedCommand.reason"
|
||||
>현재 상태: {{ selectedCommand.reason }} · 예약 입력은 가능합니다.</small
|
||||
>
|
||||
</div>
|
||||
<RecruitmentCommandForm
|
||||
v-if="
|
||||
isRecruitmentCommand &&
|
||||
props.commandTable?.inputOptions.recruitment &&
|
||||
(selectedCommand.key === 'che_징병' || selectedCommand.key === 'che_모병')
|
||||
"
|
||||
:command-key="selectedCommand.key"
|
||||
:info="props.commandTable.inputOptions.recruitment"
|
||||
@update:args="commandArgs = $event"
|
||||
@update:valid="commandArgsValid = $event"
|
||||
@submit="submitCommand"
|
||||
/>
|
||||
<CommandArgumentForm
|
||||
v-else-if="selectedCommand.reqArg && props.commandTable"
|
||||
:command-key="selectedCommand.key"
|
||||
:fields="selectedCommand.inputFields"
|
||||
:options="props.commandTable.inputOptions"
|
||||
:map-data="props.mapData"
|
||||
:map-layout="props.mapLayout"
|
||||
@update:args="commandArgs = $event"
|
||||
@update:valid="commandArgsValid = $event"
|
||||
/>
|
||||
<div class="picker-actions">
|
||||
<button :disabled="Boolean(pendingReservation)" @click="selectedCommand = null">
|
||||
명령 다시 선택</button
|
||||
><button :disabled="!commandArgsValid || Boolean(pendingReservation)" @click="submitCommand">
|
||||
{{ pendingReservation ? '저장 중' : '입력' }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</Teleport>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
@@ -967,18 +1023,59 @@ const clickOutsideMenu = (event: Event) => {
|
||||
max-height: 344px;
|
||||
}
|
||||
|
||||
.reserved-command-editor .command-picker.recruitment-picker {
|
||||
.command-picker.recruitment-picker {
|
||||
position: fixed;
|
||||
z-index: 1100;
|
||||
top: 76px;
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
left: 50%;
|
||||
width: 1000px;
|
||||
height: auto;
|
||||
max-height: calc(100vh - 82px);
|
||||
overflow: auto;
|
||||
transform: translateX(-50%);
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
max-height: none;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: #1d1d1d;
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
.command-picker.recruitment-picker > header {
|
||||
position: sticky;
|
||||
z-index: 30;
|
||||
top: 0;
|
||||
box-sizing: border-box;
|
||||
min-height: 44px;
|
||||
padding: 6px 8px;
|
||||
border-bottom: 1px solid #777;
|
||||
background: #302016 var(--sammo-texture-walnut);
|
||||
}
|
||||
.command-picker.recruitment-picker > header button {
|
||||
min-width: 36px;
|
||||
min-height: 32px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.command-picker.recruitment-picker .selected-command,
|
||||
.command-picker.recruitment-picker :deep(.recruitment-command-form),
|
||||
.command-picker.recruitment-picker .picker-actions {
|
||||
width: min(100%, 1000px);
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
.command-picker.recruitment-picker :deep(.recruitment-command-form) {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
.command-picker.recruitment-picker .picker-actions {
|
||||
position: sticky;
|
||||
z-index: 30;
|
||||
bottom: 0;
|
||||
box-sizing: border-box;
|
||||
margin-top: 0;
|
||||
padding: 6px;
|
||||
border-top: 1px solid #777;
|
||||
background: #302016 var(--sammo-texture-walnut);
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
@@ -1002,14 +1099,6 @@ const clickOutsideMenu = (event: Event) => {
|
||||
max-height: calc(100vh - 104px);
|
||||
overflow: auto;
|
||||
}
|
||||
.compact:not(.mobile) .command-picker.recruitment-picker {
|
||||
top: 76px;
|
||||
left: 50%;
|
||||
width: 1000px;
|
||||
max-height: calc(100vh - 82px);
|
||||
overflow: auto;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.mobile.compact .editor-layout {
|
||||
@@ -1055,36 +1144,10 @@ const clickOutsideMenu = (event: Event) => {
|
||||
margin-top: -330px;
|
||||
overflow: visible;
|
||||
}
|
||||
.mobile.compact .command-picker.recruitment-picker {
|
||||
position: fixed;
|
||||
top: 76px;
|
||||
left: 0;
|
||||
width: 500px;
|
||||
height: auto;
|
||||
max-height: calc(100vh - 82px);
|
||||
margin-top: 0;
|
||||
overflow: auto;
|
||||
transform: none;
|
||||
}
|
||||
.mobile.compact .advanced-actions {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 109px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.reserved-command-editor .command-picker.recruitment-picker,
|
||||
.reserved-command-editor.compact .command-picker.recruitment-picker {
|
||||
top: 76px;
|
||||
left: 0;
|
||||
width: 500px;
|
||||
height: auto;
|
||||
max-height: calc(100vh - 82px);
|
||||
overflow: auto;
|
||||
padding: 0;
|
||||
border-right: 0;
|
||||
border-left: 0;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user