Files
evening_detective_frontend/src/components/CaseBoard.vue
T
2026-10-14 23:39:17 +07:00

180 lines
3.3 KiB
Vue

<script setup lang="ts">
defineProps<{
label: string
footer?: string
fragments: ReadonlyArray<{
number: string
title: string
text: string
caption?: string
detail?: string
kind?: 'note' | 'ticket' | 'timetable'
}>
}>()
</script>
<template>
<div class="case-board">
<p class="board-label">{{ label }}</p>
<div class="clues">
<article
v-for="(fragment, index) in fragments"
:key="index"
class="clue"
:class="fragment.kind"
>
<span class="clue-number">{{ fragment.number }} / {{ fragment.title }}</span>
<p
:class="{
'ticket-route': fragment.kind === 'ticket',
'departure-time': fragment.kind === 'timetable',
}"
>
{{ fragment.text }}
</p>
<p v-if="fragment.detail" class="ticket-time">{{ fragment.detail }}</p>
<span v-if="fragment.caption" class="clue-caption">{{ fragment.caption }}</span>
</article>
</div>
<p v-if="footer || $slots.footer" class="board-footer">
<slot name="footer">{{ footer }}</slot>
</p>
</div>
</template>
<style scoped>
.case-board {
background: var(--brand-background);
border: 1px solid var(--brand-border);
border-radius: 8px;
padding: 24px 16px;
}
.board-label,
.board-footer {
color: var(--brand-text-muted);
font-size: 14px;
letter-spacing: 0.08em;
margin: 0;
text-transform: uppercase;
}
.board-footer {
margin-top: 24px;
}
.clues {
display: grid;
gap: 24px;
margin-top: 24px;
position: relative;
}
.clues::before {
background: var(--brand-bronze);
bottom: 16px;
content: '';
left: 50%;
pointer-events: none;
position: absolute;
top: 16px;
transition: background-color 160ms ease;
width: 1px;
}
.case-board:focus-within .clues::before {
background: var(--brand-accent);
}
@media (hover: hover) {
.case-board:hover .clues::before {
background: var(--brand-accent);
}
}
@media (prefers-reduced-motion: reduce) {
.clues::before {
transition: none;
}
}
.clue {
background: var(--brand-paper) url('@/assets/images/paper.jpg') center / cover;
box-shadow: 0 4px 8px rgb(0 0 0 / 35%);
color: var(--brand-background);
padding: 16px;
position: relative;
}
.clue::before {
background: var(--brand-bronze);
border: 2px solid var(--brand-background);
border-radius: 50%;
content: '';
height: 8px;
left: calc(50% - 6px);
position: absolute;
top: -6px;
width: 8px;
}
.clue-number,
.clue-caption {
display: block;
font-size: 14px;
}
.clue-number {
border-bottom: 1px dashed var(--brand-background);
margin-bottom: 12px;
padding-bottom: 8px;
}
.clue p {
font-family: 'font_old_typer', serif;
font-size: 17px;
line-height: 1.5;
margin: 0 0 12px;
}
.clue .ticket-route {
font-family: inherit;
font-size: 14px;
font-weight: 600;
}
.clue .ticket-time,
.clue .departure-time {
font-size: 32px;
line-height: 1.1;
}
@media (width >= 1024px) {
.clues {
gap: 24px;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.clues::before {
height: 1px;
left: 20%;
top: 65%;
transform: rotate(-20deg);
width: 65%;
}
.note {
grid-column: 1 / -1;
margin-right: 24px;
transform: rotate(-2deg);
}
.ticket {
transform: rotate(3deg);
}
.timetable {
margin-top: 24px;
transform: rotate(-3deg);
}
}
</style>