This commit is contained in:
2026-10-19 02:48:30 +07:00
parent 2a574667f7
commit 97b5545b32
3 changed files with 457 additions and 460 deletions
+5 -335
View File
@@ -1,35 +1,13 @@
<script setup lang="ts">
import { NButton, NFlex, NInput, NInputNumber, NSelect, NSpace, NText } from 'naive-ui'
import { NButton, NFlex, NSpace, NText } from 'naive-ui'
import { computed, onBeforeUnmount, ref } from 'vue'
import type {
Application,
MapItem,
ResultsQuestion,
ScreenCommand,
ScreenMode,
ScreenPlan,
ScreenPlanStep,
ScreenState,
Team,
TeamResult,
} from '@/api/generated/crabs/evening_detective_server'
import ScreenPreview from '@/components/ScreenPreview.vue'
import type { Application, MapItem, Team } from '@/api/generated/crabs/evening_detective_server'
import UiBlock from '@/components/UiBlock.vue'
const props = defineProps<{
mapItems: MapItem[]
teams: Team[]
screens: ScreenState[]
previewQuestions: ResultsQuestion[]
previewTeams: TeamResult[]
screenPlan?: ScreenPlan
screenPlanDraft: ScreenPlanStep[]
screenPlanDirty: boolean
screenPlanSaving: boolean
screenPlanMode: 'use' | 'edit'
screenVolume: number
screenPosition: number
}>()
const GRID_SIZE = 20
@@ -38,40 +16,17 @@ const emit = defineEmits<{
saveItem: [item: MapItem, hidden?: boolean]
dragging: [value: boolean]
giveApplication: [team: Team, application: Application]
addStep: []
edit: []
cancel: []
removeStep: [index: number]
moveStep: [index: number, offset: number]
dirty: []
clearPlan: []
save: []
setStep: [index: number]
command: [mode: ScreenMode | undefined, command: ScreenCommand]
'update:screenVolume': [value: number | null]
'update:screenPosition': [value: number | null]
}>()
const mapScale = ref(1)
const mapOffset = ref({ x: 0, y: 0 })
const mapField = ref<HTMLElement>()
const previewScreen = computed(
() => props.screens.find((screen) => screen.status === 'connected') || props.screens[0],
)
const teamsNeedingApplications = computed(
() =>
new Map(props.teams.filter((team) => team.applications?.length).map((team) => [team.ID, team])),
)
const screenPlanModeOptions = [
{ label: 'Обложка', value: 'SCREEN_MODE_COVER' },
{ label: 'Введение', value: 'SCREEN_MODE_INTRO' },
{ label: 'Закрывашка', value: 'SCREEN_MODE_CONCLUSION' },
{ label: 'Результаты', value: 'SCREEN_MODE_RESULTS' },
{ label: 'Таймер', value: 'SCREEN_MODE_TIMER' },
{ label: 'Текст', value: 'SCREEN_MODE_TEXT' },
]
let dragState:
{ item: MapItem; offsetX: number; offsetY: number } | undefined
let dragState: { item: MapItem; offsetX: number; offsetY: number } | undefined
let panState: { startX: number; startY: number; offsetX: number; offsetY: number } | undefined
function getMapCoordinate(event: PointerEvent) {
@@ -172,7 +127,6 @@ onBeforeUnmount(() => {
</script>
<template>
<div class="map-preview-layout">
<UiBlock>
<n-flex align="center" justify="space-between" class="screen-header">
<p class="screens-title">Карта помещения</p>
@@ -254,264 +208,9 @@ onBeforeUnmount(() => {
</n-button>
</n-space>
</UiBlock>
<UiBlock tag="aside" class="screen-preview-panel">
<p class="screens-title">Превью экрана</p>
<ScreenPreview
v-if="previewScreen?.status === 'connected'"
:state="previewScreen"
:questions="previewQuestions"
:teams="previewTeams"
/>
<div v-else class="screen-preview-placeholder">Экран не подключён</div>
<div class="screen-plan">
<n-flex align="center" justify="space-between" class="screen-header">
<p class="screens-title">План экранов</p>
<n-space>
<n-button
v-if="!screenPlan && screenPlanMode === 'use'"
size="small"
@click="emit('addStep')"
>
Создать план
</n-button>
<n-button
v-if="screenPlan && screenPlanMode === 'use'"
size="small"
@click="emit('edit')"
>
Редактировать
</n-button>
<template v-if="screenPlanMode === 'edit'">
<n-button size="small" :disabled="screenPlanSaving" @click="emit('cancel')">
Отменить
</n-button>
<n-button
size="small"
type="primary"
:loading="screenPlanSaving"
:disabled="screenPlanSaving"
@click="emit('save')"
>
Сохранить план
</n-button>
</template>
</n-space>
</n-flex>
<p v-if="!screenPlan && screenPlanMode === 'use'" class="screen-empty">
План экранов ещё не создан.
</p>
<template v-else-if="screenPlanMode === 'edit'">
<n-space
v-for="(step, index) in screenPlanDraft"
:key="index"
class="screen-plan-step"
align="center"
justify="space-between"
wrap
>
<n-space align="center" wrap>
<n-text>{{ index + 1 }}</n-text>
<n-select
v-model:value="step.mode"
:options="screenPlanModeOptions"
:disabled="screenPlanSaving"
size="small"
style="width: 180px"
@update:value="emit('dirty')"
/>
<n-input
v-if="step.mode === 'SCREEN_MODE_TEXT'"
v-model:value="step.text"
:disabled="screenPlanSaving"
size="small"
placeholder="Текст экрана"
style="min-width: 240px"
@update:value="emit('dirty')"
/>
<template v-if="step.mode === 'SCREEN_MODE_TIMER'">
<n-input
v-model:value="step.timerLabel"
:disabled="screenPlanSaving"
size="small"
placeholder="Надпись таймера"
@update:value="emit('dirty')"
/>
<n-input-number
v-model:value="step.timerDurationSeconds"
:min="1"
:max="86400"
:disabled="screenPlanSaving"
size="small"
placeholder="Секунды"
@update:value="emit('dirty')"
/>
</template>
</n-space>
<n-space class="screen-plan-actions">
<n-button
size="small"
:disabled="screenPlanSaving || index === 0"
@click="emit('moveStep', index, -1)"
>
↑
</n-button>
<n-button
size="small"
:disabled="screenPlanSaving || index === screenPlanDraft.length - 1"
@click="emit('moveStep', index, 1)"
>
↓
</n-button>
<n-button
size="small"
type="error"
ghost
:disabled="screenPlanSaving"
@click="emit('removeStep', index)"
>
Удалить
</n-button>
</n-space>
</n-space>
<n-space>
<n-button size="small" :disabled="screenPlanSaving" @click="emit('addStep')">
Добавить шаг
</n-button>
<n-button
v-if="screenPlanDraft.length"
size="small"
:disabled="screenPlanSaving"
@click="emit('clearPlan')"
>
Удалить план
</n-button>
</n-space>
</template>
<template v-else-if="screenPlan">
<n-space
v-for="(step, index) in screenPlan.steps || []"
:key="index"
class="screen-plan-step"
align="center"
justify="space-between"
wrap
>
<n-space align="center">
<n-text>{{ index + 1 }}</n-text>
<n-text>
{{ screenPlanModeOptions.find((option) => option.value === step.mode)?.label }}
<template v-if="step.mode === 'SCREEN_MODE_TEXT'">: {{ step.text }}</template>
<template v-else-if="step.mode === 'SCREEN_MODE_TIMER'">
: {{ step.timerLabel }} ({{ step.timerDurationSeconds }} с)
</template>
</n-text>
</n-space>
<n-space class="screen-plan-actions">
<n-button
size="small"
:type="screenPlan.currentStep === index ? 'primary' : 'default'"
:disabled="screenPlan.currentStep === index"
@click="emit('setStep', index)"
>
Показать
</n-button>
<template v-if="screenPlan.currentStep === index">
<template v-if="step.mode === 'SCREEN_MODE_TIMER'">
<n-button
size="small"
@click="emit('command', step.mode, 'SCREEN_COMMAND_TIMER_START')"
>
Старт
</n-button>
<n-button
size="small"
@click="emit('command', step.mode, 'SCREEN_COMMAND_TIMER_PAUSE')"
>
Пауза
</n-button>
<n-button
size="small"
@click="emit('command', step.mode, 'SCREEN_COMMAND_TIMER_RESUME')"
>
Продолжить
</n-button>
<n-button
size="small"
@click="emit('command', step.mode, 'SCREEN_COMMAND_TIMER_RESET')"
>
Сбросить
</n-button>
</template>
<template
v-else-if="
step.mode === 'SCREEN_MODE_INTRO' || step.mode === 'SCREEN_MODE_CONCLUSION'
"
>
<n-button size="small" @click="emit('command', step.mode, 'SCREEN_COMMAND_PLAY')">
Запуск
</n-button>
<n-button
size="small"
@click="emit('command', step.mode, 'SCREEN_COMMAND_PAUSE')"
>
Пауза
</n-button>
<n-button
size="small"
@click="emit('command', step.mode, 'SCREEN_COMMAND_RESUME')"
>
Продолжить
</n-button>
<n-button size="small" @click="emit('command', step.mode, 'SCREEN_COMMAND_STOP')">
Стоп
</n-button>
<n-button
size="small"
@click="emit('command', step.mode, 'SCREEN_COMMAND_REPLAY')"
>
Повтор
</n-button>
<n-input-number
:value="screenVolume"
:min="0"
:max="100"
:step="5"
size="small"
@update:value="emit('update:screenVolume', $event)"
/>
<n-button
size="small"
@click="emit('command', step.mode, 'SCREEN_COMMAND_VOLUME')"
>
Громкость
</n-button>
<n-input-number
:value="screenPosition"
:min="0"
:step="5"
size="small"
@update:value="emit('update:screenPosition', $event)"
/>
<n-button size="small" @click="emit('command', step.mode, 'SCREEN_COMMAND_SEEK')">
Перемотать
</n-button>
</template>
</template>
</n-space>
</n-space>
</template>
</div>
</UiBlock>
</div>
</template>
<style scoped>
.map-preview-layout {
display: grid;
gap: 8px;
grid-template-columns: minmax(0, 1fr) minmax(280px, 420px);
}
.screens-title {
font-size: 18px;
font-weight: 600;
@@ -522,31 +221,8 @@ onBeforeUnmount(() => {
margin-bottom: 8px;
}
.screen-preview-placeholder {
align-items: center;
aspect-ratio: 16 / 9;
background-color: #111;
color: #aaa;
display: flex;
justify-content: center;
text-align: center;
}
.screen-preview-panel > .screens-title {
margin-bottom: 8px;
}
.screen-plan {
margin-top: 16px;
}
.screen-plan-step {
border-top: 1px solid rgb(255 255 255 / 10%);
padding: 8px 0;
}
.map-field {
aspect-ratio: 1;
aspect-ratio: 4;
background-color: #151515;
border: 1px dashed rgb(255 255 255 / 20%);
cursor: grab;
@@ -648,10 +324,4 @@ onBeforeUnmount(() => {
.map-hidden-items {
margin-top: 16px;
}
@media (width < 1024px) {
.map-preview-layout {
grid-template-columns: 1fr;
}
}
</style>
+49 -51
View File
@@ -46,6 +46,7 @@ import type {
} from '@/api/generated/crabs/evening_detective_server'
import notificationSoundURL from '@/assets/icq-uh-oh.wav'
import GameMapPanel from '@/components/GameMapPanel.vue'
import GameScreenPlanPanel from '@/components/GameScreenPlanPanel.vue'
import GameScreensPanel from '@/components/GameScreensPanel.vue'
import GameStatePanel from '@/components/GameStatePanel.vue'
import GameTeamsPanel from '@/components/GameTeamsPanel.vue'
@@ -72,10 +73,10 @@ const router = useRouter()
const route = useRoute()
const gameId = route.params.id
type GameTab = 'settings' | 'teams' | 'screens' | 'game' | 'payment' | 'answers' | 'state'
type GameTab = 'settings' | 'screens' | 'game' | 'payment' | 'answers' | 'state'
function isGameTab(value: string): value is GameTab {
return ['settings', 'teams', 'screens', 'game', 'payment', 'answers', 'state'].includes(value)
return ['settings', 'screens', 'game', 'payment', 'answers', 'state'].includes(value)
}
const queryTab = typeof route.query.tab === 'string' ? route.query.tab : ''
@@ -1362,33 +1363,6 @@ useSimplePolling(() => {
</UiBlock>
</div>
</n-tab-pane>
<n-tab-pane v-if="canManageGame" name="teams" tab="Команды">
<div class="game-tab-content">
<n-flex justify="space-between" align="center">
<n-tag :bordered="false" type="info"> Участвуют команд: {{ activeTeamCount }} </n-tag>
<n-button
v-if="activeTeamCount"
quaternary
:loading="isDownloadingQR"
:disabled="isDownloadingQR"
@click="downloadTeamQR()"
>
QR команд
</n-button>
</n-flex>
<GameTeamsPanel
:teams="game.teams || []"
:can-print-receipts="uiStore.environment === 'venue'"
:participation-options="participationOptions"
@add="showAddTeamModal = true"
@open-qr="openTeamQR"
@print="openTeamReceipt"
@preview="(id, password) => toTeamStory(id, password, true)"
@delete="deleteTeam"
@give-application="giveApplication"
/>
</div>
</n-tab-pane>
<n-tab-pane v-if="canManageGame" name="screens" tab="Экраны">
<div class="game-tab-content">
<GameScreensPanel
@@ -1405,6 +1379,29 @@ useSimplePolling(() => {
@upload-track="uploadTrack"
@delete-track="deleteTrack"
/>
<GameScreenPlanPanel
:screens="screens"
:preview-questions="previewQuestions"
:preview-teams="previewTeams"
:screen-plan="screenPlan"
:screen-plan-draft="screenPlanDraft"
:screen-plan-saving="screenPlanSaving"
:screen-plan-mode="screenPlanMode"
:screen-volume="screenVolume"
:screen-position="screenPosition"
@add-step="addScreenPlanStep"
@edit="editScreenPlan"
@cancel="cancelScreenPlanEdit"
@remove-step="removeScreenPlanStep"
@move-step="moveScreenPlanStep"
@dirty="markScreenPlanDirty"
@clear-plan="clearScreenPlan"
@save="saveScreenPlan"
@set-step="setScreenPlanStep"
@command="sendScreenPlanCommand"
@update:screen-volume="screenVolume = $event ?? 0"
@update:screen-position="screenPosition = $event ?? 0"
/>
</div>
</n-tab-pane>
<n-tab-pane v-if="canManageGame" name="game" tab="Игра">
@@ -1482,31 +1479,32 @@ useSimplePolling(() => {
<GameMapPanel
:map-items="mapItems"
:teams="game.teams || []"
:screens="screens"
:preview-questions="previewQuestions"
:preview-teams="previewTeams"
:screen-plan="screenPlan"
:screen-plan-draft="screenPlanDraft"
:screen-plan-dirty="screenPlanDirty"
:screen-plan-saving="screenPlanSaving"
:screen-plan-mode="screenPlanMode"
:screen-volume="screenVolume"
:screen-position="screenPosition"
@save-item="saveMapItemAndReload"
@dragging="mapDragging = $event"
@give-application="giveApplication"
@add-step="addScreenPlanStep"
@edit="editScreenPlan"
@cancel="cancelScreenPlanEdit"
@remove-step="removeScreenPlanStep"
@move-step="moveScreenPlanStep"
@dirty="markScreenPlanDirty"
@clear-plan="clearScreenPlan"
@save="saveScreenPlan"
@set-step="setScreenPlanStep"
@command="sendScreenPlanCommand"
@update:screen-volume="screenVolume = $event ?? 0"
@update:screen-position="screenPosition = $event ?? 0"
/>
<n-flex justify="space-between" align="center">
<n-tag :bordered="false" type="info"> Участвуют команд: {{ activeTeamCount }} </n-tag>
<n-button
v-if="activeTeamCount"
quaternary
:loading="isDownloadingQR"
:disabled="isDownloadingQR"
@click="downloadTeamQR()"
>
QR команд
</n-button>
</n-flex>
<GameTeamsPanel
:teams="game.teams || []"
:can-print-receipts="uiStore.environment === 'venue'"
:participation-options="participationOptions"
@add="showAddTeamModal = true"
@open-qr="openTeamQR"
@print="openTeamReceipt"
@preview="(id, password) => toTeamStory(id, password, true)"
@delete="deleteTeam"
@give-application="giveApplication"
/>
</div>
</n-tab-pane>
+329
View File
@@ -0,0 +1,329 @@
<script setup lang="ts">
import { NButton, NFlex, NInput, NInputNumber, NSelect, NSpace, NText } from 'naive-ui'
import { computed } from 'vue'
import type {
ResultsQuestion,
ScreenCommand,
ScreenMode,
ScreenPlan,
ScreenPlanStep,
ScreenState,
TeamResult,
} from '@/api/generated/crabs/evening_detective_server'
import ScreenPreview from '@/components/ScreenPreview.vue'
import UiBlock from '@/components/UiBlock.vue'
const props = defineProps<{
screens: ScreenState[]
previewQuestions: ResultsQuestion[]
previewTeams: TeamResult[]
screenPlan?: ScreenPlan
screenPlanDraft: ScreenPlanStep[]
screenPlanSaving: boolean
screenPlanMode: 'use' | 'edit'
screenVolume: number
screenPosition: number
}>()
const emit = defineEmits<{
addStep: []
edit: []
cancel: []
removeStep: [index: number]
moveStep: [index: number, offset: number]
dirty: []
clearPlan: []
save: []
setStep: [index: number]
command: [mode: ScreenMode | undefined, command: ScreenCommand]
'update:screenVolume': [value: number | null]
'update:screenPosition': [value: number | null]
}>()
const previewScreen = computed(
() => props.screens.find((screen) => screen.status === 'connected') || props.screens[0],
)
const screenPlanModeOptions = [
{ label: 'Обложка', value: 'SCREEN_MODE_COVER' },
{ label: 'Введение', value: 'SCREEN_MODE_INTRO' },
{ label: 'Закрывашка', value: 'SCREEN_MODE_CONCLUSION' },
{ label: 'Результаты', value: 'SCREEN_MODE_RESULTS' },
{ label: 'Таймер', value: 'SCREEN_MODE_TIMER' },
{ label: 'Текст', value: 'SCREEN_MODE_TEXT' },
]
</script>
<template>
<UiBlock class="screen-preview-panel">
<p class="screens-title">Превью экрана</p>
<ScreenPreview
v-if="previewScreen?.status === 'connected'"
:state="previewScreen"
:questions="previewQuestions"
:teams="previewTeams"
/>
<div v-else class="screen-preview-placeholder">Экран не подключён</div>
<div class="screen-plan">
<n-flex align="center" justify="space-between" class="screen-header">
<p class="screens-title">План экранов</p>
<n-space>
<n-button
v-if="!screenPlan && screenPlanMode === 'use'"
size="small"
@click="emit('addStep')"
>
Создать план
</n-button>
<n-button
v-if="screenPlan && screenPlanMode === 'use'"
size="small"
@click="emit('edit')"
>
Редактировать
</n-button>
<template v-if="screenPlanMode === 'edit'">
<n-button size="small" :disabled="screenPlanSaving" @click="emit('cancel')">
Отменить
</n-button>
<n-button
size="small"
type="primary"
:loading="screenPlanSaving"
:disabled="screenPlanSaving"
@click="emit('save')"
>
Сохранить план
</n-button>
</template>
</n-space>
</n-flex>
<p v-if="!screenPlan && screenPlanMode === 'use'" class="screen-empty">
План экранов ещё не создан.
</p>
<template v-else-if="screenPlanMode === 'edit'">
<n-space
v-for="(step, index) in screenPlanDraft"
:key="index"
class="screen-plan-step"
align="center"
justify="space-between"
wrap
>
<n-space align="center" wrap>
<n-text>{{ index + 1 }}</n-text>
<n-select
v-model:value="step.mode"
:options="screenPlanModeOptions"
:disabled="screenPlanSaving"
size="small"
style="width: 180px"
@update:value="emit('dirty')"
/>
<n-input
v-if="step.mode === 'SCREEN_MODE_TEXT'"
v-model:value="step.text"
:disabled="screenPlanSaving"
size="small"
placeholder="Текст экрана"
style="min-width: 240px"
@update:value="emit('dirty')"
/>
<template v-if="step.mode === 'SCREEN_MODE_TIMER'">
<n-input
v-model:value="step.timerLabel"
:disabled="screenPlanSaving"
size="small"
placeholder="Надпись таймера"
@update:value="emit('dirty')"
/>
<n-input-number
v-model:value="step.timerDurationSeconds"
:min="1"
:max="86400"
:disabled="screenPlanSaving"
size="small"
placeholder="Секунды"
@update:value="emit('dirty')"
/>
</template>
</n-space>
<n-space class="screen-plan-actions">
<n-button
size="small"
:disabled="screenPlanSaving || index === 0"
@click="emit('moveStep', index, -1)"
>
↑
</n-button>
<n-button
size="small"
:disabled="screenPlanSaving || index === screenPlanDraft.length - 1"
@click="emit('moveStep', index, 1)"
>
↓
</n-button>
<n-button
size="small"
type="error"
ghost
:disabled="screenPlanSaving"
@click="emit('removeStep', index)"
>
Удалить
</n-button>
</n-space>
</n-space>
<n-space>
<n-button size="small" :disabled="screenPlanSaving" @click="emit('addStep')">
Добавить шаг
</n-button>
<n-button
v-if="screenPlanDraft.length"
size="small"
:disabled="screenPlanSaving"
@click="emit('clearPlan')"
>
Удалить план
</n-button>
</n-space>
</template>
<template v-else-if="screenPlan">
<n-space
v-for="(step, index) in screenPlan.steps || []"
:key="index"
class="screen-plan-step"
align="center"
justify="space-between"
wrap
>
<n-space align="center">
<n-text>{{ index + 1 }}</n-text>
<n-text>
{{ screenPlanModeOptions.find((option) => option.value === step.mode)?.label }}
<template v-if="step.mode === 'SCREEN_MODE_TEXT'">: {{ step.text }}</template>
<template v-else-if="step.mode === 'SCREEN_MODE_TIMER'">
: {{ step.timerLabel }} ({{ step.timerDurationSeconds }} с)
</template>
</n-text>
</n-space>
<n-space class="screen-plan-actions">
<n-button
size="small"
:type="screenPlan.currentStep === index ? 'primary' : 'default'"
:disabled="screenPlan.currentStep === index"
@click="emit('setStep', index)"
>
Показать
</n-button>
<template v-if="screenPlan.currentStep === index">
<template v-if="step.mode === 'SCREEN_MODE_TIMER'">
<n-button
size="small"
@click="emit('command', step.mode, 'SCREEN_COMMAND_TIMER_START')"
>
Старт
</n-button>
<n-button
size="small"
@click="emit('command', step.mode, 'SCREEN_COMMAND_TIMER_PAUSE')"
>
Пауза
</n-button>
<n-button
size="small"
@click="emit('command', step.mode, 'SCREEN_COMMAND_TIMER_RESUME')"
>
Продолжить
</n-button>
<n-button
size="small"
@click="emit('command', step.mode, 'SCREEN_COMMAND_TIMER_RESET')"
>
Сбросить
</n-button>
</template>
<template
v-else-if="
step.mode === 'SCREEN_MODE_INTRO' || step.mode === 'SCREEN_MODE_CONCLUSION'
"
>
<n-button size="small" @click="emit('command', step.mode, 'SCREEN_COMMAND_PLAY')">
Запуск
</n-button>
<n-button size="small" @click="emit('command', step.mode, 'SCREEN_COMMAND_PAUSE')">
Пауза
</n-button>
<n-button size="small" @click="emit('command', step.mode, 'SCREEN_COMMAND_RESUME')">
Продолжить
</n-button>
<n-button size="small" @click="emit('command', step.mode, 'SCREEN_COMMAND_STOP')">
Стоп
</n-button>
<n-button size="small" @click="emit('command', step.mode, 'SCREEN_COMMAND_REPLAY')">
Повтор
</n-button>
<n-input-number
:value="screenVolume"
:min="0"
:max="100"
:step="5"
size="small"
@update:value="emit('update:screenVolume', $event)"
/>
<n-button size="small" @click="emit('command', step.mode, 'SCREEN_COMMAND_VOLUME')">
Громкость
</n-button>
<n-input-number
:value="screenPosition"
:min="0"
:step="5"
size="small"
@update:value="emit('update:screenPosition', $event)"
/>
<n-button size="small" @click="emit('command', step.mode, 'SCREEN_COMMAND_SEEK')">
Перемотать
</n-button>
</template>
</template>
</n-space>
</n-space>
</template>
</div>
</UiBlock>
</template>
<style scoped>
.screens-title {
font-size: 18px;
font-weight: 600;
margin: 0;
}
.screen-header {
margin-bottom: 8px;
}
.screen-preview-placeholder {
align-items: center;
aspect-ratio: 16 / 9;
background-color: var(--brand-background);
color: var(--brand-text-muted);
display: flex;
justify-content: center;
text-align: center;
}
.screen-preview-panel > .screens-title {
margin-bottom: 8px;
}
.screen-plan {
margin-top: 16px;
}
.screen-plan-step {
border-top: 1px solid var(--color-border);
padding: 8px 0;
}
</style>