v0.1.19 - 사이드 네비게이션과 D-DAY 중복 방지 적용

This commit is contained in:
2026-04-22 10:19:23 +09:00
parent fe538fc88b
commit 9b788406ea
6 changed files with 204 additions and 45 deletions

View File

@@ -4,7 +4,7 @@
- 프로젝트명: 10 Minute Planner 웹 UI
- 기술 스택: Vue 3 + Vite + TailwindCSS + JavaScript
- 현재 기준 버전: `v0.1.18`
- 현재 기준 버전: `v0.1.19`
- Git 원격 저장소: `https://git.sori.studio/zenn/planner.sori.studio.git`
## 기준 디자인
@@ -56,6 +56,7 @@
- 프론트 목표 API 클라이언트는 `src/lib/goalsApi.js`에 추가되었다.
- 루트에서 `npm run dev:backend`, `npm run db:generate`, `npm run db:migrate`로 백엔드 명령을 호출할 수 있다.
- 화면 탭은 `PLANNER / STATS / GOALS / SETTINGS` 기준으로 확장되었다.
- 기존 상단 헤더는 왼쪽 사이드 내비게이션으로 재구성되었고, A5 본문이 더 넓게 보이도록 조정되었다.
- 상단 전환 버튼으로 `PLANNER / STATS` 화면을 오갈 수 있다.
- 통계 화면에서는 전체 집중 시간, 평균 완료율, 기록 일수, 최근 7일 흐름, 최근 기록, 베스트 데이를 보여준다.
- 통계 화면은 시작일/종료일을 직접 선택해 그 기간 기준으로 지표를 다시 계산할 수 있다.
@@ -134,6 +135,12 @@
- TASK LABELS도 별도 버튼 2개 대신 동일한 토글 UI로 단순화했다. ON이면 01~15를 채우고 OFF이면 비운다.
- SETTINGS 화면이 추가되어 닉네임, 이메일, 비밀번호 변경을 분리해서 관리할 수 있다.
- 백엔드에는 `/api/auth/profile`, `/api/auth/password`, `/api/goals/:goalId` 수정 API가 추가되었다.
- 왼쪽 사이드, 플래너 본문 래퍼, 오른쪽 정보 패널 모두 둥근 카드 톤으로 맞춰서 화면 전체의 통일감을 높였다.
- 플래너 집중 보기에서는 본문과 오른쪽 패널이 각각 독립 스크롤되도록 바뀌어서 동시에 참조하기 쉽다.
- TASK LABELS, D-DAY 토글은 공통 사이즈의 스위치로 통일했고, `translate` 기반 애니메이션으로 부드럽게 움직이게 했다.
- 목표 생성 폼은 기본적으로 `표시 시작일 = 오늘`, `표시 종료일 = 목표일` 흐름으로 자동 채워진다.
- D-DAY 기간은 서로 겹칠 수 없고, 프론트와 백엔드 모두 중복 기간을 감지하면 저장을 막는다.
- 현재 날짜에 적용된 목표가 있는 경우 D-DAY는 기본적으로 보이고, 해당 날짜에서만 토글로 숨길 수 있다.
- 이미지 저장 기능은 추후 `print-only` 또는 별도 export 전용 레이아웃을 기준으로 구현하면 화면/인쇄/공유 결과를 맞추기 쉽다.
- Docker Compose는 프론트엔드와 백엔드를 함께 올리는 기준으로 설계하되, NAS 환경에 맞는 볼륨과 재시작 정책도 함께 고려한다.

View File

@@ -44,6 +44,7 @@
- [x] 목표를 여러 개 생성하고 날짜별 대표 목표를 선택할 수 있게 한다.
- [x] 목표 관리 화면을 별도로 분리하고, 플래너에서는 D-DAY 표시 ON/OFF만 제어한다.
- [x] 목표별로 D-DAY 표시 시작일과 종료일을 설정할 수 있게 한다.
- [x] 목표 표시 기간이 서로 겹치면 저장되지 않도록 막는다.
- [ ] 목표 완료 처리와 보관 상태를 구분한다.
- [ ] 목표 편집/삭제 UI를 추가한다.
- [ ] 목표 목록 정렬 규칙과 검색 UX를 다듬는다.
@@ -79,6 +80,8 @@
- [ ] 회원 가입 / 로그인 방식 후보를 정리한다.
- [x] 회원 가입 / 로그인 방식 후보를 정리한다.
- [x] 사용자 설정 화면에서 닉네임 / 이메일 / 비밀번호 수정 흐름을 분리한다.
- [x] 상단 헤더를 왼쪽 사이드 내비게이션 구조로 재배치한다.
- [x] 본문과 오른쪽 패널이 각각 독립 스크롤되도록 조정한다.
- [ ] 사용자별 문서 분리 저장 구조를 설계한다.
- [ ] 공유가 아닌 개인 보관용 서비스 흐름으로 요구사항을 정리한다.
- [x] 향후 출력 기능을 위한 인쇄 레이아웃 요구사항을 정리한다.
@@ -111,5 +114,8 @@
- 현재는 각 날짜 플래너가 대표 목표 하나를 선택해 `D-DAY`에 연결하는 구조다.
- 목표는 별도 GOALS 화면에서 검색/생성/기간 설정을 관리하고, 플래너에서는 표시 ON/OFF만 다룬다.
- 목표가 현재 날짜에 적용되지 않았거나 `D-DAY 사용`이 꺼져 있으면 본문 `D-DAY` 영역은 숨긴다.
- 현재 날짜에 적용된 목표가 있으면 D-DAY는 기본적으로 보이고, 사용자가 해당 날짜에서만 OFF로 끌 수 있다.
- 목표 생성 시 표시 시작일 기본값은 오늘, 표시 종료일 기본값은 목표일로 맞춘다.
- 표시 기간이 다른 진행 중 목표와 겹치면 프론트와 백엔드 모두 저장을 막는다.
- TASK LABELS는 버튼 묶음 대신 동일한 토글 패턴으로 단순화했다.
- 구현할 때마다 완료된 항목은 체크하고, 큰 결정사항은 `HANDOFF.md`에도 함께 반영한다.

View File

@@ -40,6 +40,39 @@ async function requireAuthenticatedUser(request, reply) {
return user
}
function hasGoalRangeOverlap(leftStart, leftEnd, rightStart, rightEnd) {
return leftStart <= rightEnd && leftEnd >= rightStart
}
async function validateGoalSchedule({
userId,
activeFrom,
activeUntil,
status,
excludeGoalId = null,
}) {
if (!activeFrom || !activeUntil || status !== 'active') {
return null
}
const existingGoals = await db
.select()
.from(goals)
.where(eq(goals.userId, userId))
return existingGoals.find((goal) => {
if (excludeGoalId && goal.id === excludeGoalId) {
return false
}
if (goal.status !== 'active' || !goal.activeFrom || !goal.activeUntil) {
return false
}
return hasGoalRangeOverlap(activeFrom, activeUntil, goal.activeFrom, goal.activeUntil)
}) ?? null
}
export async function registerGoalRoutes(app) {
app.get('/api/goals', async (request, reply) => {
const user = await requireAuthenticatedUser(request, reply)
@@ -92,6 +125,31 @@ export async function registerGoalRoutes(app) {
})
}
if ((payload.data.activeFrom && !payload.data.activeUntil) || (!payload.data.activeFrom && payload.data.activeUntil)) {
return reply.code(400).send({
message: '표시 시작일과 종료일은 함께 입력해 주세요.',
})
}
if (payload.data.activeFrom && payload.data.activeUntil && payload.data.activeFrom > payload.data.activeUntil) {
return reply.code(400).send({
message: '표시 종료일은 시작일보다 빠를 수 없습니다.',
})
}
const overlappedGoal = await validateGoalSchedule({
userId: user.id,
activeFrom: payload.data.activeFrom ?? null,
activeUntil: payload.data.activeUntil ?? null,
status: payload.data.status ?? 'active',
})
if (overlappedGoal) {
return reply.code(409).send({
message: `표시 기간이 "${overlappedGoal.title}" 목표와 겹칩니다. D-DAY 기간은 하나만 설정할 수 있습니다.`,
})
}
const now = new Date()
const [goal] = await db
@@ -154,6 +212,36 @@ export async function registerGoalRoutes(app) {
})
}
const nextActiveFrom = payload.data.activeFrom !== undefined ? payload.data.activeFrom : existingGoal.activeFrom
const nextActiveUntil = payload.data.activeUntil !== undefined ? payload.data.activeUntil : existingGoal.activeUntil
const nextStatus = payload.data.status ?? existingGoal.status
if ((nextActiveFrom && !nextActiveUntil) || (!nextActiveFrom && nextActiveUntil)) {
return reply.code(400).send({
message: '표시 시작일과 종료일은 함께 입력해 주세요.',
})
}
if (nextActiveFrom && nextActiveUntil && nextActiveFrom > nextActiveUntil) {
return reply.code(400).send({
message: '표시 종료일은 시작일보다 빠를 수 없습니다.',
})
}
const overlappedGoal = await validateGoalSchedule({
userId: user.id,
activeFrom: nextActiveFrom,
activeUntil: nextActiveUntil,
status: nextStatus,
excludeGoalId: existingGoal.id,
})
if (overlappedGoal) {
return reply.code(409).send({
message: `표시 기간이 "${overlappedGoal.title}" 목표와 겹칩니다. D-DAY 기간은 하나만 설정할 수 있습니다.`,
})
}
const nextValues = {
updatedAt: new Date(),
}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "ten-minute-planner",
"version": "0.1.18",
"version": "0.1.19",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ten-minute-planner",
"version": "0.1.18",
"version": "0.1.19",
"dependencies": {
"vue": "^3.5.13"
},

View File

@@ -1,7 +1,7 @@
{
"name": "ten-minute-planner",
"private": true,
"version": "0.1.18",
"version": "0.1.19",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -210,7 +210,7 @@ function startOfDay(date) {
function buildFallbackRecord(date) {
return {
comment: '',
goalEnabled: false,
goalEnabled: true,
selectedGoalId: null,
tasks: Array.from({ length: 15 }, (_, index) => ({
label: '',
@@ -230,7 +230,7 @@ function buildFallbackRecord(date) {
function normalizeRecord(record) {
return {
...record,
goalEnabled: Boolean(record.goalEnabled),
goalEnabled: record.goalEnabled !== false,
selectedGoalId: record.selectedGoalId ?? null,
tasks: record.tasks.map((task, index) => ({
label: task.label ?? task.id ?? '',
@@ -709,6 +709,28 @@ function setSyncFeedback(status, message, options = {}) {
}
}
function getTodayKey() {
return toKey(new Date())
}
function findOverlappingGoal({ activeFrom, activeUntil, status, excludeGoalId = null }) {
if (!activeFrom || !activeUntil || status !== 'active') {
return null
}
return goals.value.find((goal) => {
if (excludeGoalId && goal.id === excludeGoalId) {
return false
}
if (goal.status !== 'active' || !goal.activeFrom || !goal.activeUntil) {
return false
}
return activeFrom <= goal.activeUntil && activeUntil >= goal.activeFrom
}) ?? null
}
function resetAuthForm() {
authForm.nickname = ''
authForm.email = ''
@@ -718,7 +740,7 @@ function resetAuthForm() {
function resetGoalForm() {
goalForm.title = ''
goalForm.targetDate = ''
goalForm.activeFrom = ''
goalForm.activeFrom = getTodayKey()
goalForm.activeUntil = ''
goalForm.status = 'active'
editingGoalId.value = null
@@ -875,6 +897,17 @@ async function submitGoal() {
return
}
const overlappedGoal = findOverlappingGoal({
activeFrom: goalForm.activeFrom || null,
activeUntil: goalForm.activeUntil || null,
status: goalForm.status,
})
if (overlappedGoal) {
goalMessage.value = `"${overlappedGoal.title}" 목표와 표시 기간이 겹칩니다. D-DAY 기간은 하나만 설정할 수 있습니다.`
return
}
goalBusy.value = true
goalMessage.value = ''
@@ -900,6 +933,14 @@ async function submitGoal() {
function updateGoalFormField({ field, value }) {
goalForm[field] = value
if (field === 'targetDate' && !editingGoalId.value) {
if (!goalForm.activeFrom) {
goalForm.activeFrom = getTodayKey()
}
goalForm.activeUntil = value
}
}
function startGoalEdit(goal) {
@@ -932,6 +973,18 @@ async function saveGoalEdit() {
return
}
const overlappedGoal = findOverlappingGoal({
activeFrom: goalForm.activeFrom || null,
activeUntil: goalForm.activeUntil || null,
status: goalForm.status,
excludeGoalId: editingGoalId.value,
})
if (overlappedGoal) {
goalMessage.value = `"${overlappedGoal.title}" 목표와 표시 기간이 겹칩니다. D-DAY 기간은 하나만 설정할 수 있습니다.`
return
}
goalBusy.value = true
goalMessage.value = ''
@@ -1172,6 +1225,7 @@ async function printSelectedPlanner(layout = 'single') {
}
onMounted(() => {
resetGoalForm()
setSyncFeedback('local', '로그인 후 클라우드 저장을 사용할 수 있습니다.', {
visible: false,
})
@@ -1180,9 +1234,9 @@ onMounted(() => {
</script>
<template>
<main class="min-h-screen px-4 py-6 text-ink sm:px-6 lg:px-10">
<div class="print-root mx-auto flex max-w-[1760px] flex-col gap-6 xl:grid xl:grid-cols-[280px_minmax(0,1fr)] xl:items-start">
<aside class="print-hidden rounded-[28px] border border-white/60 bg-white/70 p-5 backdrop-blur sm:p-6 xl:sticky xl:top-6">
<main class="min-h-screen px-4 py-6 text-ink sm:px-6 lg:px-10 xl:h-screen xl:overflow-hidden">
<div class="print-root mx-auto flex max-w-[1760px] flex-col gap-6 xl:h-[calc(100vh-3rem)] xl:grid xl:grid-cols-[300px_minmax(0,1fr)] xl:items-start">
<aside class="print-hidden rounded-[28px] border border-white/60 bg-white/70 p-5 backdrop-blur sm:p-6 xl:h-full xl:overflow-y-auto">
<div class="space-y-6">
<div class="space-y-2">
<p class="text-[11px] font-bold uppercase tracking-[0.28em] text-stone-500">10 Minute Planner</p>
@@ -1231,40 +1285,44 @@ onMounted(() => {
</template>
</section>
<section v-if="isAuthenticated" class="rounded-[24px] border border-stone-200 bg-white/80 p-4">
<section v-if="isAuthenticated" class="rounded-[24px] border border-stone-200 bg-white/85 p-4 shadow-[0_10px_30px_rgba(28,25,23,0.04)]">
<p class="text-[10px] font-bold tracking-[0.16em] text-stone-500">NAVIGATION</p>
<div class="mt-4 grid grid-cols-2 gap-2">
<div class="mt-4 grid gap-2">
<button
type="button"
class="rounded-2xl px-3 py-3 text-xs font-bold tracking-[0.14em] transition"
:class="screenMode === 'planner' ? 'bg-stone-900 text-white' : 'bg-stone-100 text-stone-500'"
class="rounded-[20px] border px-4 py-4 text-left transition"
:class="screenMode === 'planner' ? 'border-stone-900 bg-stone-900 text-white shadow-[0_12px_24px_rgba(28,25,23,0.18)]' : 'border-stone-200 bg-white text-stone-700 hover:border-stone-400'"
@click="screenMode = 'planner'"
>
PLANNER
<p class="text-xs font-bold tracking-[0.18em]">PLANNER</p>
<p class="mt-1 text-[11px] font-semibold tracking-[0.04em]" :class="screenMode === 'planner' ? 'text-stone-200' : 'text-stone-500'">오늘 문서 작성과 캘린더 이동</p>
</button>
<button
type="button"
class="rounded-2xl px-3 py-3 text-xs font-bold tracking-[0.14em] transition"
:class="screenMode === 'stats' ? 'bg-stone-900 text-white' : 'bg-stone-100 text-stone-500'"
class="rounded-[20px] border px-4 py-4 text-left transition"
:class="screenMode === 'stats' ? 'border-stone-900 bg-stone-900 text-white shadow-[0_12px_24px_rgba(28,25,23,0.18)]' : 'border-stone-200 bg-white text-stone-700 hover:border-stone-400'"
@click="screenMode = 'stats'"
>
STATS
<p class="text-xs font-bold tracking-[0.18em]">STATS</p>
<p class="mt-1 text-[11px] font-semibold tracking-[0.04em]" :class="screenMode === 'stats' ? 'text-stone-200' : 'text-stone-500'">기간별 집중 시간과 완료율</p>
</button>
<button
type="button"
class="rounded-2xl px-3 py-3 text-xs font-bold tracking-[0.14em] transition"
:class="screenMode === 'goals' ? 'bg-stone-900 text-white' : 'bg-stone-100 text-stone-500'"
class="rounded-[20px] border px-4 py-4 text-left transition"
:class="screenMode === 'goals' ? 'border-stone-900 bg-stone-900 text-white shadow-[0_12px_24px_rgba(28,25,23,0.18)]' : 'border-stone-200 bg-white text-stone-700 hover:border-stone-400'"
@click="screenMode = 'goals'"
>
GOALS
<p class="text-xs font-bold tracking-[0.18em]">GOALS</p>
<p class="mt-1 text-[11px] font-semibold tracking-[0.04em]" :class="screenMode === 'goals' ? 'text-stone-200' : 'text-stone-500'">D-DAY 목표와 표시 기간 관리</p>
</button>
<button
type="button"
class="rounded-2xl px-3 py-3 text-xs font-bold tracking-[0.14em] transition"
:class="screenMode === 'settings' ? 'bg-stone-900 text-white' : 'bg-stone-100 text-stone-500'"
class="rounded-[20px] border px-4 py-4 text-left transition"
:class="screenMode === 'settings' ? 'border-stone-900 bg-stone-900 text-white shadow-[0_12px_24px_rgba(28,25,23,0.18)]' : 'border-stone-200 bg-white text-stone-700 hover:border-stone-400'"
@click="screenMode = 'settings'"
>
SETTINGS
<p class="text-xs font-bold tracking-[0.18em]">SETTINGS</p>
<p class="mt-1 text-[11px] font-semibold tracking-[0.04em]" :class="screenMode === 'settings' ? 'text-stone-200' : 'text-stone-500'">계정 정보와 비밀번호 수정</p>
</button>
</div>
</section>
@@ -1333,10 +1391,10 @@ onMounted(() => {
</div>
</aside>
<div class="min-w-0 space-y-6">
<div class="min-w-0 space-y-6 xl:h-full xl:overflow-hidden">
<section
v-if="!isAuthenticated"
class="print-hidden rounded-[32px] border border-white/60 bg-white/65 p-6 shadow-[0_24px_80px_rgba(28,25,23,0.08)] sm:p-8"
class="print-hidden rounded-[28px] border border-white/60 bg-white/65 p-6 shadow-[0_24px_80px_rgba(28,25,23,0.08)] sm:p-8 xl:h-full xl:overflow-y-auto"
>
<div class="mx-auto flex max-w-3xl flex-col gap-6 text-center">
<div class="space-y-3">
@@ -1393,9 +1451,9 @@ onMounted(() => {
<section
v-else-if="screenMode === 'planner' && viewMode === 'focus'"
class="print-hidden grid gap-6 xl:grid-cols-[minmax(0,1fr)_320px]"
class="print-hidden grid gap-6 xl:h-full xl:min-h-0 xl:grid-cols-[minmax(0,1fr)_340px]"
>
<div class="print-target">
<div class="print-target rounded-[28px] border border-white/60 bg-white/45 p-4 shadow-[0_18px_60px_rgba(28,25,23,0.06)] xl:h-full xl:min-h-0 xl:overflow-y-auto xl:pr-3">
<PlannerPage
:date-main="selectedDateDisplay.main"
:date-weekday="selectedDateDisplay.weekday"
@@ -1418,8 +1476,8 @@ onMounted(() => {
/>
</div>
<aside class="print-hidden flex flex-col gap-4">
<section class="border border-stone-200 bg-white/80 p-5">
<aside class="print-hidden flex flex-col gap-4 xl:h-full xl:min-h-0 xl:overflow-y-auto xl:pr-2">
<section class="rounded-[24px] border border-stone-200 bg-white/82 p-5 shadow-[0_12px_36px_rgba(28,25,23,0.05)]">
<p class="mb-4 text-[11px] font-bold tracking-[0.22em] text-ink">PREV SNAPSHOT</p>
<div class="space-y-3">
<p
@@ -1432,7 +1490,7 @@ onMounted(() => {
</div>
</section>
<section class="border border-stone-200 bg-white/80 p-5">
<section class="rounded-[24px] border border-stone-200 bg-white/82 p-5 shadow-[0_12px_36px_rgba(28,25,23,0.05)]">
<div class="flex items-start justify-between gap-3">
<div>
<p class="text-[11px] font-bold tracking-[0.22em] text-ink">TASK LABELS</p>
@@ -1442,19 +1500,19 @@ onMounted(() => {
</div>
<button
type="button"
class="relative h-8 w-16 shrink-0 rounded-full transition"
class="relative h-8 w-16 shrink-0 rounded-full transition-colors duration-300 ease-out"
:class="areTaskLabelsNumbered(planner) ? 'bg-stone-900' : 'bg-stone-300'"
@click="areTaskLabelsNumbered(planner) ? clearTaskLabels(planner) : fillTaskLabelsWithNumbers(planner)"
>
<span
class="absolute top-1 h-6 w-6 rounded-full bg-white shadow-sm transition"
:class="areTaskLabelsNumbered(planner) ? 'left-9' : 'left-1'"
class="absolute left-1 top-1 h-6 w-6 transform-gpu rounded-full bg-white shadow-sm transition-transform duration-300 ease-out will-change-transform"
:class="areTaskLabelsNumbered(planner) ? 'translate-x-8' : 'translate-x-0'"
/>
</button>
</div>
</section>
<section class="border border-stone-200 bg-white/80 p-5">
<section class="rounded-[24px] border border-stone-200 bg-white/82 p-5 shadow-[0_12px_36px_rgba(28,25,23,0.05)]">
<p class="mb-4 text-[11px] font-bold tracking-[0.22em] text-ink">READ NEXT</p>
<div class="space-y-3">
<p
@@ -1467,7 +1525,7 @@ onMounted(() => {
</div>
</section>
<section class="border border-stone-200 bg-white/80 p-5">
<section class="rounded-[24px] border border-stone-200 bg-white/82 p-5 shadow-[0_12px_36px_rgba(28,25,23,0.05)]">
<div class="flex items-start justify-between gap-3">
<div>
<p class="text-[11px] font-bold tracking-[0.22em] text-ink">D-DAY 사용</p>
@@ -1477,14 +1535,14 @@ onMounted(() => {
</div>
<button
type="button"
class="relative h-8 w-16 shrink-0 rounded-full transition disabled:cursor-not-allowed disabled:opacity-50"
class="relative h-8 w-16 shrink-0 rounded-full transition-colors duration-300 ease-out disabled:cursor-not-allowed disabled:opacity-50"
:class="planner.goalEnabled ? 'bg-stone-900' : 'bg-stone-300'"
:disabled="!hasActiveGoalForSelectedDate"
@click="updateGoalEnabled(planner, !planner.goalEnabled)"
>
<span
class="absolute top-1 h-6 w-6 rounded-full bg-white shadow-sm transition"
:class="planner.goalEnabled ? 'left-9' : 'left-1'"
class="absolute left-1 top-1 h-6 w-6 transform-gpu rounded-full bg-white shadow-sm transition-transform duration-300 ease-out will-change-transform"
:class="planner.goalEnabled ? 'translate-x-8' : 'translate-x-0'"
/>
</button>
</div>
@@ -1525,7 +1583,7 @@ onMounted(() => {
/>
<section class="grid grid-cols-2 gap-4">
<article class="border border-stone-200 bg-white/80 p-5">
<article class="rounded-[24px] border border-stone-200 bg-white/82 p-5 shadow-[0_12px_36px_rgba(28,25,23,0.05)]">
<p class="text-[11px] font-bold tracking-[0.22em] text-ink">STATS</p>
<div class="mt-5 space-y-4">
<div>
@@ -1539,7 +1597,7 @@ onMounted(() => {
</div>
</article>
<article class="border border-stone-200 bg-white/80 p-5">
<article class="rounded-[24px] border border-stone-200 bg-white/82 p-5 shadow-[0_12px_36px_rgba(28,25,23,0.05)]">
<p class="text-[11px] font-bold tracking-[0.22em] text-ink">NEXT DAY</p>
<div class="mt-5 space-y-3">
<p class="text-lg font-semibold tracking-[-0.04em] text-stone-900">
@@ -1557,7 +1615,7 @@ onMounted(() => {
<section
v-else-if="screenMode === 'planner'"
class="print-hidden overflow-x-auto rounded-[32px] border border-white/60 bg-white/40 p-4 sm:p-6"
class="print-hidden overflow-x-auto rounded-[28px] border border-white/60 bg-white/45 p-4 shadow-[0_18px_60px_rgba(28,25,23,0.06)] sm:p-6 xl:h-full xl:overflow-y-auto"
>
<div class="flex min-w-[1260px] gap-6">
<div class="print-target">
@@ -1609,7 +1667,7 @@ onMounted(() => {
<GoalsDashboard
v-else-if="screenMode === 'goals'"
class="print-hidden"
class="print-hidden xl:h-full xl:overflow-y-auto"
:goals="filteredGoals"
:query="goalQuery"
:status="goalStatusFilter"
@@ -1629,7 +1687,7 @@ onMounted(() => {
<SettingsDashboard
v-else-if="screenMode === 'settings'"
class="print-hidden"
class="print-hidden xl:h-full xl:overflow-y-auto"
:user="currentUser"
:profile-form="profileForm"
:password-form="passwordForm"
@@ -1645,7 +1703,7 @@ onMounted(() => {
<StatsDashboard
v-else
class="print-hidden"
class="print-hidden xl:h-full xl:overflow-y-auto"
:overview-cards="overviewCards"
:weekly-records="weeklyRecords"
:recent-records="recentRecords"