diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..521a9f7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true diff --git a/apps/web/index.css b/apps/web/index.css index 23fece8..d9a377d 100644 --- a/apps/web/index.css +++ b/apps/web/index.css @@ -1,690 +1,56 @@ -@import url("https://fonts.googleapis.com/css2?family=Crimson+Pro:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&family=Inter:wght@300;400;500;600;700&display=swap"); +@import url('https://fonts.googleapis.com/css2?family=Crimson+Pro:ital,wght@0,400;0,600;1,400;1,600&family=Nunito:wght@400;600;700&family=VT323&display=swap'); @tailwind base; @tailwind components; @tailwind utilities; -:root { - --accent: 184 149 108; - /* #b8956c - Antique Gold */ - --accent-dark: 212 181 139; - /* #d4b58b - Parchment Gold */ - --radius: 16px; - --shadow-color: 28 24 20; - --shadow-elevation-low: 0 1px 2px rgba(var(--shadow-color), 0.06), 0 2px 4px rgba(var(--shadow-color), 0.04); - --shadow-elevation-medium: 0 4px 8px rgba(var(--shadow-color), 0.08), 0 8px 16px rgba(var(--shadow-color), 0.06); - --shadow-elevation-high: 0 8px 16px rgba(var(--shadow-color), 0.1), 0 16px 32px rgba(var(--shadow-color), 0.08); - --page-max-width: 84rem; - --page-gutter: clamp(1rem, 3vw, 2rem); - --page-gutter-tight: clamp(0.75rem, 2vw, 1.25rem); -} - -.dark { - --shadow-color: 0 0 0; -} - @layer base { - html { - scroll-behavior: smooth; - font-size: 16px; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - } + :root { + /* New Palette: Cozy Library */ + --paper-cream: 253 251 247; /* #FDFBF7 */ + --aged-paper: 245 230 211; /* #F5E6D3 */ + --ink-navy: 28 42 58; /* #1C2A3A */ + --sepia-brown: 62 39 35; /* #3E2723 */ + --sage-green: 126 156 142; /* #7E9C8E */ + --woodstock-gold: 255 213 79; /* #FFD54F */ + --clay-red: 211 84 0; /* #D35400 */ - body { - overflow-x: hidden; - letter-spacing: -0.01em; - font-family: 'Inter', system-ui, sans-serif; - font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11'; - /* Grainy texture overlay logic handled in main app wrapper for better performance */ - } - - ::selection { - background-color: rgba(184, 149, 108, 0.3); - color: inherit; + /* Pixel Art Touches */ + --pixel-border: 2px solid rgb(var(--ink-navy)); } - .dark ::selection { - background-color: rgba(212, 181, 139, 0.3); - } - - h1, - h2, - h3, - h4, - h5, - h6 { - font-family: 'Crimson Pro', Georgia, serif; - letter-spacing: -0.025em; - font-weight: 600; - } - - /* Scrollbar Styling */ - * { - scrollbar-width: thin; - scrollbar-color: rgba(var(--accent), 0.2) transparent; - } - - *::-webkit-scrollbar { - width: 6px; - height: 6px; - } - - *::-webkit-scrollbar-track { - background: transparent; - } - - *::-webkit-scrollbar-thumb { - background-color: rgba(var(--accent), 0.2); - border-radius: 99px; - border: 1px solid transparent; - background-clip: content-box; - transition: background-color 0.2s; - } - - *::-webkit-scrollbar-thumb:hover { - background-color: rgba(var(--accent), 0.4); - } - - .dark *::-webkit-scrollbar-thumb { - background-color: rgba(var(--accent-dark), 0.2); + body { + @apply bg-[rgb(var(--paper-cream))] text-[rgb(var(--ink-navy))] font-sans antialiased selection:bg-[rgb(var(--woodstock-gold))] selection:text-[rgb(var(--ink-navy))]; } - .dark *::-webkit-scrollbar-thumb:hover { - background-color: rgba(var(--accent-dark), 0.4); + h1, h2, h3, h4, h5, h6 { + @apply font-serif font-semibold; } } @layer components { - - /* Layout Classes */ - .standard-layout { - @apply transition-all duration-300 ease-out; - } - - .immersive-layout { - @apply transition-all duration-500 ease-out; - } - - .standard-main { - @apply transition-all duration-300 ease-out; - padding-top: clamp(5.25rem, 8vw, 6rem); - padding-bottom: clamp(7rem, 10vw, 8.5rem); - padding-inline: env(safe-area-inset-left) env(safe-area-inset-right); - } - - .reader-main { - @apply transition-all duration-500 ease-out; - } - - .page-shell { - width: min(100%, var(--page-max-width)); - margin-inline: auto; - padding-inline: var(--page-gutter); - } - - .page-narrow { - width: min(100%, 56rem); - margin-inline: auto; - padding-inline: var(--page-gutter-tight); - } - - .page-stack { - @apply space-y-8; - padding-bottom: clamp(3rem, 8vw, 4.5rem); - } - - /* Glass Effects */ - .glass { - @apply bg-light-surface dark:bg-dark-surface border border-black/[0.08] dark:border-white/[0.08]; - box-shadow: 0 2px 10px rgba(var(--shadow-color), 0.08); - } - - .glass-strong { - @apply bg-light-surface dark:bg-dark-surface border border-black/[0.08] dark:border-white/[0.08]; - box-shadow: 0 6px 18px rgba(var(--shadow-color), 0.12); - } - - .glass-ultra { - @apply bg-light-surface dark:bg-dark-surface border border-black/[0.08] dark:border-white/[0.08]; - box-shadow: 0 8px 24px rgba(var(--shadow-color), 0.14); - } - - /* Text Effects */ - .gradient-text { - @apply text-light-text dark:text-dark-text; - } - - /* Glow Effects */ - .glow-sm { - box-shadow: 0 0 20px rgba(var(--accent), 0.15), 0 0 40px rgba(var(--accent), 0.08); - } - - .dark .glow-sm { - box-shadow: 0 0 20px rgba(var(--accent-dark), 0.12), 0 0 40px rgba(var(--accent-dark), 0.06); - } - - .glow-md { - box-shadow: 0 0 32px rgba(var(--accent), 0.2), 0 0 64px rgba(var(--accent), 0.1); - } - - .dark .glow-md { - box-shadow: 0 0 32px rgba(var(--accent-dark), 0.18), 0 0 64px rgba(var(--accent-dark), 0.08); - } - - .glow-lg { - box-shadow: 0 0 48px rgba(var(--accent), 0.25), 0 0 96px rgba(var(--accent), 0.12); - } - - .dark .glow-lg { - box-shadow: 0 0 48px rgba(var(--accent-dark), 0.22), 0 0 96px rgba(var(--accent-dark), 0.1); - } - - /* Button Components */ - .btn-primary { - @apply relative inline-flex items-center justify-center gap-2.5 rounded-2xl px-6 py-3.5 text-sm font-semibold text-white overflow-hidden transition-all duration-200 ease-out; - background: rgb(var(--accent)); - box-shadow: 0 2px 8px rgba(var(--shadow-color), 0.15); - } - - .btn-primary:hover { - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(var(--shadow-color), 0.2); - } - - .btn-primary:active { - transform: translateY(-1px); - transition-duration: 100ms; - } - - .dark .btn-primary { - background: rgb(var(--accent-dark)); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); - } - - .dark .btn-primary:hover { - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35); - } - - .btn-secondary { - @apply inline-flex items-center justify-center gap-2.5 rounded-2xl bg-light-surface dark:bg-dark-surface px-6 py-3.5 text-sm font-semibold text-light-text dark:text-dark-text border border-black/[0.08] dark:border-white/[0.08] transition-all duration-200 ease-out; - box-shadow: var(--shadow-elevation-low); - } - - .btn-secondary:hover { - @apply bg-light-card dark:bg-dark-card border-black/[0.12] dark:border-white/[0.12]; - transform: translateY(-1px); - box-shadow: var(--shadow-elevation-medium); - } - - .btn-ghost { - @apply inline-flex items-center justify-center gap-2 rounded-xl px-4 py-2.5 text-sm font-medium text-light-text-muted dark:text-dark-text-muted transition-all duration-150 ease-out; - } - - .btn-ghost:hover { - @apply bg-black/[0.05] dark:bg-white/[0.05] text-light-text dark:text-dark-text; - } - - /* Card Components */ - .card { - @apply bg-light-surface dark:bg-dark-surface rounded-2xl border border-black/[0.08] dark:border-white/[0.08] transition-colors duration-200; - box-shadow: 0 2px 10px rgba(var(--shadow-color), 0.08); - } - - .card-hover:hover { - transform: translateY(-1px); - box-shadow: 0 8px 20px rgba(var(--shadow-color), 0.14); - border-color: rgba(var(--accent), 0.35); - } - - .dark .card-hover:hover { - border-color: rgba(var(--accent-dark), 0.25); - } - - .card-interactive { - @apply cursor-pointer; - } - - .card-interactive:active { - transform: translateY(0); - transition-duration: 100ms; - } - - /* Input Components */ - .input { - @apply w-full rounded-2xl bg-black/[0.04] dark:bg-white/[0.04] border border-black/[0.08] dark:border-white/[0.08] px-5 py-4 text-light-text dark:text-dark-text placeholder:text-light-text-muted/60 dark:placeholder:text-dark-text-muted/60 transition-all duration-200 ease-out; - font-feature-settings: 'tnum'; - } - - .input:focus { - @apply outline-none bg-white dark:bg-dark-surface border-light-accent/60 dark:border-dark-accent/60; - box-shadow: 0 0 0 4px rgba(var(--accent), 0.12), var(--shadow-elevation-low); - } - - .dark .input:focus { - box-shadow: 0 0 0 4px rgba(var(--accent-dark), 0.15), var(--shadow-elevation-low); - } - - /* Toggle Components */ - .toggle { - @apply relative inline-flex items-center h-7 w-12 rounded-full cursor-pointer transition-all duration-300 ease-out; - background: linear-gradient(135deg, #e5e7eb 0%, #f3f4f6 100%); - } - - .toggle.active { - background: linear-gradient(135deg, rgb(var(--accent)) 0%, #d97706 100%); - } - - .dark .toggle { - background: linear-gradient(135deg, #374151 0%, #4b5563 100%); - } - - .dark .toggle.active { - background: linear-gradient(135deg, rgb(var(--accent-dark)) 0%, #f59e0b 100%); - } - - .toggle-thumb { - @apply absolute w-5 h-5 rounded-full bg-white shadow-lg transition-all duration-300 ease-out; - left: 4px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); - } - - .toggle.active .toggle-thumb { - transform: translateX(20px); - } - - .dark .toggle-thumb { - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); - } - - /* Range Input Styling */ - .range-input { - -webkit-appearance: none; - appearance: none; - background: transparent; - cursor: pointer; - height: 8px; - border-radius: 4px; - } - - .range-input::-webkit-slider-runnable-track { - height: 8px; - border-radius: 4px; - background: rgba(var(--accent), 0.18); - } - - .dark .range-input::-webkit-slider-runnable-track { - background: rgba(var(--accent-dark), 0.2); - } - - .range-input::-webkit-slider-thumb { - -webkit-appearance: none; - width: 20px; - height: 20px; - border-radius: 50%; - background: rgb(var(--accent)); - box-shadow: 0 2px 12px rgba(var(--accent), 0.4); - margin-top: -6px; - transition: all 0.2s ease-out; - } - - .dark .range-input::-webkit-slider-thumb { - background: rgb(var(--accent-dark)); - box-shadow: 0 2px 12px rgba(var(--accent-dark), 0.35); - } - - .range-input::-webkit-slider-thumb:hover { - transform: scale(1.2); - box-shadow: 0 4px 20px rgba(var(--accent), 0.5); - } - - .dark .range-input::-webkit-slider-thumb:hover { - box-shadow: 0 4px 20px rgba(var(--accent-dark), 0.45); - } -} - -@layer utilities { - - /* Layout Utilities */ - .container-fluid { - @apply w-full max-w-none; - padding-inline: var(--page-gutter); - } - - .container-narrow { - @apply max-w-4xl mx-auto; - padding-inline: var(--page-gutter); - } - - .container-wide { - @apply mx-auto; - width: min(100%, var(--page-max-width)); - padding-inline: var(--page-gutter); - } - - /* Spacing Utilities */ - .space-y-fluid>*+* { - margin-top: clamp(1rem, 2.5vw, 2rem); - } - - .space-x-fluid>*+* { - margin-left: clamp(0.5rem, 1.5vw, 1rem); - } - - /* Typography Utilities */ - .text-balance { - text-wrap: balance; + /* Cozy Primitives */ + .card-paper { + @apply bg-white border border-[rgb(var(--aged-paper))] rounded-xl shadow-sm transition-all duration-300; } - .text-pretty { - text-wrap: pretty; + .btn-cozy { + @apply inline-flex items-center justify-center rounded-full px-4 py-2 font-medium transition-all duration-200 active:scale-95; + @apply bg-[rgb(var(--ink-navy))] text-[rgb(var(--paper-cream))] hover:bg-[rgb(var(--sage-green))]; } - /* Focus Utilities */ - .focus-ring { - @apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-light-accent/50 dark:focus-visible:ring-dark-accent/50 focus-visible:ring-offset-2 focus-visible:ring-offset-light-primary dark:focus-visible:ring-offset-dark-primary; + .input-underline { + @apply bg-transparent border-b-2 border-[rgb(var(--aged-paper))] px-2 py-1 outline-none transition-colors focus:border-[rgb(var(--sage-green))]; } - /* Animation Utilities */ - .animate-in { - animation: fadeInUp 0.5s ease-out forwards; - } - - .animate-out { - animation: fadeOut 0.3s ease-out forwards; - } - - /* Gradient Utilities */ - .bg-gradient-radial { - background-image: radial-gradient(circle, var(--tw-gradient-stops)); - } - - .bg-gradient-conic { - background-image: conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops)); - } - - /* Book-specific utilities */ - .book-spine-shadow { - box-shadow: inset -8px 0 16px -8px rgba(0, 0, 0, 0.4); - } - - .book-cover-glow { - box-shadow: 0 8px 32px rgba(var(--accent), 0.15), 0 16px 64px rgba(var(--accent), 0.08); - } - - .dark .book-cover-glow { - box-shadow: 0 8px 32px rgba(var(--accent-dark), 0.12), 0 16px 64px rgba(var(--accent-dark), 0.06); - } - - /* Scrollbar Utilities */ - .scrollbar-hide { - -ms-overflow-style: none; - /* IE and Edge */ - scrollbar-width: none; - /* Firefox */ - } - - .scrollbar-hide::-webkit-scrollbar { - display: none; - } -} - -/* Enhanced Animations */ -@keyframes fadeIn { - from { - opacity: 0; - } - - to { - opacity: 1; - } -} - -@keyframes fadeOut { - from { - opacity: 1; - } - - to { - opacity: 0; - } -} - -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(24px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes fadeInDown { - from { - opacity: 0; - transform: translateY(-24px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes scaleIn { - from { - opacity: 0; - transform: scale(0.95); - } - - to { - opacity: 1; - transform: scale(1); - } -} - -@keyframes slideInLeft { - from { - opacity: 0; - transform: translateX(-32px); - } - - to { - opacity: 1; - transform: translateX(0); - } -} - -@keyframes slideInRight { - from { - opacity: 0; - transform: translateX(32px); - } - - to { - opacity: 1; - transform: translateX(0); - } -} - -@keyframes shimmer { - 0% { - transform: translateX(-100%); - } - - 100% { - transform: translateX(100%); - } -} - -@keyframes float { - - 0%, - 100% { - transform: translateY(0px) rotate(0deg); - } - - 33% { - transform: translateY(-10px) rotate(1deg); - } - - 66% { - transform: translateY(-5px) rotate(-1deg); - } -} - -@keyframes pulse-soft { - - 0%, - 100% { - opacity: 1; - } - - 50% { - opacity: 0.7; - } -} - -@keyframes gradient-shift { - - 0%, - 100% { - background-position: 0% 50%; - } - - 50% { - background-position: 100% 50%; - } -} - -@keyframes bounce-gentle { - - 0%, - 100% { - transform: translateY(0); - } - - 50% { - transform: translateY(-4px); - } -} - -/* Animation Classes */ -.animate-fadeIn { - animation: fadeIn 0.4s ease-out forwards; -} - -.animate-fadeOut { - animation: fadeOut 0.3s ease-out forwards; -} - -.animate-fadeInUp { - animation: fadeInUp 0.5s ease-out forwards; -} - -.animate-fadeInDown { - animation: fadeInDown 0.5s ease-out forwards; -} - -.animate-scaleIn { - animation: scaleIn 0.3s ease-out forwards; -} - -.animate-slideInLeft { - animation: slideInLeft 0.4s ease-out forwards; -} - -.animate-slideInRight { - animation: slideInRight 0.4s ease-out forwards; -} - -.animate-shimmer { - animation: shimmer 2.5s infinite; -} - -.animate-float { - animation: float 6s ease-in-out infinite; -} - -.animate-pulse-soft { - animation: pulse-soft 2.5s ease-in-out infinite; -} - -.animate-bounce-gentle { - animation: bounce-gentle 2s ease-in-out infinite; -} - -/* Stagger Delays */ -.stagger-1 { - animation-delay: 100ms; -} - -.stagger-2 { - animation-delay: 200ms; -} - -.stagger-3 { - animation-delay: 300ms; -} - -.stagger-4 { - animation-delay: 400ms; -} - -.stagger-5 { - animation-delay: 500ms; -} - -.stagger-6 { - animation-delay: 600ms; -} - -/* Reduced Motion Support */ -@media (prefers-reduced-motion: reduce) { - - .reduce-motion *, - .reduce-motion *::before, - .reduce-motion *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - scroll-behavior: auto !important; - } - - .reduce-motion .animate-float, - .reduce-motion .animate-bounce-gentle, - .reduce-motion .animate-pulse-soft { - animation: none !important; - } -} - -/* High Contrast Mode */ -@media (prefers-contrast: high) { - .card { - @apply border-2 border-black/20 dark:border-white/20; - } - - .btn-primary { - @apply border-2 border-black/20; - } - - .input { - @apply border-2 border-black/20 dark:border-white/20; - } -} - -/* Print Styles */ -@media print { - .no-print { - display: none !important; + /* Pixel Art */ + .pixel-border { + border: 2px solid rgb(var(--ink-navy)); + box-shadow: 4px 4px 0px rgb(var(--ink-navy)); } - * { - background: white !important; - color: black !important; - box-shadow: none !important; + .pixel-btn { + @apply font-pixel text-xs px-4 py-2 bg-[rgb(var(--woodstock-gold))] text-[rgb(var(--ink-navy))] uppercase tracking-widest border-2 border-[rgb(var(--ink-navy))] hover:translate-y-0.5 hover:shadow-none active:translate-y-1 transition-all; + box-shadow: 3px 3px 0px rgb(var(--ink-navy)); } } diff --git a/apps/web/public/images/bunnies-stack.jpg b/apps/web/public/images/bunnies-stack.jpg new file mode 100644 index 0000000..d366f45 Binary files /dev/null and b/apps/web/public/images/bunnies-stack.jpg differ diff --git a/apps/web/public/images/dashboard-signin.jpg b/apps/web/public/images/dashboard-signin.jpg new file mode 100644 index 0000000..ae4e389 Binary files /dev/null and b/apps/web/public/images/dashboard-signin.jpg differ diff --git a/apps/web/public/images/snoopy-reading.jpg b/apps/web/public/images/snoopy-reading.jpg new file mode 100644 index 0000000..c69bf8c Binary files /dev/null and b/apps/web/public/images/snoopy-reading.jpg differ diff --git a/apps/web/src/components/pages/Auth.tsx b/apps/web/src/components/pages/Auth.tsx index 1cadeef..752c485 100644 --- a/apps/web/src/components/pages/Auth.tsx +++ b/apps/web/src/components/pages/Auth.tsx @@ -1,6 +1,7 @@ import React from "react"; -import { User, BookOpen } from "lucide-react"; +import { User, BookOpen, Sparkles } from "lucide-react"; import { SignIn } from "@/hooks/useAuth"; +import { motion } from "framer-motion"; interface AuthProps { onContinueAsGuest?: () => void; @@ -8,50 +9,92 @@ interface AuthProps { const Auth: React.FC = ({ onContinueAsGuest }) => { return ( -
-
-
-
-
+
+ {/* Left: Cozy Illustration (Desktop Only) */} +
+ {/* Background Texture */} +
-
-
-
- -
-

Sanctuary

-

Your personal reading haven

+
+
+ Cozy Reading Bunnies + {/* Pixel decoration */} +
+
💤
+
+ +

+ "Just one more chapter..." +

+

+ Join the bunnies in your personal reading sanctuary. +

+
-
-
- -
-
-
-
-
-
- - or - + {/* Right: Login Form (Guest Book Style) */} +
+ {/* Background decoration for mobile */} +
+ + +
+
+
+

Sanctuary

+
+

Please sign the guestbook to enter.

- - -

- Guest data is stored locally on this device -

-
+
+ {/* "Paper" lines background */} +
+ +
+
+ +
+ +
+
+
+
+
+ + Or + +
+
+ + + +

+ (Your reading progress will be saved on this device) +

+
+
+
); diff --git a/apps/web/src/components/pages/LibraryGrid.tsx b/apps/web/src/components/pages/LibraryGrid.tsx index 1a41b52..6089bf3 100644 --- a/apps/web/src/components/pages/LibraryGrid.tsx +++ b/apps/web/src/components/pages/LibraryGrid.tsx @@ -3,6 +3,7 @@ import type { Book, SortOption, FilterOption, ViewMode } from "@/types"; import { Grid3X3, List, SortAsc, Filter, Star, Clock, ChevronRight, ChevronDown, Search, BookOpen } from "lucide-react"; import BookCard from "../ui/BookCard"; import AddBookButton from "../ui/AddBookButton"; +import BunniesPick from "../ui/BunniesPick"; import { useBookStore } from "@/store/useBookStore"; import { useUIStore } from "@/store/useUIStore"; import { useShallow } from "zustand/react/shallow"; @@ -164,7 +165,7 @@ const LibraryGrid: React.FC = ({ ); const HorizontalScroll = ({ books: scrollBooks }: { books: Book[] }) => ( -
+
{scrollBooks.map((book) => (
@@ -217,6 +218,11 @@ const LibraryGrid: React.FC = ({ return (
+ {/* Bunnies' Pick Feature - Only show when filtering "All" and no search term */} + {filterBy === "all" && !searchTerm && books.length > 0 && ( + + )} +

Library

diff --git a/apps/web/src/components/pages/ReaderView.tsx b/apps/web/src/components/pages/ReaderView.tsx index 0e9b85b..ae6819a 100644 --- a/apps/web/src/components/pages/ReaderView.tsx +++ b/apps/web/src/components/pages/ReaderView.tsx @@ -239,22 +239,28 @@ const ReaderView: React.FC = ({
+ {/* Ambient Background Noise/Texture */} +
+ {bookmarkError && ( -
+
{bookmarkError}
)} {contentError && !isLoading && (
-
-

{contentError}

+
+

{contentError}

)} +
void }) => { - const [isEditing, setIsEditing] = useState(false); - const [tempKeys, setTempKeys] = useState([]); - - const startEditing = () => { - setTempKeys([...keys]); - setIsEditing(true); - }; - - const cancelEditing = () => { - setIsEditing(false); - setTempKeys([]); - }; - - const saveEditing = () => { - onChange(tempKeys); - setIsEditing(false); - setTempKeys([]); - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - e.preventDefault(); - const key = e.key; - if (key === "Escape") { - cancelEditing(); - } else if (key === "Enter") { - saveEditing(); - } else if (key === "Backspace") { - if (tempKeys.length > 0) { - setTempKeys(tempKeys.slice(0, -1)); - } else { - onChange([]); - cancelEditing(); - } - } else if (!tempKeys.includes(key)) { - setTempKeys([...tempKeys, key]); - } - }; - - const removeKey = (keyToRemove: string) => { - const newKeys = keys.filter(k => k !== keyToRemove); - onChange(newKeys); - }; - - return ( -
- {label} -
- {isEditing ? ( - - ) : ( -
- {keys.map((key, index) => ( - - - {key === " " ? "Space" : key} - - - - ))} - -
- )} -
-
- ); -}; - -type Tab = "typography" | "layout" | "reading" | "colors" | "shortcuts" | "goals"; - -const COLOR_PRESETS = [ - { id: "light", label: "Paper", fg: "#1a1a1a", bg: "#ffffff", accent: "#8B7355", icon: Sun }, - { id: "cream", label: "Ivory", fg: "#2B2B2B", bg: "#FBF8F3", accent: "#8B7355", icon: Coffee }, - { id: "sepia", label: "Sepia", fg: "#5C4B37", bg: "#F4ECD8", accent: "#8B7355", icon: Droplets }, - { id: "dark", label: "Ink", fg: "#e8e6e3", bg: "#1a1a1a", accent: "#d4b58b", icon: Moon }, - { id: "midnight", label: "Midnight", fg: "#c9d1d9", bg: "#0d1117", accent: "#79c0ff", icon: Moon }, -]; +import { Folder, Volume2, Moon, Sun, Type, Monitor, Sparkles, Brain, Save } from "lucide-react"; +import { Theme } from "@/types"; +import { useUIStore } from "@/store/useUIStore"; +import { motion } from "framer-motion"; const SettingsView: React.FC = () => { - const [activeTab, setActiveTab] = useState("colors"); const { - readerForeground, setReaderForeground, - readerBackground, setReaderBackground, - readerAccent, setReaderAccent, - keybinds, setKeybinds, - dailyGoal, setDailyGoal, - weeklyGoal, setWeeklyGoal, - showStreakReminder, setShowStreakReminder, - trackingEnabled, setTrackingEnabled, - showFloatingCapsule, setShowFloatingCapsule, - resetToDefaults, + theme, + toggleTheme + } = useUIStore(); + + const { + fontSize, + lineHeight, + fontFamily, + textAlign, + reduceMotion, + setFontSize, + setLineHeight, + setFontFamily, + setTextAlign, + setReduceMotion, } = useSettingsShallow((state) => ({ - readerForeground: state.readerForeground, - setReaderForeground: state.setReaderForeground, - readerBackground: state.readerBackground, - setReaderBackground: state.setReaderBackground, - readerAccent: state.readerAccent, - setReaderAccent: state.setReaderAccent, - keybinds: state.keybinds, - setKeybinds: state.setKeybinds, - dailyGoal: state.dailyGoal, - setDailyGoal: state.setDailyGoal, - weeklyGoal: state.weeklyGoal, - setWeeklyGoal: state.setWeeklyGoal, - showStreakReminder: state.showStreakReminder, - setShowStreakReminder: state.setShowStreakReminder, - trackingEnabled: state.trackingEnabled, - setTrackingEnabled: state.setTrackingEnabled, - showFloatingCapsule: state.showFloatingCapsule, - setShowFloatingCapsule: state.setShowFloatingCapsule, - resetToDefaults: state.resetToDefaults, + fontSize: state.fontSize, + lineHeight: state.lineHeight, + fontFamily: state.fontFamily, + textAlign: state.textAlign, + reduceMotion: state.reduceMotion, + setFontSize: state.setFontSize, + setLineHeight: state.setLineHeight, + setFontFamily: state.setFontFamily, + setTextAlign: state.setTextAlign, + setReduceMotion: state.setReduceMotion, })); + // Mock state for new "Organizer" features + const [cozyMode, setCozyMode] = React.useState(true); + const [aiAssistant, setAiAssistant] = React.useState(false); // Default off/hidden + const tabs = [ - { id: "colors" as Tab, label: "Colors", icon: Palette, description: "Theme" }, - { id: "shortcuts" as Tab, label: "Shortcuts", icon: Zap, description: "Keybinds" }, - { id: "goals" as Tab, label: "Goals", icon: Target, description: "Tracking" }, + { id: "general", label: "General", icon: Folder }, + { id: "reading", label: "Reading", icon: Type }, + { id: "advanced", label: "Advanced", icon: Brain }, ]; + const [activeTab, setActiveTab] = React.useState("general"); - // Premium Toggle Component - const Toggle = ({ checked, onChange, label, sublabel }: { checked: boolean; onChange: (v: boolean) => void; label: string; sublabel?: string }) => ( + const TabButton = ({ id, label, icon: Icon }: { id: string, label: string, icon: any }) => ( ); - // Premium Slider Component - const Slider = ({ - value, - onChange, - min, - max, - step = 1, - label, - displayValue, - icon: Icon, - }: { - value: number; - onChange: (v: number) => void; - min: number; - max: number; - step?: number; - label: string; - displayValue?: string; - icon?: React.ElementType; - }) => { - const percentage = ((value - min) / (max - min)) * 100; - - return ( -
-
-
- {Icon && ( -
- -
- )} - {label} -
-
- - {displayValue || value} - -
-
-
-
-
-
- onChange(parseFloat(e.target.value))} - className="absolute inset-0 w-full opacity-0 cursor-pointer" - /> -
-
-
- ); - }; + return ( +
+

Settings Organizer

- // Premium Section Component - const Section = ({ title, icon: Icon, children }: { title: string; icon?: React.ElementType; children: React.ReactNode }) => ( -
-
-
- {Icon && ( -
- -
- )} -

{title}

-
-
{children}
+ {/* Folder Tabs */} +
+ {tabs.map(tab => )}
-
- ); - - // Color Swatch - const ColorSwatch = ({ - preset, - isActive, - onClick, - }: { - preset: typeof COLOR_PRESETS[0]; - isActive: boolean; - onClick: () => void; - }) => { - const Icon = preset.icon; - return ( - - ); - }; + {/* Folder Content Area */} +
- return ( -
- {/* Hero Header */} -
-
-
-
-
- -
-

Settings

-
-

- Craft your perfect reading experience with personalized typography, colors, and layout preferences. -

-
- - -
-
- - {/* Premium Tab Navigation */} -
-
- {tabs.map((tab) => { - const isActive = activeTab === tab.id; - return ( - - ); - })} -
-
+ +
+ + + {/* Cozy Mode */} +
+

+ Atmosphere +

+
+
+

Cozy Mode

+

Enable page turn sounds and gentle animations

+
+ +
- {/* Global Interface Toggles */} -
- -
+
+
+

Reduce Motion

+

Minimize animations for accessibility

+
+ +
+
+ + )} - {/* Tab Content */} -
- {activeTab === "colors" && ( - <> -
-

- - Color Themes -

-
- {COLOR_PRESETS.map((preset) => ( - { - setReaderForeground(preset.fg); - setReaderBackground(preset.bg); - setReaderAccent(preset.accent); - }} - /> + {activeTab === "reading" && ( + +
+

+ Typography +

+ + {/* Font Family */} +
+ {["Serif", "Sans", "Mono"].map((font) => ( + ))}
-
-
-
- {[ - { label: "Text", value: readerForeground, onChange: setReaderForeground }, - { label: "Background", value: readerBackground, onChange: setReaderBackground }, - { label: "Accent", value: readerAccent, onChange: setReaderAccent }, - ].map(({ label, value, onChange }) => ( -
- + + )} - {activeTab === "shortcuts" && ( - <> -
-
-
- Customize keyboard shortcuts for reading navigation. + {activeTab === "advanced" && ( + +
+

+ Intelligence +

+ +
+
+
-
- setKeybinds({ ...keybinds, nextPage: keys })} - /> - setKeybinds({ ...keybinds, prevPage: keys })} - /> - setKeybinds({ ...keybinds, toggleBookmark: keys })} - /> - setKeybinds({ ...keybinds, toggleFullscreen: keys })} - /> - setKeybinds({ ...keybinds, toggleUI: keys })} - /> - setKeybinds({ ...keybinds, close: keys })} - /> +
+

AI Assistant

+

+ The AI is currently hiding in the stacks. It helps with definitions and summaries when asked. +

+
-
- - )} - - {activeTab === "goals" && ( - <> -
- - -
-
-
- - -
-
- +
+
+ +

Data Management

+
+

Your library is stored locally on this device. Sync features coming soon.

+
+
+ )} -
); diff --git a/apps/web/src/components/pages/StatsView.tsx b/apps/web/src/components/pages/StatsView.tsx index 2c78ad2..d8856a9 100644 --- a/apps/web/src/components/pages/StatsView.tsx +++ b/apps/web/src/components/pages/StatsView.tsx @@ -1,521 +1,175 @@ -import React, { useState, useMemo } from "react"; -import type { Badge } from "@/types"; -import { Flame, Trophy, BookOpen, Clock, Target, TrendingUp, BarChart3, PieChart, Zap, Calendar, Award, Star, Users } from "lucide-react"; -import { useSettingsShallow } from "@/context/SettingsContext"; +import React from "react"; import { useStatsStore } from "@/store/useStatsStore"; -import { useShallow } from "zustand/react/shallow"; -import { useUIStore } from "@/store/useUIStore"; -import { View } from "@/types"; - -const ICON_MAP: Record = { - flame: Flame, - trophy: Trophy, - book: BookOpen, - star: Star, - award: Award, - zap: Zap, - target: Target, -}; +import { Book, Award, Clock, Flame, Calendar, Sparkles } from "lucide-react"; +import { motion } from "framer-motion"; const StatsView: React.FC = () => { - const { stats } = useStatsStore(useShallow((state) => ({ - stats: state.stats, - }))); - const { dailyGoal, weeklyGoal, setReadingGoals } = useSettingsShallow((state) => ({ - dailyGoal: state.dailyGoal, - weeklyGoal: state.weeklyGoal, - setReadingGoals: state.setReadingGoals, - })); - const setView = useUIStore((state) => state.setView); - const safeLabel = (value: string, max = 40) => value.length > max ? `${value.slice(0, max - 1)}...` : value; - const onUpdateGoal = (daily: number, weekly: number) => { - setReadingGoals(daily, weekly); - }; - const [activeTab, setActiveTab] = useState<"overview" | "charts" | "badges" | "insights">("overview"); - const weeklyTotal = useMemo(() => stats.weeklyData.reduce((a, d) => a + d.minutes, 0), [stats.weeklyData]); - const dailyAvg = useMemo(() => Math.round(weeklyTotal / 7), [weeklyTotal]); - - const StatCard = ({ - icon: Icon, - label, - value, - subtext, - accent = false, - }: { - icon: React.ElementType; - label: string; - value: string | number; - subtext?: string; - accent?: boolean; - }) => ( -
-
-
- -
-
-

- {label} -

-

{value}

- {subtext && ( -

{subtext}

- )} -
-
-
- ); - - const ProgressRing = ({ progress, size = 80, stroke = 6 }: { progress: number; size?: number; stroke?: number }) => { - const radius = (size - stroke) / 2; - const circumference = radius * 2 * Math.PI; - const offset = circumference - (Math.min(progress, 100) / 100) * circumference; - return ( - - - - - - - - - - - ); - }; + const stats = useStatsStore((state) => state.stats); - const HeatmapCell = ({ level }: { level: number }) => { - const colors = [ - "bg-black/[0.03] dark:bg-white/[0.03]", - "bg-light-accent/20 dark:bg-dark-accent/20", - "bg-light-accent/45 dark:bg-dark-accent/45", - "bg-light-accent dark:bg-dark-accent", + // Mock data for new visualization features + const moodData = [ + { genre: "Fiction", count: 12, color: "bg-[rgb(var(--sage-green))]" }, + { genre: "History", count: 5, color: "bg-[rgb(var(--woodstock-gold))]" }, + { genre: "Sci-Fi", count: 8, color: "bg-[rgb(var(--ink-navy))]" }, ]; - return
; - }; - const BadgeCard = ({ badge }: { badge: Badge }) => { - const IconComponent = ICON_MAP[badge.icon.toLowerCase()] || Award; - return ( -
-
- -
-

{badge.name}

-

{badge.description}

- {badge.target && !badge.unlocked && ( -
-
-
+ // Helper to generate a pixel-art contribution grid (mock) + const renderContributionGrid = () => { + return ( +
+ {[...Array(28)].map((_, i) => ( +
0.7 + ? "bg-[rgb(var(--sage-green))]" + : Math.random() > 0.4 + ? "bg-[rgb(var(--woodstock-gold))]" + : "bg-[rgb(var(--aged-paper))]" + }`} + /> + ))}
-

- {badge.progress}/{badge.target} -

-
- )} - {badge.unlocked && ( - Unlocked - )} -
- ); - }; - - const BarChart = ({ data, maxValue }: { data: { label: string; value: number }[]; maxValue: number }) => ( -
- {data.map((d) => ( -
-
-
0 ? (d.value / maxValue) * 100 : 0}%` }} - /> -
- {d.label} -
- ))} -
- ); + ); + }; - const tabs = [ - { id: "overview", label: "Overview", icon: BarChart3 }, - { id: "charts", label: "Charts", icon: PieChart }, - { id: "badges", label: "Badges", icon: Trophy }, - { id: "insights", label: "Insights", icon: Zap }, - ] as const; - - return ( -
-
-
-

Stats

-

Track your reading

-
-
- -
- {stats.currentStreak} - day streak -
-
-
- -
- {tabs.map((tab) => ( - - ))} -
- - {activeTab === "overview" && ( -
-
-
-

Today

-
-
-
- -
- - {Math.round((stats.dailyProgress / dailyGoal) * 100)}% - + return ( +
+ {/* Journal Header */} +
+
+
-
-
-

- {stats.dailyProgress}{" "} - / {dailyGoal} pages -

-

- {stats.dailyProgress >= dailyGoal ? "Goal achieved" : `${dailyGoal - stats.dailyProgress} pages to go`} -

-
+

Reading Journal

+

"A room without books is like a body without a soul."

-
-
- - - - -
- -
-
-

This Week

- {weeklyTotal} min -
- ({ label: d.day, value: d.minutes }))} - maxValue={Math.max(...stats.weeklyData.map((d) => d.minutes), 1)} - /> -
-

- Weekly goal: {weeklyGoal} pages -

-
- - -
-
-
- -
-
-
- -
-
-

- Reading Style -

-

{stats.readingPersonality}

-

- {stats.personalityDescription} -

-
-
-
-
- )} - - {activeTab === "charts" && ( -
-
-

Activity ({stats.heatmapData.length} weeks)

-
- {stats.heatmapData.map((week, wi) => ( -
- {week.map((level, di) => ( - - ))} -
- ))} -
-
- Less - {[0, 1, 2, 3].map((l) => ( - - ))} - More +
+ +
+
+

Pages Turned

+

{stats.totalPagesRead.toLocaleString()}

+
+ + + {/* Time Reading (Badge Style) */} + +
+ +
+
+

Time Spent

+

+ {Math.round(stats.totalReadingTime / 60)} hours +

+
+
+ + {/* Level / XP (Pixel Progress) */} + +
+ LEVEL 3 BOOKWORM + XP: 850/1000 +
+
+
+
+
-
- -
-

Monthly Hours

- ({ label: d.month, value: d.hours }))} - maxValue={Math.max(...stats.monthlyData.map((d) => d.hours), 1)} - /> -
-
-

Genres

- {stats.genreDistribution.length > 0 ? ( -
- {stats.genreDistribution.map((g, i) => ( -
-
- {safeLabel(g.genre, 28)} - - {g.count} - -
- ))} -
- ) : ( -

Add genres to see distribution

- )} -
- -
-

Top Authors

- {stats.authorNetwork.length > 0 ? ( -
- {stats.authorNetwork.map((a, i) => ( -
-
- + {/* Reading Mood & Calendar */} +
+ {/* Calendar */} +
+
+ +

Consistency Calendar

- {safeLabel(a.author, 28)} - - {a.books} - -
- ))} -
- ) : ( -

Start reading to see favorites

- )} -
-
- )} - - {activeTab === "badges" && ( -
-

- {stats.badges.filter((b) => b.unlocked).length} of {stats.badges.length} unlocked -

-
- {stats.badges.map((badge) => ( - - ))} -
-
- )} - - {activeTab === "insights" && ( -
-
-

Insights

-
- {[ - { - icon: BookOpen, - title: "Completion Rate", - value: stats.totalBooksInLibrary > 0 ? `${Math.round((stats.totalBooksRead / stats.totalBooksInLibrary) * 100)}%` : "N/A", - desc: "Books finished", - }, - { - icon: Clock, - title: "Avg Session", - value: `${stats.totalReadingTime > 0 ? Math.round(stats.totalReadingTime / Math.max(stats.weeklyData.filter((d) => d.minutes > 0).length * 4, 1)) : 0} min`, - desc: "Per sitting", - }, - { - icon: TrendingUp, - title: "Pages/Session", - value: `${stats.averageReadingSpeed > 0 ? Math.round(stats.averageReadingSpeed / 2) : 0}`, - desc: "Average", - }, - { - icon: Target, - title: "Today's Goal", - value: `${Math.round((stats.dailyProgress / dailyGoal) * 100)}%`, - desc: "Progress", - }, - ].map((item) => ( -
-
- -
-
-

{item.title}

-

{item.desc}

-
- {item.value} +
+ {/* Replace with real calendar later */} + {renderContributionGrid()} +
+

KEEP THE FIRE BURNING!

- ))} -
-
-
-

Milestones

-
- {[ - { icon: BookOpen, title: "5 Books", progress: stats.totalBooksRead, target: 5, show: stats.totalBooksRead < 5 }, - { icon: Flame, title: "7 Day Streak", progress: stats.currentStreak, target: 7, show: stats.currentStreak < 7 }, - { icon: Calendar, title: "100 Pages", progress: stats.totalPagesRead, target: 100, show: stats.totalPagesRead < 100 }, - { icon: Clock, title: "10 Hours", progress: stats.totalReadingTime, target: 600, show: stats.totalReadingTime < 600 }, - ] - .filter((m) => m.show) - .map((m) => ( -
-
- + {/* Mood Palette */} +
+
+ +

Reading Mood

-
-
- {m.title} - - {m.progress}/{m.target} - -
-
-
-
+
+ {moodData.map((mood) => ( +
+
+ {mood.genre} + {mood.count} +
+
+
+
+
+ ))}
-
- ))} +
-
-
-

Tips

-
    -
  • - - - Set a consistent reading time daily -
  • -
  • - - - Start with shorter sessions -
  • -
  • - - - Use immersive mode for focus -
  • -
-
- - + {/* Recent Badges Section */} +
+

--- RECENTLY UNLOCKED ---

+
+ {[1, 2, 3].map((i) => ( +
+ +
+ badge_name_0{i} +
+
+ ))} +
+ ? +
+
-
- )} -
- ); + ); }; export default StatsView; diff --git a/apps/web/src/components/ui/BookCard.tsx b/apps/web/src/components/ui/BookCard.tsx index 3d7e11a..cd97aec 100644 --- a/apps/web/src/components/ui/BookCard.tsx +++ b/apps/web/src/components/ui/BookCard.tsx @@ -1,275 +1,202 @@ import React, { useState } from "react"; import type { Book } from "@/types"; -import { Star, Clock, BookOpen, Heart } from "lucide-react"; -import { useSettings } from "@/context/SettingsContext"; +import { Star, BookOpen, Trash2, Clock } from "lucide-react"; +import { useBookStore } from "@/store/useBookStore"; +import { motion, AnimatePresence } from "framer-motion"; interface BookCardProps { book: Book; onSelect: (book: Book) => void; - onToggleFavorite?: (id: string) => void; - variant?: "default" | "compact" | "featured"; + onToggleFavorite: (id: string) => void; + variant?: "standard" | "compact"; } const BookCard: React.FC = ({ book, onSelect, onToggleFavorite, - variant = "default" + variant = "standard", }) => { - const [imageLoaded, setImageLoaded] = useState(false); + const removeBook = useBookStore((state) => state.removeBook); + const [showConfirmDelete, setShowConfirmDelete] = useState(false); const [imageError, setImageError] = useState(false); - const reduceMotion = useSettings((state) => state.reduceMotion); - const progressPercentage = Math.round((book.progress / book.totalPages) * 100); - const isRecent = book.lastOpenedAt && Date.now() - new Date(book.lastOpenedAt).getTime() < 7 * 24 * 60 * 60 * 1000; - const isCompleted = progressPercentage >= 100; - - const handleImageLoad = () => setImageLoaded(true); - const handleImageError = () => setImageError(true); + const handleDelete = (e: React.MouseEvent) => { + e.stopPropagation(); + setShowConfirmDelete(true); + }; - const handleFavoriteClick = (e: React.MouseEvent) => { + const confirmDelete = (e: React.MouseEvent) => { e.stopPropagation(); - onToggleFavorite?.(book.id); + removeBook(book.id); }; - const handleCardKeyDown = (e: React.KeyboardEvent, selectedBook: Book) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - onSelect(selectedBook); - } + + const cancelDelete = (e: React.MouseEvent) => { + e.stopPropagation(); + setShowConfirmDelete(false); }; if (variant === "compact") { + // Compact variant for horizontal scrolls (keep simpler but consistent) return ( -
onSelect(book)} - onKeyDown={(e) => handleCardKeyDown(e, book)} - role="button" - tabIndex={0} - className="group flex items-center gap-4 p-3 rounded-xl border border-black/[0.08] dark:border-white/[0.08] bg-light-surface dark:bg-dark-surface hover:border-light-accent/40 dark:hover:border-dark-accent/40 transition-colors cursor-pointer" + whileHover={{ y: -4 }} > -
-
- {book.coverUrl && !imageError ? ( - {book.title} - ) : ( -
- -
- )} -
- {progressPercentage > 0 && ( -
- {progressPercentage}% +
+ {!imageError ? ( + {book.title} setImageError(true)} + /> + ) : ( +
+ + {book.title} +
)} -
- -
-

- {book.title} -

-

- {book.author} -

-
- -
- {book.isFavorite && ( - - )} - {isRecent && ( - + {/* Progress Bar Overlay */} + {book.progress > 0 && ( +
+
+
)}
-
+

{book.title}

+ ); } - if (variant === "featured") { - return ( -
onSelect(book)} - onKeyDown={(e) => handleCardKeyDown(e, book)} - role="button" - tabIndex={0} - className="group relative overflow-hidden rounded-2xl border border-black/[0.08] dark:border-white/[0.08] bg-light-surface dark:bg-dark-surface hover:border-light-accent/40 dark:hover:border-dark-accent/40 transition-colors cursor-pointer p-5" - > -
-
-
- {book.coverUrl && !imageError ? ( - {book.title} - ) : ( -
- -
- )} -
- - {!reduceMotion && ( -
- )} -
- -
-
-
-

- {book.title} -

-

- {book.author} -

-
- + // Standard 3D Book Card + return ( +
+ + {showConfirmDelete && ( + e.stopPropagation()} + > +

Delete book?

+
+
+
+ )} +
-
- {progressPercentage > 0 && ( -
-
- Progress - {progressPercentage}% -
-
-
-
-
- )} - -
- {isRecent && ( -
- - Recently read -
- )} - {isCompleted && ( -
- - Completed -
- )} + onSelect(book)} + className="cursor-pointer relative transform-style-3d transition-transform duration-300 group-hover:-translate-y-2 group-hover:rotate-y-[-5deg]" + > + {/* Book Spine (Fake 3D) */} +
+ + {/* Main Cover */} +
+ {!imageError ? ( + {book.title} setImageError(true)} + /> + ) : ( +
+
+
+ + {book.title} + + + {book.author} +
-
-
-
- ); - } + )} - // Default variant - return ( -
onSelect(book)} - onKeyDown={(e) => handleCardKeyDown(e, book)} - role="button" - tabIndex={0} - className="group relative overflow-hidden rounded-2xl border border-black/[0.08] dark:border-white/[0.08] bg-light-surface dark:bg-dark-surface hover:border-light-accent/35 dark:hover:border-dark-accent/35 transition-colors cursor-pointer" - > - {/* Book Cover */} -
- {book.coverUrl && !imageError ? ( - {book.title} - ) : ( -
- -
- )} + {/* Shine/Texture Overlay */} +
+
{/* Spine crease */} - {/* Overlay */} - {!reduceMotion &&
} + {/* Badges */} +
+ {book.isFavorite && ( +
+ +
+ )} + {book.progress > 0 && book.progress < 100 && ( +
+ +
+ )} +
+
- {/* Favorite Button */} - + {/* Shadow (Bottom) */} +
- {/* Progress Indicator */} - {progressPercentage > 0 && ( -
-
-
- )} + {/* Info (Below Book) */} +
+

+ {book.title} +

+

+ {book.author} +

- {/* Status Badges */} -
- {isRecent && ( -
- Recent -
- )} - {isCompleted && ( -
- Complete + {/* Progress Bar (Visible on hover or if active) */} + {book.progress > 0 && ( +
+
)}
-
- {/* Book Info */} -
-

- {book.title} -

-

- {book.author} -

- - {progressPercentage > 0 && ( -
- - {book.progress} / {book.totalPages} pages - - - {progressPercentage}% - -
- )} -
-
+ {/* Hover Actions */} +
+ + +
+ +
); }; diff --git a/apps/web/src/components/ui/BunniesPick.tsx b/apps/web/src/components/ui/BunniesPick.tsx new file mode 100644 index 0000000..68e5026 --- /dev/null +++ b/apps/web/src/components/ui/BunniesPick.tsx @@ -0,0 +1,111 @@ +import React, { useMemo } from "react"; +import { Book } from "@/types"; +import { BookOpen, Star, Sparkles } from "lucide-react"; +import { motion } from "framer-motion"; + +interface BunniesPickProps { + books: Book[]; + onSelectBook: (book: Book) => void; +} + +const BunniesPick: React.FC = ({ books, onSelectBook }) => { + // Logic: Prefer a "Favorite" book that is NOT finished. + // If no favorites, pick a random unfinished book. + // If all finished, pick a random book. + const pickedBook = useMemo(() => { + if (!books.length) return null; + + const favorites = books.filter((b) => b.isFavorite && b.readingList !== "finished"); + if (favorites.length > 0) { + return favorites[Math.floor(Math.random() * favorites.length)]; + } + + const unfinished = books.filter((b) => b.readingList !== "finished"); + if (unfinished.length > 0) { + return unfinished[Math.floor(Math.random() * unfinished.length)]; + } + + return books[Math.floor(Math.random() * books.length)]; + }, [books]); + + if (!pickedBook) return null; + + return ( + + {/* Decorative Background Elements */} +
+
+ + {/* Pixel Art Decoration (SVG) */} +
+ + + +
+ +
+ {/* Book Cover with 3D effect */} +
+
+ + {pickedBook.title} + {/* Shine effect */} +
+ +
+ + {/* Content */} +
+
+ + Bunnies' Pick + +
+ +
+

+ {pickedBook.title} +

+

+ by {pickedBook.author} +

+
+ +

+ The bunnies have sniffed around your library and think this is the perfect story for right now. + Curl up with a warm drink and dive in! +

+ +
+ + + {pickedBook.progress > 0 && ( + + {pickedBook.progress}% Complete + + )} +
+
+
+ + ); +}; + +export default BunniesPick; diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js index c2d61a3..0f3c8b5 100644 --- a/apps/web/tailwind.config.js +++ b/apps/web/tailwind.config.js @@ -8,54 +8,42 @@ export default { theme: { extend: { fontFamily: { - sans: ['Inter', 'system-ui', 'sans-serif'], + sans: ['Nunito', 'system-ui', 'sans-serif'], serif: ['Crimson Pro', 'Georgia', 'serif'], mono: ['JetBrains Mono', 'Menlo', 'Monaco', 'monospace'], + pixel: ['"VT323"', 'monospace'], }, colors: { - // Light theme colors + // Light theme colors (Cozy Library) light: { - primary: '#fefcf8', - secondary: '#f8f6f1', + primary: 'rgb(var(--paper-cream))', + secondary: 'rgb(var(--aged-paper))', surface: '#ffffff', - card: '#fdfdfc', - accent: 'rgb(184 149 108)', - text: '#1a1816', - 'text-muted': '#6b6560', - border: '#e8e6e1', + card: '#ffffff', + accent: 'rgb(var(--sage-green))', + text: 'rgb(var(--ink-navy))', + 'text-muted': 'rgb(var(--sepia-brown))', + border: 'rgb(var(--aged-paper))', 'border-muted': '#f0efea', }, - // Dark theme colors + // Dark theme colors (Night Reading) - kept similar structure but warmer dark: { - primary: '#0f0e0d', - secondary: '#1a1917', + primary: '#1A1410', + secondary: '#2C2319', surface: '#252320', card: '#2a2825', - accent: 'rgb(212 181 139)', - text: '#f5f4f2', - 'text-muted': '#a8a29e', - border: '#3a3835', + accent: '#D4AF37', // Gold + text: '#E8DCC8', // Parchment + 'text-muted': '#B8A890', + border: '#3E2723', 'border-muted': '#2f2e2b', }, }, - spacing: { - '18': '4.5rem', - '88': '22rem', - '100': '25rem', - '112': '28rem', - '128': '32rem', - }, - borderRadius: { - '4xl': '2rem', - '5xl': '2.5rem', - }, - fontSize: { - '2xs': ['0.625rem', { lineHeight: '0.75rem' }], - '3xl': ['1.875rem', { lineHeight: '2.25rem' }], - '4xl': ['2.25rem', { lineHeight: '2.5rem' }], - '5xl': ['3rem', { lineHeight: '3.25rem' }], - '6xl': ['3.75rem', { lineHeight: '4rem' }], + boxShadow: { + 'pixel': '4px 4px 0px rgb(var(--ink-navy))', + 'pixel-sm': '2px 2px 0px rgb(var(--ink-navy))', }, + // ... existing spacing/animation configs can stay or be cleaned up animation: { 'fade-in': 'fadeIn 0.4s ease-out', 'fade-out': 'fadeOut 0.3s ease-out', @@ -66,7 +54,6 @@ export default { 'pulse-soft': 'pulseSoft 2.5s ease-in-out infinite', 'float': 'float 6s ease-in-out infinite', 'shimmer': 'shimmer 2.5s infinite', - 'gradient-shift': 'gradientShift 3s ease-in-out infinite', }, keyframes: { fadeIn: { @@ -106,43 +93,6 @@ export default { '0%': { transform: 'translateX(-100%)' }, '100%': { transform: 'translateX(100%)' }, }, - gradientShift: { - '0%, 100%': { backgroundPosition: '0% 50%' }, - '50%': { backgroundPosition: '100% 50%' }, - }, - }, - backdropBlur: { - xs: '2px', - }, - boxShadow: { - 'glow-sm': '0 0 20px rgba(184, 149, 108, 0.15), 0 0 40px rgba(184, 149, 108, 0.08)', - 'glow-md': '0 0 32px rgba(184, 149, 108, 0.2), 0 0 64px rgba(184, 149, 108, 0.1)', - 'glow-lg': '0 0 48px rgba(184, 149, 108, 0.25), 0 0 96px rgba(184, 149, 108, 0.12)', - 'inner-glow': 'inset 0 0 20px rgba(184, 149, 108, 0.1)', - 'book-spine': 'inset -8px 0 16px -8px rgba(0, 0, 0, 0.4)', - 'book-cover': '0 8px 32px rgba(184, 149, 108, 0.15), 0 16px 64px rgba(184, 149, 108, 0.08)', - }, - backgroundImage: { - 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', - 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', - 'noise': "url(\"data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='0.02'/%3E%3C/svg%3E\")", - }, - scale: { - '102': '1.02', - '103': '1.03', - }, - transitionTimingFunction: { - 'bounce-gentle': 'cubic-bezier(0.68, -0.55, 0.265, 1.55)', - 'smooth': 'cubic-bezier(0.4, 0, 0.2, 1)', - 'smooth-in': 'cubic-bezier(0.4, 0, 1, 1)', - 'smooth-out': 'cubic-bezier(0, 0, 0.2, 1)', - }, - zIndex: { - '60': '60', - '70': '70', - '80': '80', - '90': '90', - '100': '100', }, }, }, diff --git a/package-lock.json b/package-lock.json index 61af8ec..5c15263 100644 --- a/package-lock.json +++ b/package-lock.json @@ -557,6 +557,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -573,6 +574,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -588,6 +590,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -603,6 +606,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -620,6 +624,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", @@ -810,6 +815,7 @@ "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -930,6 +936,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1116,6 +1123,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", @@ -1147,6 +1155,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", @@ -1181,6 +1190,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1211,6 +1221,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", @@ -1227,6 +1238,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", @@ -1295,6 +1307,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", @@ -1311,6 +1324,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1326,6 +1340,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", @@ -1342,6 +1357,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1357,6 +1373,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", @@ -1373,6 +1390,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1419,6 +1437,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1452,6 +1471,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1482,6 +1502,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1497,6 +1518,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1512,6 +1534,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.27.1", @@ -1544,6 +1567,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.28.6", @@ -1562,6 +1586,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-transforms": "^7.27.1", @@ -1594,6 +1619,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1609,6 +1635,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1624,6 +1651,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1658,6 +1686,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", @@ -1674,6 +1703,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1689,6 +1719,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", @@ -1753,6 +1784,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1859,6 +1891,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" @@ -1874,6 +1907,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", @@ -1890,6 +1924,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1971,6 +2006,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -1986,6 +2022,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -2020,6 +2057,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" @@ -2035,6 +2073,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", @@ -2067,6 +2106,7 @@ "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.28.5", @@ -2083,6 +2123,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.0.tgz", "integrity": "sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==", + "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.29.0", @@ -2167,6 +2208,7 @@ "version": "0.14.0", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.0.tgz", "integrity": "sha512-AvDcMxJ34W4Wgy4KBIIePQTAOP1Ie2WFwkQp3dB7FQ/f0lI5+nM96zUnYEOE1P9sEg0es5VCP0HxiWu5fUHZAQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.6", @@ -2197,6 +2239,7 @@ "version": "0.1.6-no-external-plugins", "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", @@ -6609,7 +6652,7 @@ }, "node_modules/@types/react": { "version": "19.2.7", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -8698,7 +8741,7 @@ }, "node_modules/csstype": { "version": "3.2.3", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/d": { @@ -9891,20 +9934,6 @@ "expo": "*" } }, - "node_modules/expo-font": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-13.3.2.tgz", - "integrity": "sha512-wUlMdpqURmQ/CNKK/+BIHkDA5nGjMqNlYmW0pJFXY/KE/OG80Qcavdu2sHsL4efAIiNGvYdBS10WztuQYU4X0A==", - "license": "MIT", - "peer": true, - "dependencies": { - "fontfaceobserver": "^2.1.0" - }, - "peerDependencies": { - "expo": "*", - "react": "*" - } - }, "node_modules/expo-keep-awake": { "version": "13.0.2", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-13.0.2.tgz",