generated from VLADIMIR/template_frontend
180 lines
3.3 KiB
Vue
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>
|