feat(calendar): add week hover jump for month rows
This commit is contained in:
293
frontend/app.vue
293
frontend/app.vue
@@ -358,6 +358,22 @@ const pilotWaveContainer = ref<HTMLDivElement | null>(null);
|
|||||||
const livePilotUserText = ref("");
|
const livePilotUserText = ref("");
|
||||||
const livePilotAssistantText = ref("");
|
const livePilotAssistantText = ref("");
|
||||||
const pilotLiveLogs = ref<Array<{ id: string; text: string; at: string }>>([]);
|
const pilotLiveLogs = ref<Array<{ id: string; text: string; at: string }>>([]);
|
||||||
|
const PILOT_LIVE_LOGS_PREVIEW_LIMIT = 5;
|
||||||
|
const pilotLiveLogsExpanded = ref(false);
|
||||||
|
const pilotLiveLogHiddenCount = computed(() => {
|
||||||
|
const hidden = pilotLiveLogs.value.length - PILOT_LIVE_LOGS_PREVIEW_LIMIT;
|
||||||
|
return hidden > 0 ? hidden : 0;
|
||||||
|
});
|
||||||
|
const pilotVisibleLiveLogs = computed(() => {
|
||||||
|
if (pilotLiveLogsExpanded.value || pilotLiveLogHiddenCount.value === 0) return pilotLiveLogs.value;
|
||||||
|
return pilotLiveLogs.value.slice(-PILOT_LIVE_LOGS_PREVIEW_LIMIT);
|
||||||
|
});
|
||||||
|
const pilotVisibleLogCount = computed(() =>
|
||||||
|
Math.min(pilotLiveLogs.value.length, PILOT_LIVE_LOGS_PREVIEW_LIMIT),
|
||||||
|
);
|
||||||
|
function togglePilotLiveLogsExpanded() {
|
||||||
|
pilotLiveLogsExpanded.value = !pilotLiveLogsExpanded.value;
|
||||||
|
}
|
||||||
let pilotMediaRecorder: MediaRecorder | null = null;
|
let pilotMediaRecorder: MediaRecorder | null = null;
|
||||||
let pilotRecorderStream: MediaStream | null = null;
|
let pilotRecorderStream: MediaStream | null = null;
|
||||||
let pilotRecordingChunks: Blob[] = [];
|
let pilotRecordingChunks: Blob[] = [];
|
||||||
@@ -422,6 +438,15 @@ let pilotBackgroundPoll: ReturnType<typeof setInterval> | null = null;
|
|||||||
const lifecycleNowMs = ref(Date.now());
|
const lifecycleNowMs = ref(Date.now());
|
||||||
let lifecycleClock: ReturnType<typeof setInterval> | null = null;
|
let lifecycleClock: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => pilotLiveLogs.value.length,
|
||||||
|
(len) => {
|
||||||
|
if (len === 0 || len <= PILOT_LIVE_LOGS_PREVIEW_LIMIT) {
|
||||||
|
pilotLiveLogsExpanded.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => authMe.value?.conversation.id,
|
() => authMe.value?.conversation.id,
|
||||||
(id) => {
|
(id) => {
|
||||||
@@ -763,6 +788,7 @@ async function sendPilotText(rawText: string) {
|
|||||||
pilotInput.value = "";
|
pilotInput.value = "";
|
||||||
livePilotUserText.value = text;
|
livePilotUserText.value = text;
|
||||||
livePilotAssistantText.value = "";
|
livePilotAssistantText.value = "";
|
||||||
|
pilotLiveLogsExpanded.value = false;
|
||||||
pilotLiveLogs.value = [];
|
pilotLiveLogs.value = [];
|
||||||
try {
|
try {
|
||||||
await pilotChat.sendMessage({ text });
|
await pilotChat.sendMessage({ text });
|
||||||
@@ -1883,6 +1909,38 @@ const calendarViewOptions: { value: CalendarView; label: string }[] = [
|
|||||||
{ value: "agenda", label: "Agenda" },
|
{ value: "agenda", label: "Agenda" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const calendarZoomOptions: Array<{ value: number; label: string }> = [
|
||||||
|
{ value: 1, label: "400%" },
|
||||||
|
{ value: 2, label: "250%" },
|
||||||
|
{ value: 3, label: "100%" },
|
||||||
|
{ value: 4, label: "50%" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const calendarZoomLevel = computed<number>({
|
||||||
|
get() {
|
||||||
|
if (calendarView.value === "day") return 1;
|
||||||
|
if (calendarView.value === "week") return 2;
|
||||||
|
if (calendarView.value === "month" || calendarView.value === "agenda") return 3;
|
||||||
|
return 4;
|
||||||
|
},
|
||||||
|
set(next) {
|
||||||
|
const level = Math.max(1, Math.min(4, Number(next) || 3));
|
||||||
|
if (level === 1) {
|
||||||
|
calendarView.value = "day";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (level === 2) {
|
||||||
|
calendarView.value = "week";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (level === 3) {
|
||||||
|
calendarView.value = "month";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
calendarView.value = "year";
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const monthCells = computed(() => {
|
const monthCells = computed(() => {
|
||||||
const year = calendarCursor.value.getFullYear();
|
const year = calendarCursor.value.getFullYear();
|
||||||
const month = calendarCursor.value.getMonth();
|
const month = calendarCursor.value.getMonth();
|
||||||
@@ -1903,6 +1961,20 @@ const monthCells = computed(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const monthRows = computed(() => {
|
||||||
|
const rows: Array<{ key: string; startKey: string; cells: typeof monthCells.value }> = [];
|
||||||
|
for (let index = 0; index < monthCells.value.length; index += 7) {
|
||||||
|
const cells = monthCells.value.slice(index, index + 7);
|
||||||
|
if (!cells.length) continue;
|
||||||
|
rows.push({
|
||||||
|
key: `${cells[0]?.key ?? index}-week-row`,
|
||||||
|
startKey: cells[0]?.key ?? selectedDateKey.value,
|
||||||
|
cells,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
});
|
||||||
|
|
||||||
function monthCellHasFocusedEvent(events: CalendarEvent[]) {
|
function monthCellHasFocusedEvent(events: CalendarEvent[]) {
|
||||||
const id = focusedCalendarEventId.value.trim();
|
const id = focusedCalendarEventId.value.trim();
|
||||||
if (!id) return false;
|
if (!id) return false;
|
||||||
@@ -2023,6 +2095,16 @@ function pickDate(key: string) {
|
|||||||
calendarCursor.value = new Date(d.getFullYear(), d.getMonth(), 1);
|
calendarCursor.value = new Date(d.getFullYear(), d.getMonth(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openDayView(key: string) {
|
||||||
|
pickDate(key);
|
||||||
|
calendarView.value = "day";
|
||||||
|
}
|
||||||
|
|
||||||
|
function openWeekView(key: string) {
|
||||||
|
pickDate(key);
|
||||||
|
calendarView.value = "week";
|
||||||
|
}
|
||||||
|
|
||||||
function openYearMonth(monthIndex: number) {
|
function openYearMonth(monthIndex: number) {
|
||||||
focusedCalendarEventId.value = "";
|
focusedCalendarEventId.value = "";
|
||||||
const year = calendarCursor.value.getFullYear();
|
const year = calendarCursor.value.getFullYear();
|
||||||
@@ -3393,8 +3475,21 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="pilotLiveLogs.length" class="pilot-stream-status">
|
<div v-if="pilotLiveLogs.length" class="pilot-stream-status">
|
||||||
|
<div class="pilot-stream-head">
|
||||||
|
<p v-if="!pilotLiveLogsExpanded && pilotLiveLogHiddenCount > 0" class="pilot-stream-caption">
|
||||||
|
Showing last {{ pilotVisibleLogCount }} steps
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
v-if="pilotLiveLogHiddenCount > 0 || pilotLiveLogsExpanded"
|
||||||
|
type="button"
|
||||||
|
class="pilot-stream-toggle"
|
||||||
|
@click="togglePilotLiveLogsExpanded"
|
||||||
|
>
|
||||||
|
{{ pilotLiveLogsExpanded ? "Show less" : `Show all (+${pilotLiveLogHiddenCount})` }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<p
|
<p
|
||||||
v-for="log in pilotLiveLogs"
|
v-for="log in pilotVisibleLiveLogs"
|
||||||
:key="`pilot-log-${log.id}`"
|
:key="`pilot-log-${log.id}`"
|
||||||
class="pilot-stream-line"
|
class="pilot-stream-line"
|
||||||
:class="log.id === pilotLiveLogs[pilotLiveLogs.length - 1]?.id ? 'pilot-stream-line-current' : ''"
|
:class="log.id === pilotLiveLogs[pilotLiveLogs.length - 1]?.id ? 'pilot-stream-line-current' : ''"
|
||||||
@@ -3546,19 +3641,18 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
<div class="grid grid-cols-[auto_1fr_auto] items-center gap-2">
|
<div class="grid grid-cols-[auto_1fr_auto] items-center gap-2">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<button class="btn btn-xs" @click="setToday">Today</button>
|
<button class="btn btn-xs" @click="setToday">Today</button>
|
||||||
<button class="btn btn-xs btn-ghost" @click="shiftCalendar(-1)">←</button>
|
|
||||||
<button class="btn btn-xs btn-ghost" @click="shiftCalendar(1)">→</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-center text-sm font-medium">
|
<div class="text-center text-sm font-medium">
|
||||||
{{ calendarPeriodLabel }}
|
{{ calendarPeriodLabel }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="justify-self-end">
|
<div class="justify-self-end flex items-center gap-2">
|
||||||
<select v-model="calendarView" class="select select-bordered select-xs w-36">
|
<span class="text-[10px] uppercase tracking-wide text-base-content/60">Zoom</span>
|
||||||
|
<select v-model.number="calendarZoomLevel" class="select select-bordered select-xs w-24">
|
||||||
<option
|
<option
|
||||||
v-for="option in calendarViewOptions"
|
v-for="option in calendarZoomOptions"
|
||||||
:key="`calendar-view-${option.value}`"
|
:key="`calendar-zoom-${option.value}`"
|
||||||
:value="option.value"
|
:value="option.value"
|
||||||
>
|
>
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
@@ -3579,7 +3673,24 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
<p class="mt-1 text-xs text-base-content/80">{{ focusedCalendarEvent.note || "No note" }}</p>
|
<p class="mt-1 text-xs text-base-content/80">{{ focusedCalendarEvent.note || "No note" }}</p>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<div class="min-h-0 flex-1 overflow-y-auto pr-1">
|
<div class="calendar-content-wrap min-h-0 flex-1">
|
||||||
|
<button
|
||||||
|
class="calendar-side-nav calendar-side-nav-left"
|
||||||
|
type="button"
|
||||||
|
title="Previous period"
|
||||||
|
@click="shiftCalendar(-1)"
|
||||||
|
>
|
||||||
|
<span>←</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="calendar-side-nav calendar-side-nav-right"
|
||||||
|
type="button"
|
||||||
|
title="Next period"
|
||||||
|
@click="shiftCalendar(1)"
|
||||||
|
>
|
||||||
|
<span>→</span>
|
||||||
|
</button>
|
||||||
|
<div class="calendar-content-scroll min-h-0 h-full overflow-y-auto pr-1">
|
||||||
<div v-if="calendarView === 'month'" class="space-y-1">
|
<div v-if="calendarView === 'month'" class="space-y-1">
|
||||||
<div class="grid grid-cols-7 gap-1 text-center text-xs font-semibold text-base-content/60">
|
<div class="grid grid-cols-7 gap-1 text-center text-xs font-semibold text-base-content/60">
|
||||||
<span>Sun</span>
|
<span>Sun</span>
|
||||||
@@ -3591,11 +3702,25 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
<span>Sat</span>
|
<span>Sat</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div
|
||||||
|
v-for="row in monthRows"
|
||||||
|
:key="row.key"
|
||||||
|
class="group relative"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="calendar-hover-jump calendar-hover-jump-row"
|
||||||
|
title="Open week view"
|
||||||
|
@click.stop="openWeekView(row.startKey)"
|
||||||
|
>
|
||||||
|
Week
|
||||||
|
</button>
|
||||||
<div class="grid grid-cols-7 gap-1">
|
<div class="grid grid-cols-7 gap-1">
|
||||||
<button
|
<button
|
||||||
v-for="cell in monthCells"
|
v-for="cell in row.cells"
|
||||||
:key="cell.key"
|
:key="cell.key"
|
||||||
class="min-h-24 rounded-lg border p-1 text-left"
|
class="group relative min-h-24 rounded-lg border p-1 text-left"
|
||||||
:class="[
|
:class="[
|
||||||
cell.inMonth ? 'border-base-300 bg-base-100' : 'border-base-200 bg-base-200/40 text-base-content/40',
|
cell.inMonth ? 'border-base-300 bg-base-100' : 'border-base-200 bg-base-200/40 text-base-content/40',
|
||||||
selectedDateKey === cell.key ? 'border-primary bg-primary/5' : '',
|
selectedDateKey === cell.key ? 'border-primary bg-primary/5' : '',
|
||||||
@@ -3604,6 +3729,13 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
@click="pickDate(cell.key)"
|
@click="pickDate(cell.key)"
|
||||||
>
|
>
|
||||||
<p class="mb-1 text-xs font-semibold">{{ cell.day }}</p>
|
<p class="mb-1 text-xs font-semibold">{{ cell.day }}</p>
|
||||||
|
<span
|
||||||
|
class="calendar-hover-jump"
|
||||||
|
title="Open day view"
|
||||||
|
@click.stop="openDayView(cell.key)"
|
||||||
|
>
|
||||||
|
Day
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
v-for="event in monthCellEvents(cell.events)"
|
v-for="event in monthCellEvents(cell.events)"
|
||||||
:key="event.id"
|
:key="event.id"
|
||||||
@@ -3616,16 +3748,26 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-else-if="calendarView === 'week'" class="space-y-2">
|
<div v-else-if="calendarView === 'week'" class="space-y-2">
|
||||||
<article
|
<article
|
||||||
v-for="day in weekDays"
|
v-for="day in weekDays"
|
||||||
:key="day.key"
|
:key="day.key"
|
||||||
class="rounded-xl border border-base-300 p-3"
|
class="group relative rounded-xl border border-base-300 p-3"
|
||||||
:class="selectedDateKey === day.key ? 'border-primary bg-primary/5' : ''"
|
:class="selectedDateKey === day.key ? 'border-primary bg-primary/5' : ''"
|
||||||
@click="pickDate(day.key)"
|
@click="pickDate(day.key)"
|
||||||
>
|
>
|
||||||
<p class="mb-2 text-sm font-semibold">{{ day.label }} {{ day.day }}</p>
|
<p class="mb-2 text-sm font-semibold">{{ day.label }} {{ day.day }}</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="calendar-hover-jump calendar-hover-jump-week"
|
||||||
|
title="Open day view"
|
||||||
|
@click.stop="openDayView(day.key)"
|
||||||
|
>
|
||||||
|
Day
|
||||||
|
</button>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<button
|
<button
|
||||||
v-for="event in day.events"
|
v-for="event in day.events"
|
||||||
@@ -3691,6 +3833,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-else-if="selectedTab === 'communications' && false" class="space-y-3">
|
<section v-else-if="selectedTab === 'communications' && false" class="space-y-3">
|
||||||
@@ -4732,6 +4875,102 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.calendar-content-wrap {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 40px;
|
||||||
|
padding-right: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-content-scroll {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-side-nav {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 4;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: 1px solid color-mix(in oklab, var(--color-base-content) 16%, transparent);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: color-mix(in oklab, var(--color-base-100) 88%, transparent);
|
||||||
|
color: color-mix(in oklab, var(--color-base-content) 86%, transparent);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background-color 120ms ease, border-color 120ms ease, transform 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-side-nav:hover {
|
||||||
|
border-color: color-mix(in oklab, var(--color-primary) 50%, transparent);
|
||||||
|
background: color-mix(in oklab, var(--color-primary) 14%, var(--color-base-100));
|
||||||
|
transform: translateY(-50%) scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-side-nav-left {
|
||||||
|
left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-side-nav-right {
|
||||||
|
right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-hover-jump {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
z-index: 3;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 36px;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid color-mix(in oklab, var(--color-primary) 35%, transparent);
|
||||||
|
background: color-mix(in oklab, var(--color-base-100) 86%, transparent);
|
||||||
|
color: color-mix(in oklab, var(--color-base-content) 78%, transparent);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 120ms ease, transform 120ms ease, background-color 120ms ease;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group:hover > .calendar-hover-jump,
|
||||||
|
.group:focus-within > .calendar-hover-jump {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-hover-jump:hover {
|
||||||
|
background: color-mix(in oklab, var(--color-primary) 12%, var(--color-base-100));
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-hover-jump-week,
|
||||||
|
.calendar-hover-jump-row {
|
||||||
|
top: 8px;
|
||||||
|
right: 10px;
|
||||||
|
min-width: 46px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.calendar-content-wrap {
|
||||||
|
padding-left: 32px;
|
||||||
|
padding-right: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-side-nav {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.pilot-shell {
|
.pilot-shell {
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 10% -10%, rgba(124, 144, 255, 0.25), transparent 40%),
|
radial-gradient(circle at 10% -10%, rgba(124, 144, 255, 0.25), transparent 40%),
|
||||||
@@ -5030,6 +5269,38 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
gap: 3px;
|
gap: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pilot-stream-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pilot-stream-caption {
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: rgba(174, 185, 223, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pilot-stream-toggle {
|
||||||
|
border: 1px solid rgba(164, 179, 230, 0.35);
|
||||||
|
background: rgba(25, 33, 56, 0.45);
|
||||||
|
color: rgba(229, 235, 255, 0.92);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.2;
|
||||||
|
padding: 3px 8px;
|
||||||
|
transition: background-color 0.15s ease, border-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pilot-stream-toggle:hover {
|
||||||
|
border-color: rgba(180, 194, 240, 0.6);
|
||||||
|
background: rgba(35, 45, 72, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
.pilot-stream-line {
|
.pilot-stream-line {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user