diff --git a/package.json b/package.json index cd62b73..4134401 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "@fullcalendar/core": "^6.1.20", "@fullcalendar/daygrid": "^6.1.20", "@fullcalendar/interaction": "^6.1.20", + "@fullcalendar/list": "^6.1.20", "@fullcalendar/react": "^6.1.20", "@fullcalendar/timegrid": "^6.1.20", "canvas-confetti": "^1.9.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d61da71..6033721 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@fullcalendar/interaction': specifier: ^6.1.20 version: 6.1.20(@fullcalendar/core@6.1.20) + '@fullcalendar/list': + specifier: ^6.1.20 + version: 6.1.20(@fullcalendar/core@6.1.20) '@fullcalendar/react': specifier: ^6.1.20 version: 6.1.20(@fullcalendar/core@6.1.20)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -976,6 +979,11 @@ packages: peerDependencies: '@fullcalendar/core': ~6.1.20 + '@fullcalendar/list@6.1.20': + resolution: {integrity: sha512-7Hzkbb7uuSqrXwTyD0Ld/7SwWNxPD6SlU548vtkIpH55rZ4qquwtwYdMPgorHos5OynHA4OUrZNcH51CjrCf2g==} + peerDependencies: + '@fullcalendar/core': ~6.1.20 + '@fullcalendar/react@6.1.20': resolution: {integrity: sha512-1w0pZtceaUdfAnxMSCGHCQalhi+mR1jOe76sXzyAXpcPz/Lf0zHSdcGK/U2XpZlnQgQtBZW+d+QBnnzVQKCxAA==} peerDependencies: @@ -5810,6 +5818,10 @@ snapshots: dependencies: '@fullcalendar/core': 6.1.20 + '@fullcalendar/list@6.1.20(@fullcalendar/core@6.1.20)': + dependencies: + '@fullcalendar/core': 6.1.20 + '@fullcalendar/react@6.1.20(@fullcalendar/core@6.1.20)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@fullcalendar/core': 6.1.20 diff --git a/src/app/globals.css b/src/app/globals.css index 2a8fa90..63e63e5 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1424,3 +1424,90 @@ select { .animate-emoji-float { animation: emojiFloat 1.8s ease-out both; } + +/* ── Calendar min-height CSS variable ───────────────────────────── */ +/* Used by calendar-shell so the grid fills the viewport correctly */ +/* on both desktop (topbar only) and mobile (topbar + bottom nav). */ +:root { + --cal-topbar-h: 57px; + --cal-nav-h: 0px; +} +:where(html[data-nav="bottom"]), +:where(html[data-nav="fab"]) { + --cal-nav-h: 62px; +} + +/* ── Mobile overrides (≤ 759 px) ────────────────────────────────── */ +@media (max-width: 759px) { + /* Prevent iOS Safari from zooming into inputs (requires ≥ 16 px) */ + .input, + input, + select, + textarea { + font-size: 16px; + } + + /* Topbar: tighten padding; avatar is redundant (Settings in bottom nav) */ + .topbar { + padding: 10px 14px; + gap: 8px; + } + .topbar-actions > .avatar { + display: none; + } + + /* Keyboard shortcut hints are meaningless on touch */ + .kbd { + display: none; + } + + /* Eliminate 300 ms tap delay on interactive controls */ + .btn, + .checkbox, + .nav-item, + .list-row, + .fab, + .bottom-nav button, + .bottom-nav a { + touch-action: manipulation; + } + + /* Momentum scrolling + prevent page bounce fighting in-app scroll */ + .scroll-area { + -webkit-overflow-scrolling: touch; + overscroll-behavior-y: contain; + } + + /* Checkbox: keep the 18 px visual but expand the tap area to 44 px */ + .checkbox { + position: relative; + flex-shrink: 0; + } + .checkbox::after { + content: ""; + position: absolute; + inset: -13px; /* 18 + 2×13 = 44 px tap target */ + } + + /* Delete button on list rows: always visible on touch (no hover state) */ + .list-row-delete { + opacity: 1 !important; + } +} + +/* Landscape phone: compress bottom nav to icon-only to reclaim vertical space */ +@media (max-width: 759px) and (orientation: landscape) and (max-height: 500px) { + .bottom-nav { + height: 44px; + padding: 2px 8px env(safe-area-inset-bottom, 2px); + } + .bottom-nav .nav-label { + display: none; + } + :where(html[data-nav="bottom"]) .scroll-area { + padding-bottom: 52px; + } + .fab { + bottom: 50px !important; + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index ba61772..fd48537 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -42,6 +42,7 @@ const jetbrains = JetBrains_Mono({ export const viewport: Viewport = { themeColor: "#1F1B16", + viewportFit: "cover", }; export const metadata: Metadata = { diff --git a/src/modules/calendar/components/calendar-shell.tsx b/src/modules/calendar/components/calendar-shell.tsx index 7bd064d..5fb1ea1 100644 --- a/src/modules/calendar/components/calendar-shell.tsx +++ b/src/modules/calendar/components/calendar-shell.tsx @@ -4,10 +4,11 @@ import FullCalendar from "@fullcalendar/react"; import dayGridPlugin from "@fullcalendar/daygrid"; import interactionPlugin from "@fullcalendar/interaction"; import timeGridPlugin from "@fullcalendar/timegrid"; +import listPlugin from "@fullcalendar/list"; import type { DateSelectArg, EventClickArg, EventDropArg } from "@fullcalendar/core"; import type { EventResizeDoneArg } from "@fullcalendar/interaction"; -import { CalendarPlus, Check, Eye, EyeOff, Plus, Trash2 } from "lucide-react"; -import { useMemo, useState, useTransition } from "react"; +import { CalendarPlus, Check, Eye, EyeOff, Plus, Trash2, X } from "lucide-react"; +import { useMemo, useState, useEffect, useTransition } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -44,6 +45,8 @@ type EventDraft = { remindMinutesBefore: number | null; }; +type MobileView = "day" | "week" | "agenda"; + const DEFAULT_COLOR = "#B85C3C"; const VIEW_MAP: Record = { @@ -52,8 +55,13 @@ const VIEW_MAP: Record = { day: "timeGridDay", }; +const MOBILE_VIEW_MAP: Record = { + day: "timeGridDay", + week: "timeGridWeek", + agenda: "listWeek", +}; + function withAlpha(hex: string, alpha: number): string { - // Accept #RGB / #RRGGBB / non-hex (return as-is for non-hex e.g. var(--)) if (!hex.startsWith("#")) return hex; let h = hex.slice(1); if (h.length === 3) @@ -88,6 +96,17 @@ export function CalendarShell({ ); const [lastCalendarId, setLastCalendarId] = useState(calendars[0]?.id ?? ""); const [isPending, startTransition] = useTransition(); + const [isMobile, setIsMobile] = useState(false); + const [showCalendarsPanel, setShowCalendarsPanel] = useState(false); + const [mobileView, setMobileView] = useState("day"); + + useEffect(() => { + const mq = window.matchMedia("(max-width: 759px)"); + const apply = () => setIsMobile(mq.matches); + apply(); + mq.addEventListener("change", apply); + return () => mq.removeEventListener("change", apply); + }, []); const defaultCalendarId = calendarRows.some((calendar) => calendar.id === lastCalendarId) ? lastCalendarId @@ -289,106 +308,49 @@ export function CalendarShell({ }); } - return ( -
-