Mobile fixes: - Add viewport meta tag (was missing, causing broken mobile rendering) - Make header nav controls visible on mobile (opacity-0 group-hover was invisible on touch) - Add touch swipe gestures for day navigation on mobile - Fix viewDays responsive override after profile load Bug fixes: - Fix goal save (WeeklyView used POST but route only had GET/PUT; fix to use PUT, add POST alias) - Fix body key mismatch (goal → text) in goal save request Auth & identity (Task 1): - Add accountNumber (auto-increment) to User model for stable identity - Fix OAuth flows: pass user CUID in state instead of email - Google/Outlook callbacks now look up users by ID, not email - Non-Gmail users can now connect Google Calendar/Tasks CalDAV performance (Task 5): - Embed CalDAV object URL in Apple event IDs (caldav::<url>::<uid> format) - Delete/update now use O(1) direct URL access instead of O(n) full calendar scan - Optimistic UI removal on delete (no blocking force-refresh) - Legacy fallback for old-format IDs during transition Apple Calendar data (Task 2): - Pass URL, location, recurringEventId, isRecurring through full pipeline - Add url, recurringEventId, isRecurring to CachedCalendarEvent schema - Calendar cache now reads/writes all new fields Projects (Task 6): - New Project model (name, icon, color, description, order) - CRUD API at /api/tasks/projects - Tasks now support optional projectId with cascading nullify Quotes (Task 4): - Curated local quote database (50 quotes, DE+EN) with tag filtering - Preset quote source APIs (ZenQuotes, Quotable, Forismatic, Type.fit) Fonts (Task 7): - FontPicker component with searchable dropdown and live preview - /api/fonts endpoint (Google Fonts API proxy with 24h cache, fallback to 40 popular fonts) Quick Settings (Task 8): - QuickSettingsSidebar component (font size, spacing, show completed, start day, show lines) - New user preferences: showCompletedTasks, showLines, startDayOffset, quoteSourceUrls v1.9.0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
206 lines
8.0 KiB
TypeScript
206 lines
8.0 KiB
TypeScript
"use client";
|
|
import { X, Type, Space, CheckSquare, Calendar, Minus } from "lucide-react";
|
|
|
|
interface QuickSettingsProps {
|
|
fontSize: string;
|
|
onFontSizeChange: (size: string) => void;
|
|
spacing: string;
|
|
onSpacingChange: (spacing: string) => void;
|
|
showCompleted: boolean;
|
|
onShowCompletedChange: (show: boolean) => void;
|
|
startDayOffset: number;
|
|
onStartDayOffsetChange: (offset: number) => void;
|
|
showLines: boolean;
|
|
onShowLinesChange: (show: boolean) => void;
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
darkMode?: boolean;
|
|
}
|
|
|
|
export default function QuickSettingsSidebar({
|
|
fontSize,
|
|
onFontSizeChange,
|
|
spacing,
|
|
onSpacingChange,
|
|
showCompleted,
|
|
onShowCompletedChange,
|
|
startDayOffset,
|
|
onStartDayOffsetChange,
|
|
showLines,
|
|
onShowLinesChange,
|
|
isOpen,
|
|
onClose,
|
|
darkMode,
|
|
}: QuickSettingsProps) {
|
|
const bg = darkMode ? "#1f2937" : "#ffffff";
|
|
const text = darkMode ? "#e5e7eb" : "#333333";
|
|
const border = darkMode ? "#374151" : "#e5e7eb";
|
|
const accent = "#0ea5e9";
|
|
const btnBg = darkMode ? "#374151" : "#f3f4f6";
|
|
const btnActiveBg = accent;
|
|
const btnActiveText = "#ffffff";
|
|
const labelColor = darkMode ? "#9ca3af" : "#6b7280";
|
|
|
|
const sizes = ["S", "M", "L"];
|
|
|
|
const SegmentedButton = ({
|
|
options,
|
|
value,
|
|
onChange,
|
|
}: {
|
|
options: string[];
|
|
value: string;
|
|
onChange: (v: string) => void;
|
|
}) => (
|
|
<div style={{ display: "flex", gap: "2px", background: btnBg, borderRadius: "8px", padding: "2px" }}>
|
|
{options.map((opt) => (
|
|
<button
|
|
key={opt}
|
|
onClick={() => onChange(opt)}
|
|
style={{
|
|
flex: 1,
|
|
padding: "6px 10px",
|
|
border: "none",
|
|
borderRadius: "6px",
|
|
cursor: "pointer",
|
|
fontSize: "0.75rem",
|
|
fontWeight: value === opt ? 600 : 400,
|
|
background: value === opt ? btnActiveBg : "transparent",
|
|
color: value === opt ? btnActiveText : text,
|
|
transition: "all 0.15s ease",
|
|
}}
|
|
>
|
|
{opt}
|
|
</button>
|
|
))}
|
|
</div>
|
|
);
|
|
|
|
const Toggle = ({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) => (
|
|
<button
|
|
onClick={() => onChange(!checked)}
|
|
style={{
|
|
width: "36px",
|
|
height: "20px",
|
|
borderRadius: "10px",
|
|
border: "none",
|
|
background: checked ? accent : (darkMode ? "#4b5563" : "#d1d5db"),
|
|
cursor: "pointer",
|
|
position: "relative",
|
|
transition: "background 0.2s ease",
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
width: "16px",
|
|
height: "16px",
|
|
borderRadius: "50%",
|
|
background: "#fff",
|
|
position: "absolute",
|
|
top: "2px",
|
|
left: checked ? "18px" : "2px",
|
|
transition: "left 0.2s ease",
|
|
boxShadow: "0 1px 3px rgba(0,0,0,0.2)",
|
|
}}
|
|
/>
|
|
</button>
|
|
);
|
|
|
|
return (
|
|
<>
|
|
{/* Backdrop */}
|
|
{isOpen && (
|
|
<div
|
|
onClick={onClose}
|
|
style={{
|
|
position: "fixed",
|
|
inset: 0,
|
|
background: "rgba(0,0,0,0.1)",
|
|
zIndex: 998,
|
|
}}
|
|
/>
|
|
)}
|
|
{/* Panel */}
|
|
<div
|
|
style={{
|
|
position: "fixed",
|
|
right: 0,
|
|
top: 0,
|
|
bottom: 0,
|
|
width: isOpen ? "220px" : "0",
|
|
overflow: "hidden",
|
|
background: bg,
|
|
borderLeft: isOpen ? `1px solid ${border}` : "none",
|
|
transition: "width 0.2s ease",
|
|
zIndex: 999,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
}}
|
|
>
|
|
<div style={{ padding: "16px", display: "flex", flexDirection: "column", gap: "20px", overflowY: "auto", flex: 1 }}>
|
|
{/* Header */}
|
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
|
<span style={{ fontWeight: 600, fontSize: "0.85rem", color: text }}>Quick Settings</span>
|
|
<button
|
|
onClick={onClose}
|
|
style={{ background: "none", border: "none", cursor: "pointer", color: labelColor, padding: "2px" }}
|
|
>
|
|
<X size={16} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Font Size */}
|
|
<div>
|
|
<div style={{ display: "flex", alignItems: "center", gap: "6px", marginBottom: "6px" }}>
|
|
<Type size={14} style={{ color: labelColor }} />
|
|
<span style={{ fontSize: "0.75rem", color: labelColor }}>Font Size</span>
|
|
</div>
|
|
<SegmentedButton options={sizes} value={fontSize} onChange={onFontSizeChange} />
|
|
</div>
|
|
|
|
{/* Spacing */}
|
|
<div>
|
|
<div style={{ display: "flex", alignItems: "center", gap: "6px", marginBottom: "6px" }}>
|
|
<Space size={14} style={{ color: labelColor }} />
|
|
<span style={{ fontSize: "0.75rem", color: labelColor }}>Spacing</span>
|
|
</div>
|
|
<SegmentedButton options={sizes} value={spacing} onChange={onSpacingChange} />
|
|
</div>
|
|
|
|
{/* Show Completed */}
|
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
|
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
|
<CheckSquare size={14} style={{ color: labelColor }} />
|
|
<span style={{ fontSize: "0.75rem", color: labelColor }}>Show Completed</span>
|
|
</div>
|
|
<Toggle checked={showCompleted} onChange={onShowCompletedChange} />
|
|
</div>
|
|
|
|
{/* Start Day */}
|
|
<div>
|
|
<div style={{ display: "flex", alignItems: "center", gap: "6px", marginBottom: "6px" }}>
|
|
<Calendar size={14} style={{ color: labelColor }} />
|
|
<span style={{ fontSize: "0.75rem", color: labelColor }}>Start View</span>
|
|
</div>
|
|
<SegmentedButton
|
|
options={["Yesterday", "Today"]}
|
|
value={startDayOffset === -1 ? "Yesterday" : "Today"}
|
|
onChange={(v) => onStartDayOffsetChange(v === "Yesterday" ? -1 : 0)}
|
|
/>
|
|
</div>
|
|
|
|
{/* Show Lines */}
|
|
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
|
<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
|
|
<Minus size={14} style={{ color: labelColor }} />
|
|
<span style={{ fontSize: "0.75rem", color: labelColor }}>Show Lines</span>
|
|
</div>
|
|
<Toggle checked={showLines} onChange={onShowLinesChange} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|